使用单个form_with在Rails中创建和编辑嵌套资源

9

背景

在我的应用程序中,一个系列由许多书籍组成。系列的展示页面允许用户查看该系列中的所有书籍,并使用表单向该系列添加新书。

展示页面上列出的每本书都有一个链接到该书的编辑页面。编辑页面包含与最初添加书籍时相同的表单。在编辑书籍时,表单应自动填充书籍的现有信息。

问题

如何配置form_with标签,以便它既可以创建新书籍,又可以编辑现有书籍(自动填充编辑表单)?我尝试了以下配置,但它们要么破坏了编辑页面,要么破坏了展示页面:

  1. <%= form_with(model: [ @series, @series.books.build ], local: true) do |form| %>
    • 破坏了书籍编辑页面
    • 错误:没有错误,但表单不会自动填充数据
  2. <%= 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]
  3. <%= form_with(model: [:series, @book], local: true) do |form| %>
    • 破坏了系列展示页面
    • 错误:Undefined method 'model_name' for nil:NilClass
  4. <%= form_with(model: [@series, @series.books.find(@book.id)], local: true) do |form| %>
    • 破坏了系列展示页面
    • 错误:undefined method 'id' for nil:NilClass
  5. <%= form_with(model: @book, url: [@series, @book], local: true) do |form| %>
    • 在系列展示页面上提交新书时出错
    • 错误:No route matches [POST] "/series/6"

我参考的资源:

现有代码

以下是相关代码的简化部分,以及它们在我的GitHub存储库中的链接。

config/routes.rb

resources :series do
  resources :books
end

app/models/book.rb

class Book < ApplicationRecord
  belongs_to :series
end

app/models/series.rb

class Series < ApplicationRecord
  has_many :books, dependent: :destroy
end

db/schema.rb

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) %>

app/views/books/edit.html.erb

<%= 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
1个回答

11

您可以将数组传递给表单以处理嵌套和“浅层”路由:

<%= 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

您可以使用局部变量,在视图中处理此操作:

# 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 选项,以避免嵌套成员路由(show、edit、update、destroy):
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) %>

请注意,form_with中的 local: true 选项与本地变量无关。它只是设置表单上的 data-remote 属性,该属性决定表单是正常发送还是使用AJAX方式发送。 - max
这个带有非常易读的例子的东西真是太有帮助了!现在它完美地运行了!关于你概述的不同方法-哪种更好,或者在社区中更受欢迎? - Taylor Liss
1
两者只是解决同一问题的不同方式。通过使用locals,您明确地将变量传递给partial,而不仅仅依赖于外部状态 - 这使得制作真正可重用的partials更容易,因为它们在本质上更具有功能性。 - max
只是一条快速的注释:我不得不更改show.html.erb代码为<%= render partial: 'books/form', locals: {model: [@series, @series.books.new]} %>以使其正常工作。当使用局部变量时,似乎需要明确说明它是一个部分。 - Taylor Liss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接