Leon's Blogging

Coding blogging for hackers.

Ruby on Rails - Accepts_nested_attributes_for

| Comments

accepts_nested_attributes_for 是一個蠻常會用到的語法

簡單的來說,就是可以透過這個語法,在更新 data 的時候,同時更新其他 model 裡的 data 所以並不是每個 model 都必須要有 controller 才能夠做更新的動作

這裏有兩種情境

One-to-one

1
2
3
4
class Book < ActiveRecord::Base
  has_one :author
  accepts_nested_attributes_for :author
end
1
2
3
4
params = { book: { name: 'Harry Potter', author_attributes: { name: 'J. K. Rowling' } } }

book = Book.create(params[:book])
book.author.name # => 'J. K. Rowling'

透過 params 更新 Book 的 name 同時透過 author_attributes: { name: 'J. K. Rowling' } 更新 Author 的 name

當然 update 同樣適用

1
2
3
params = { book: { author_attributes: { name: 'J. K. Rowling' } } }
book.update params[:book]
book.author.name # => 'J. K. Rowling'

另外值得注意的是

1
accepts_nested_attributes_for :author, :allow_destroy => true, :reject_if => :all_blank

:allow_destroy => true 可以在表單增加一個 _destroy 核選塊來表示刪除 :reject_if => :all_blank 表示在什麼條件下就當作沒有要動作, all_blank 表示如果資料都是空的,就不執行

One-to-many

1
2
3
4
class Book < ActiveRecord::Base
  has_many :pages
  accepts_nested_attributes_for :page
end
1
2
3
4
5
6
7
8
9
10
11
params = { book: {
  name: 'Harry Potter', page_attributes: [
      { title: "Philosopher's Stone" },
      { title: "Chamber of Secrets" }
  ]
}}

book = Book.create(params[:book])
book.pages.length # => 2
book.pages.first.title # => 'Philosopher's Stone'
book.pages.last.title # => 'Chamber of Secrets'

rails5

在 rails 5 中,要用 accepts_nested_attributes_for 必須在 belongs_to 加上 options: true 或是 required: false

1
2
3
4
5
6
7
8
class Item < ApplicationRecord
  has_many :item_options
  accepts_nested_attributes_for :item_options, allow_destroy: true
end

class ItemOption < ApplicationRecord
  belongs_to :items, required: false
end

必須有 :id 才能夠 update,再加上 :_destroy 就能夠在參數加上 _destroy:1 (or true) 去做刪除 (model 必須要 allow_destroy: true)

1
2
3
4
5
6
class ItemsController < ApplicationController
  private
    def item_params
      params.require(:item).permit(:name, item_options_attributes: [:id, :value, :position, :_destroy])
    end
end

通常會搭配 fields_for 來嵌入到同一個表單

One-to-one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%= form_for @book do |b| %>
  <%= b.label :name %>
  <%= b.text_field : name %>

  <%= b.fields_for :author do |a| %>
   <%= a.label :name %>
       <%= a.text_field : name %>

        <% unless a.object.new_record? %>
          <%= a.label :_destroy, 'Remove:' %>
        <%= a.check_box :_destroy %>
       <% end %>
  <% end %>

  <%= b.submit %>
<% end %>

這樣就能夠透過原本是 @book 的表單,裡面再放入 author 的欄位進行更新。

One-to-many

one-to-many 會比較麻煩,因為當新增的時候,並不知道要新增幾個,因此會動用到 jquery 的動態新增,就是可以在表單上面一直增加欄位。

不過幸好有 gem 可以解決了,以下有幾個 gem

strong parameter

最後記得要加 strong parameter one-to-one 和 one-to-many 都要

1
2
3
4
def book_params
    params.require(:book)
        .permit(:name, pages_attributes:[:title, :_destroy, :id])
end

每個 gem strong parameter 的方式都有點不太一樣,記得要看一下

helper

book_helper.rb

1
2
3
4
5
def setup_author(book)
    book.build_author if book.author.blank?
    #one-to-one 用 build_author , one-to-many 可以用 authors.build or authors.new
    book
end

如果一開始沒設定的話,在 book 表單上是看不到 author 的欄位,因為一開始還沒 build 因此要給它一個判斷,如果是 nil 就先 build_author 給它

接著用 setup_author(@book) 來置換 form_for 中的 @book

1
2
3
4
5
<%= form_for setup_author(@book), :url => books_path do |b| %>

...

<% end %>

官方文件:

Comments