Rails 7 个具有 hotwire/turbo 框架的动态嵌套表单?
Rails 7 Dynamic Nested Forms with hotwire/turbo frames?
我是 rails 的新手。我是从 rails7 开始的,所以关于我的问题的信息仍然很少。
这是我的:
app/models/cocktail.rb
class Cocktail < ApplicationRecord
has_many :cocktail_ingredients, dependent: :destroy
has_many :ingredients, through: :cocktail_ingredients
accepts_nested_attributes_for :cocktail_ingredients
end
app/models/ingredient.rb
class Ingredient < ApplicationRecord
has_many :cocktail_ingredients
has_many :cocktails, :through => :cocktail_ingredients
end
app/models/cocktail_ingredient.rb
class CocktailIngredient < ApplicationRecord
belongs_to :cocktail
belongs_to :ingredient
end
app/controllers/cocktails_controller.rb
def new
@cocktail = Cocktail.new
@cocktail.cocktail_ingredients.build
@cocktail.ingredients.build
end
def create
@cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
if @cocktail.save
format.html { redirect_to cocktail_url(@cocktail), notice: "Cocktail was successfully created." }
format.json { render :show, status: :created, location: @cocktail }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @cocktail.errors, status: :unprocessable_entity }
end
end
end
def cocktail_params
params.require(:cocktail).permit(:name, :recipe, cocktail_ingredients_attributes: [:quantity, ingredient_id: []])
end
...
db/seeds.rb
Ingredient.create([ {name: "rum"}, {name: "gin"} ,{name: "coke"}])
架构中的相关表
create_table "cocktail_ingredients", force: :cascade do |t|
t.float "quantity"
t.bigint "ingredient_id", null: false
t.bigint "cocktail_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cocktail_id"], name: "index_cocktail_ingredients_on_cocktail_id"
t.index ["ingredient_id"], name: "index_cocktail_ingredients_on_ingredient_id"
end
create_table "cocktails", force: :cascade do |t|
t.string "name"
t.text "recipe"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
...
add_foreign_key "cocktail_ingredients", "cocktails"
add_foreign_key "cocktail_ingredients", "ingredients"
app/views/cocktails/_form.html.erb
<%= form_for @cocktail do |form| %>
<% if cocktail.errors.any? %>
<% cocktail.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name, value: "aa"%>
</div>
<div>
<%= form.label :recipe, style: "display: block" %>
<%= form.text_area :recipe, value: "nn" %>
</div>
<%= form.simple_fields_for :cocktail_ingredients do |ci| %>
<%= ci.collection_check_boxes(:ingredient_id, Ingredient.all, :id, :name) %>
<%= ci.text_field :quantity, value: "1"%>
<% end %>
<div>
<%= form.submit %>
</div>
<% end %>
当前错误:
鸡尾酒配料成分必须存在
我想要达到的目标:
我想要一个部分,我可以在其中选择 3 种成分中的一种并输入其数量。 add/remove 成分应该有 added/remove 个按钮。
我用什么?涡轮帧?热线?我该怎么做?
我仍然对 rails 中的所有内容感到非常困惑,因此非常感谢深入的回答。
我有一个简单但有点脏的解决方案。看到你是这一切的新手。我认为这可以帮助您在深入研究 Turbo
和所有其他内容之前了解表单本身发生了什么。
此外,使用 accepts_nested_attributes_for
既快捷又有趣;但是,当您不理解它时,它会让您发疯。
为了简单起见,首先让我们修改一下表格并放宽 simple_form
<!-- form_for or form_tag https://guides.rubyonrails.org/form_helpers.html#using-form-tag-and-form-for -->
<!-- form_with does it all -->
<%= form_with model: cocktail do |f| %>
<% if cocktail.errors.any? %>
<% cocktail.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
<% end %>
<div>
<%= f.label :name, style: "display: block" %>
<%= f.text_field :name, value: "aa"%>
</div>
<div>
<%= f.label :recipe, style: "display: block" %>
<%= f.text_area :recipe, value: "nn" %>
</div>
<!-- https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for -->
<%= f.fields_for "cocktail_ingredients" do |ff| %>
<br>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<%= ff.text_field :quantity, value: "1" %>
<div>
<%= ff.label 'Delete' %>
<%= ff.check_box :_destroy %>
</div>
<% end %>
<!-- Yes we're submitting our form, but with a different button -->
<!-- see CocktailController#create -->
<div> <%= f.submit "Add ingredient", name: 'add_ingredient' %> </div>
<div> <%= f.submit %> </div>
<% end %>
collection_check_boxes
不适用于这种情况,我们希望每个记录有一个成分,如 belongs_to :ingredient
所示。 single select
是一个明显的选择; collection_radio_buttons
同样适用。
如果特定记录已保存在数据库中,fields_for
助手还将输出一个隐藏字段,其中“id”为 cocktail_ingredient 记录。这就是 rails 知道更新现有记录(带 id)和创建新记录(不带 id)的方式。
fields_for
还将“_attributes”附加到 record_name 如果表单的模型,在本例中 Cocktail
,为 record_name 定义了 accepts_nested_attributes_for
。
澄清一下,如果你的模型中有这个:
accepts_nested_attributes_for :cocktail_ingredients
这意味着
f.fields_for "cocktail_ingredients"
将在输入名称前加上 cocktail[cocktail_ingredients_attributes]
。
控制器
# GET /cocktails/new
def new
@cocktail = Cocktail.new
# build one new object for our new form; otherwise `fields_for` will not render anything
# with this new form will always have one empty `cocktail_ingredient`
# NOTE: this is necessary when using `accepts_nested_attributes_for`
# NOTE: without `accepts_nested_attributes_for`, `fields_for` will render fields; see what a cluster it is.
@cocktail.cocktail_ingredients.build
end
# POST /cocktails
def create
@cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
# NOTE: catch when we submit our form with add_ingredient button
# params will have "add_ingredient"=>"Add ingredient"
if params[:add_ingredient]
# build another cocktail_ingredient to be rendered by `fields_for` helper
@cocktail.cocktail_ingredients.build
# and just render the form again. tada! you're done.
# NOTE: rails 7 submits as TURBO_STREAM format; hey we're actually using turbo.
# it expects a form to redirect when valid; so we have to use some kind of invalid status.
format.html { render :new, status: :unprocessable_entity }
else
if @cocktail.save
format.html { redirect_to cocktail_url(@cocktail), notice: "Cocktail was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
end
型号
更新 Cocktail
以允许在保存时使用 _destroy
表单字段删除记录。
accepts_nested_attributes_for :cocktail_ingredients, allow_destroy: true
解释的很长,最后我只加了四行代码:
if params[:add_ingredient]
@cocktail.cocktail_ingredients.build
format.html { render :new, status: :unprocessable_entity }
<%= f.submit "Add ingredient", name: 'add_ingredient' %>
希望这是有道理的。这里有很多改进的余地。我认为 cocoon
gem 对于简单的用例仍然是一个可行的选择,如果你想研究一下的话。
更新。添加 turbo-frame
现在,当添加新成分时,整个页面都是 re-rendered。我们可以添加 turbo-frame
以仅更新表格的成分部分:
...
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for "cocktail_ingredients" do |ff| %>
<br>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<%= ff.text_field :quantity %> <%# NOTE: remove preset value %>
<div>
<%= ff.label 'Delete' %>
<%= ff.check_box :_destroy %>
</div>
<% end %>
</turbo-frame>
...
更改 'Add ingredient' 按钮以了解我们要更新的 turbo-frame
。
...
<%= f.submit "Add ingredient",
data: { turbo_frame: f.field_id(:ingredients)},
name: 'add_ingredient' %>
...
Turbo 框架 id
必须匹配按钮 data-turbo-frame
属性。
<turbo-frame id="has_to_match">
<input data-turbo-frame="has_to_match" ...>
现在,当单击“添加成分”按钮时,它仍然会转到同一个控制器,它仍然会在服务器上呈现整个表单,但是现在,re-rendering 整个页面,只有里面的内容turbo-frame
已更新。这意味着,页面滚动保持不变,turbo frame 标签外的表单状态不变。对于所有意图和目的,这现在是一个动态表单。
可能的改进可能是停止扰乱 create
操作并通过不同的控制器操作“添加成分”,例如 add_ingredient
# config/routes.rb
resources :cocktails do
post :add_ingredient
end
...
<%= f.submit 'Add ingredient',
formmethod: 'post',
formaction: '/cocktails/add_ingredient',
data: { turbo_frame: f.field_id(:ingredients)} %>
...
将 add_ingredient
操作添加到 CocktailsController
def add_ingredient
@cocktail = Cocktail.new cocktail_params
@cocktail.cocktail_ingredients.build # add another ingredient
render :new
end
create
操作现在可以恢复为默认值。
我觉得这已经很简单了。这是简短的版本(大约 10 行额外的代码来添加动态字段,并且没有 javascript)
# config/routes.rb
resources :cocktails do
post :add_ingredient
end
# app/controllers/cocktails_controller.rb
# the usual scaffold for other actions
def add_ingredient
@cocktail = Cocktail.new cocktail_params
@cocktail.cocktail_ingredients.build # add another ingredient
render :new
end
# app/views/cocktails/new.html.erb
<%= form_with model: @cocktail do |f| %>
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for :cocktail_ingredients do |ff| %>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<% end %>
</turbo-frame>
<%= f.button "Add ingredient",
formmethod: 'post',
formaction: '/cocktails/add_ingredient',
data: { turbo_frame: f.field_id(:ingredients)},%>
<%= f.submit %>
<% end %>
我是 rails 的新手。我是从 rails7 开始的,所以关于我的问题的信息仍然很少。
这是我的:
app/models/cocktail.rb
class Cocktail < ApplicationRecord
has_many :cocktail_ingredients, dependent: :destroy
has_many :ingredients, through: :cocktail_ingredients
accepts_nested_attributes_for :cocktail_ingredients
end
app/models/ingredient.rb
class Ingredient < ApplicationRecord
has_many :cocktail_ingredients
has_many :cocktails, :through => :cocktail_ingredients
end
app/models/cocktail_ingredient.rb
class CocktailIngredient < ApplicationRecord
belongs_to :cocktail
belongs_to :ingredient
end
app/controllers/cocktails_controller.rb
def new
@cocktail = Cocktail.new
@cocktail.cocktail_ingredients.build
@cocktail.ingredients.build
end
def create
@cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
if @cocktail.save
format.html { redirect_to cocktail_url(@cocktail), notice: "Cocktail was successfully created." }
format.json { render :show, status: :created, location: @cocktail }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @cocktail.errors, status: :unprocessable_entity }
end
end
end
def cocktail_params
params.require(:cocktail).permit(:name, :recipe, cocktail_ingredients_attributes: [:quantity, ingredient_id: []])
end
...
db/seeds.rb
Ingredient.create([ {name: "rum"}, {name: "gin"} ,{name: "coke"}])
架构中的相关表
create_table "cocktail_ingredients", force: :cascade do |t|
t.float "quantity"
t.bigint "ingredient_id", null: false
t.bigint "cocktail_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cocktail_id"], name: "index_cocktail_ingredients_on_cocktail_id"
t.index ["ingredient_id"], name: "index_cocktail_ingredients_on_ingredient_id"
end
create_table "cocktails", force: :cascade do |t|
t.string "name"
t.text "recipe"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
...
add_foreign_key "cocktail_ingredients", "cocktails"
add_foreign_key "cocktail_ingredients", "ingredients"
app/views/cocktails/_form.html.erb
<%= form_for @cocktail do |form| %>
<% if cocktail.errors.any? %>
<% cocktail.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name, value: "aa"%>
</div>
<div>
<%= form.label :recipe, style: "display: block" %>
<%= form.text_area :recipe, value: "nn" %>
</div>
<%= form.simple_fields_for :cocktail_ingredients do |ci| %>
<%= ci.collection_check_boxes(:ingredient_id, Ingredient.all, :id, :name) %>
<%= ci.text_field :quantity, value: "1"%>
<% end %>
<div>
<%= form.submit %>
</div>
<% end %>
当前错误:
鸡尾酒配料成分必须存在
我想要达到的目标:
我想要一个部分,我可以在其中选择 3 种成分中的一种并输入其数量。 add/remove 成分应该有 added/remove 个按钮。
我用什么?涡轮帧?热线?我该怎么做?
我仍然对 rails 中的所有内容感到非常困惑,因此非常感谢深入的回答。
我有一个简单但有点脏的解决方案。看到你是这一切的新手。我认为这可以帮助您在深入研究 Turbo
和所有其他内容之前了解表单本身发生了什么。
此外,使用 accepts_nested_attributes_for
既快捷又有趣;但是,当您不理解它时,它会让您发疯。
为了简单起见,首先让我们修改一下表格并放宽 simple_form
<!-- form_for or form_tag https://guides.rubyonrails.org/form_helpers.html#using-form-tag-and-form-for -->
<!-- form_with does it all -->
<%= form_with model: cocktail do |f| %>
<% if cocktail.errors.any? %>
<% cocktail.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
<% end %>
<div>
<%= f.label :name, style: "display: block" %>
<%= f.text_field :name, value: "aa"%>
</div>
<div>
<%= f.label :recipe, style: "display: block" %>
<%= f.text_area :recipe, value: "nn" %>
</div>
<!-- https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for -->
<%= f.fields_for "cocktail_ingredients" do |ff| %>
<br>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<%= ff.text_field :quantity, value: "1" %>
<div>
<%= ff.label 'Delete' %>
<%= ff.check_box :_destroy %>
</div>
<% end %>
<!-- Yes we're submitting our form, but with a different button -->
<!-- see CocktailController#create -->
<div> <%= f.submit "Add ingredient", name: 'add_ingredient' %> </div>
<div> <%= f.submit %> </div>
<% end %>
collection_check_boxes
不适用于这种情况,我们希望每个记录有一个成分,如 belongs_to :ingredient
所示。 single select
是一个明显的选择; collection_radio_buttons
同样适用。
fields_for
助手还将输出一个隐藏字段,其中“id”为 cocktail_ingredient 记录。这就是 rails 知道更新现有记录(带 id)和创建新记录(不带 id)的方式。
fields_for
还将“_attributes”附加到 record_name 如果表单的模型,在本例中 Cocktail
,为 record_name 定义了 accepts_nested_attributes_for
。
澄清一下,如果你的模型中有这个:
accepts_nested_attributes_for :cocktail_ingredients
这意味着
f.fields_for "cocktail_ingredients"
将在输入名称前加上 cocktail[cocktail_ingredients_attributes]
。
控制器
# GET /cocktails/new
def new
@cocktail = Cocktail.new
# build one new object for our new form; otherwise `fields_for` will not render anything
# with this new form will always have one empty `cocktail_ingredient`
# NOTE: this is necessary when using `accepts_nested_attributes_for`
# NOTE: without `accepts_nested_attributes_for`, `fields_for` will render fields; see what a cluster it is.
@cocktail.cocktail_ingredients.build
end
# POST /cocktails
def create
@cocktail = Cocktail.new(cocktail_params)
respond_to do |format|
# NOTE: catch when we submit our form with add_ingredient button
# params will have "add_ingredient"=>"Add ingredient"
if params[:add_ingredient]
# build another cocktail_ingredient to be rendered by `fields_for` helper
@cocktail.cocktail_ingredients.build
# and just render the form again. tada! you're done.
# NOTE: rails 7 submits as TURBO_STREAM format; hey we're actually using turbo.
# it expects a form to redirect when valid; so we have to use some kind of invalid status.
format.html { render :new, status: :unprocessable_entity }
else
if @cocktail.save
format.html { redirect_to cocktail_url(@cocktail), notice: "Cocktail was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
end
型号
更新 Cocktail
以允许在保存时使用 _destroy
表单字段删除记录。
accepts_nested_attributes_for :cocktail_ingredients, allow_destroy: true
解释的很长,最后我只加了四行代码:
if params[:add_ingredient]
@cocktail.cocktail_ingredients.build
format.html { render :new, status: :unprocessable_entity }
<%= f.submit "Add ingredient", name: 'add_ingredient' %>
希望这是有道理的。这里有很多改进的余地。我认为 cocoon
gem 对于简单的用例仍然是一个可行的选择,如果你想研究一下的话。
更新。添加 turbo-frame
现在,当添加新成分时,整个页面都是 re-rendered。我们可以添加 turbo-frame
以仅更新表格的成分部分:
...
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for "cocktail_ingredients" do |ff| %>
<br>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<%= ff.text_field :quantity %> <%# NOTE: remove preset value %>
<div>
<%= ff.label 'Delete' %>
<%= ff.check_box :_destroy %>
</div>
<% end %>
</turbo-frame>
...
更改 'Add ingredient' 按钮以了解我们要更新的 turbo-frame
。
...
<%= f.submit "Add ingredient",
data: { turbo_frame: f.field_id(:ingredients)},
name: 'add_ingredient' %>
...
Turbo 框架 id
必须匹配按钮 data-turbo-frame
属性。
<turbo-frame id="has_to_match">
<input data-turbo-frame="has_to_match" ...>
现在,当单击“添加成分”按钮时,它仍然会转到同一个控制器,它仍然会在服务器上呈现整个表单,但是现在,re-rendering 整个页面,只有里面的内容turbo-frame
已更新。这意味着,页面滚动保持不变,turbo frame 标签外的表单状态不变。对于所有意图和目的,这现在是一个动态表单。
可能的改进可能是停止扰乱 create
操作并通过不同的控制器操作“添加成分”,例如 add_ingredient
# config/routes.rb
resources :cocktails do
post :add_ingredient
end
...
<%= f.submit 'Add ingredient',
formmethod: 'post',
formaction: '/cocktails/add_ingredient',
data: { turbo_frame: f.field_id(:ingredients)} %>
...
将 add_ingredient
操作添加到 CocktailsController
def add_ingredient
@cocktail = Cocktail.new cocktail_params
@cocktail.cocktail_ingredients.build # add another ingredient
render :new
end
create
操作现在可以恢复为默认值。
我觉得这已经很简单了。这是简短的版本(大约 10 行额外的代码来添加动态字段,并且没有 javascript)
# config/routes.rb
resources :cocktails do
post :add_ingredient
end
# app/controllers/cocktails_controller.rb
# the usual scaffold for other actions
def add_ingredient
@cocktail = Cocktail.new cocktail_params
@cocktail.cocktail_ingredients.build # add another ingredient
render :new
end
# app/views/cocktails/new.html.erb
<%= form_with model: @cocktail do |f| %>
<turbo-frame id="<%= f.field_id(:ingredients) %>" class="contents">
<%= f.fields_for :cocktail_ingredients do |ff| %>
<%= ff.select :ingredient_id, Ingredient.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
<% end %>
</turbo-frame>
<%= f.button "Add ingredient",
formmethod: 'post',
formaction: '/cocktails/add_ingredient',
data: { turbo_frame: f.field_id(:ingredients)},%>
<%= f.submit %>
<% end %>