使用单个 form_with 在 Rails 中创建和编辑嵌套资源
Use a single form_with to create and edit nested resources in Rails
背景
在我的应用程序内部,系列 由许多书籍 组成。系列的 Show 页面允许用户查看系列中的所有书籍并使用表单向系列中添加新书。
Show 页面上列出的每本书都有一个 link 到该书的 Edit 页面。编辑页面包含 用于最初添加图书的相同表单。编辑书籍时,表格应自动填写书籍现有信息。
问题
如何配置我的 form_with
标签,使其既可以创建新书又可以编辑现有书(自动填写编辑表单)?我尝试了以下配置,但它们要么破坏了编辑页面,要么破坏了显示页面:
<%= form_with(model: [ @series, @series.books.build ], local: true) do |form| %>
- 打破图书编辑页面
- 错误:没有错误,但表单不会自动填充数据
<%= form_with(model: @book, url: series_book_path, local: true) do |form| %>
- 休息系列显示页面
- 错误:
No route matches {:action=>"show", :controller=>"books", :id=>"6"}, missing required keys: [series_id]
<%= form_with(model: [:series, @book], local: true) do |form| %>
- 休息系列显示页面
- 错误:
Undefined method 'model_name' for nil:NilClass
<%= form_with(model: [@series, @series.books.find(@book.id)], local: true) do |form| %>
- 休息系列显示页面
- 错误:
undefined method 'id' for nil:NilClass
<%= form_with(model: @book, url: [@series, @book], local: true) do |form| %>
- 在系列展示页面上提交新书时中断
- 错误:
No route matches [POST] "/series/6"
我参考过的资源:
- https://api.rubyonrails.org/v5.2.2/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
- https://guides.rubyonrails.org/form_helpers.html
- https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default
- https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb
现有代码
下面是相关代码的精简部分,以及 link 它们在我当前 GitHub 存储库中的位置。
resources :series do
resources :books
end
class Book < ApplicationRecord
belongs_to :series
end
class Series < ApplicationRecord
has_many :books, dependent: :destroy
end
create_table "books", force: :cascade do |t|
t.integer "series_number"
t.integer "year_published"
t.integer "series_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["series_id"], name: "index_books_on_series_id"
end
create_table "series", force: :cascade do |t|
t.string "title"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
app/views/series/show.html.erb
<%= render @series.books %>
<%= render 'books/form' %>
app/views/books/_book.html.erb
<%= link_to 'Edit', edit_series_book_path(book.series, book) %>
<%= render 'form' %>
app/views/books/_form.html.erb
<%= form_with(model: @book, url: [@series, @book], local: true) do |form| %>
<%= form.label :series_number %>
<%= form.number_field :series_number %>
<%= form.label :year_published %>
<%= form.number_field :year_published %>
<% end %>
app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@book = Book.find(params[:id])
end
def new
@book = Book.new
end
def edit
@series = Series.find(params[:series_id])
@book = @series.books.find(params[:id])
end
def create
@series = Series.find(params[:series_id])
@book = @series.books.create(book_params)
redirect_to series_path(@series)
end
def destroy
@series = Series.find(params[:series_id])
@book = @series.books.find(params[:id])
@book.destroy
redirect_to series_path(@series)
end
private
def book_params
params.require(:book).permit(:year_published, :series_number)
end
end
路线
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
series_books GET /series/:series_id/books(.:format) books#index
POST /series/:series_id/books(.:format) books#create
new_series_book GET /series/:series_id/books/new(.:format) books#new
edit_series_book GET /series/:series_id/books/:id/edit(.:format) books#edit
series_book GET /series/:series_id/books/:id(.:format) books#show
PATCH /series/:series_id/books/:id(.:format) books#update
PUT /series/:series_id/books/:id(.:format) books#update
DELETE /series/:series_id/books/:id(.:format) books#destroy
series_index GET /series(.:format) series#index
POST /series(.:format) series#create
new_series GET /series/new(.:format) series#new
edit_series GET /series/:id/edit(.:format) series#edit
series GET /series/:id(.:format) series#show
PATCH /series/:id(.:format) series#update
PUT /series/:id(.:format) series#update
DELETE /series/:id(.:format) series#destroy
您可以将数组传递给表单以处理嵌套和 "shallow" 路由:
<%= form_with(model: [@series, @book], local: true) do |form| %>
<% end %>
Rails 压缩数组(删除 nil 值),因此如果 @series
为 nil,则表单将退回到 book_url(@book)
或 books_url
。但是,您需要从控制器正确处理设置 @series
和 @book
。
class SeriesController < ApplicationController
def show
@series = Series.find(params[:id])
@book = @series.books.new # used by the form
end
end
您可以使用 local 变量在您的视图中处理此问题:
# app/views/books/_form.html.erb
<%= form_with(model: model, local: true) do |form| %>
<% end %>
# app/views/books/edit.html.erb
<%= render 'form', locals: { model: [@series, @book] } %>
# app/views/series/show.html.erb
<%= render 'books/form', locals: { model: [@series, @series.book.new] } %>
您还可以在路由中使用 shallow: true
选项来避免嵌套成员路由(显示、编辑、更新、销毁):
resources :series do
resources :books, shallow: true
end
这会让你做到:
# app/views/books/edit.html.erb
<%= render 'form', model: @book %>
# app/views/books/_book.html.erb
<%= link_to 'Edit', edit_book_path(book) %>
背景
在我的应用程序内部,系列 由许多书籍 组成。系列的 Show 页面允许用户查看系列中的所有书籍并使用表单向系列中添加新书。
Show 页面上列出的每本书都有一个 link 到该书的 Edit 页面。编辑页面包含 用于最初添加图书的相同表单。编辑书籍时,表格应自动填写书籍现有信息。
问题
如何配置我的 form_with
标签,使其既可以创建新书又可以编辑现有书(自动填写编辑表单)?我尝试了以下配置,但它们要么破坏了编辑页面,要么破坏了显示页面:
<%= form_with(model: [ @series, @series.books.build ], local: true) do |form| %>
- 打破图书编辑页面
- 错误:没有错误,但表单不会自动填充数据
<%= form_with(model: @book, url: series_book_path, local: true) do |form| %>
- 休息系列显示页面
- 错误:
No route matches {:action=>"show", :controller=>"books", :id=>"6"}, missing required keys: [series_id]
<%= form_with(model: [:series, @book], local: true) do |form| %>
- 休息系列显示页面
- 错误:
Undefined method 'model_name' for nil:NilClass
<%= form_with(model: [@series, @series.books.find(@book.id)], local: true) do |form| %>
- 休息系列显示页面
- 错误:
undefined method 'id' for nil:NilClass
<%= form_with(model: @book, url: [@series, @book], local: true) do |form| %>
- 在系列展示页面上提交新书时中断
- 错误:
No route matches [POST] "/series/6"
我参考过的资源:
- https://api.rubyonrails.org/v5.2.2/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
- https://guides.rubyonrails.org/form_helpers.html
- https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default
- https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb
现有代码
下面是相关代码的精简部分,以及 link 它们在我当前 GitHub 存储库中的位置。
resources :series do
resources :books
end
class Book < ApplicationRecord
belongs_to :series
end
class Series < ApplicationRecord
has_many :books, dependent: :destroy
end
create_table "books", force: :cascade do |t|
t.integer "series_number"
t.integer "year_published"
t.integer "series_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["series_id"], name: "index_books_on_series_id"
end
create_table "series", force: :cascade do |t|
t.string "title"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
app/views/series/show.html.erb
<%= render @series.books %>
<%= render 'books/form' %>
app/views/books/_book.html.erb
<%= link_to 'Edit', edit_series_book_path(book.series, book) %>
<%= render 'form' %>
app/views/books/_form.html.erb
<%= form_with(model: @book, url: [@series, @book], local: true) do |form| %>
<%= form.label :series_number %>
<%= form.number_field :series_number %>
<%= form.label :year_published %>
<%= form.number_field :year_published %>
<% end %>
app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@book = Book.find(params[:id])
end
def new
@book = Book.new
end
def edit
@series = Series.find(params[:series_id])
@book = @series.books.find(params[:id])
end
def create
@series = Series.find(params[:series_id])
@book = @series.books.create(book_params)
redirect_to series_path(@series)
end
def destroy
@series = Series.find(params[:series_id])
@book = @series.books.find(params[:id])
@book.destroy
redirect_to series_path(@series)
end
private
def book_params
params.require(:book).permit(:year_published, :series_number)
end
end
路线
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
series_books GET /series/:series_id/books(.:format) books#index
POST /series/:series_id/books(.:format) books#create
new_series_book GET /series/:series_id/books/new(.:format) books#new
edit_series_book GET /series/:series_id/books/:id/edit(.:format) books#edit
series_book GET /series/:series_id/books/:id(.:format) books#show
PATCH /series/:series_id/books/:id(.:format) books#update
PUT /series/:series_id/books/:id(.:format) books#update
DELETE /series/:series_id/books/:id(.:format) books#destroy
series_index GET /series(.:format) series#index
POST /series(.:format) series#create
new_series GET /series/new(.:format) series#new
edit_series GET /series/:id/edit(.:format) series#edit
series GET /series/:id(.:format) series#show
PATCH /series/:id(.:format) series#update
PUT /series/:id(.:format) series#update
DELETE /series/:id(.:format) series#destroy
您可以将数组传递给表单以处理嵌套和 "shallow" 路由:
<%= form_with(model: [@series, @book], local: true) do |form| %>
<% end %>
Rails 压缩数组(删除 nil 值),因此如果 @series
为 nil,则表单将退回到 book_url(@book)
或 books_url
。但是,您需要从控制器正确处理设置 @series
和 @book
。
class SeriesController < ApplicationController
def show
@series = Series.find(params[:id])
@book = @series.books.new # used by the form
end
end
您可以使用 local 变量在您的视图中处理此问题:
# app/views/books/_form.html.erb
<%= form_with(model: model, local: true) do |form| %>
<% end %>
# app/views/books/edit.html.erb
<%= render 'form', locals: { model: [@series, @book] } %>
# app/views/series/show.html.erb
<%= render 'books/form', locals: { model: [@series, @series.book.new] } %>
您还可以在路由中使用 shallow: true
选项来避免嵌套成员路由(显示、编辑、更新、销毁):
resources :series do
resources :books, shallow: true
end
这会让你做到:
# app/views/books/edit.html.erb
<%= render 'form', model: @book %>
# app/views/books/_book.html.erb
<%= link_to 'Edit', edit_book_path(book) %>