write ahead log

ロールフォワード用

Rails5とcocoonを使って明細のあるフォームを作る

一昔前にRailsを触っていた時にはnested_formというプラグインが良く使われていた気がするのですが,最近はcocoonというものが良いそうです.

どちらを選んでもどうせ大したことはしませんし, どちらでも良いのですがnested_formの方はあまりメンテされている感じでもないのでcocoonを使う事にしました.

メンテされないなら自分で直すぐらいの気持ちでやっていきたいものですけどね.

サンプルプロジェクトの内容

今回は簡単な注文書を作るアプリにしてみようと思います.

細かい作り込みは無視して注文書のヘッダ(鏡とかとも言いますね)に

  • 表題
  • 顧客名

で, 明細に

  • 品名
  • 金額
  • 数量

を持たせるというとても単純な構成です.

当然, 明細は複数あるのでここでcocoonを使います.

本当に業務などで使うには全然足りませんがサンプルには十分かなと.

サンプルプロジェクトの作成

$ rails new cocoon_sample

プラグインの導入

とりあえずGemfileをいじります. 以下を追記.

まだjQuery生きてます.

gem 'jquery-rails'
gem 'cocoon'

いつも通り.

$ bundle install

application.jsへ以下を追加.

//= require turbolinks
//= require jquery  <=---ここ
//= require cocoon  <=---ここ
//= require_tree .

注文書管理ページを作る

scaffoldで雑に用意.

$ rails g scaffold orders title:string customer:string
$ rails db:migrate

注文書明細のモデルを用意する

$ rails g model OrderDetails order:references item:string price:integer quantity:integer
$ rails db:migrate

モデルを関連付ける

モデル2つを関連付けてやります.

# app/model/order.rb

class Order < ApplicationRecord
  has_many :order_details
end

こちらは明細側. モデル生成時にもう行追加されてますが.

class OrderDetail < ApplicationRecord
  belongs_to :order
end

ネストしたモデルで更新できるようにする

# app/model/order.rb
class Order < ApplicationRecord
  has_many :order_details

  # 削除も受け入れる
  accepts_nested_attributes_for :order_details, allow_destroy: true
end

Strong Parameterの対応が要るのでコントローラを以下のように変更.

# app/controller/orders_controller.rb

    def order_params
      params.require(:order).permit(:title, :customer, order_details_attributes: [:id, :_destroy, :item, :price, :quantity])
    end

ビューの用意

明細を部分テンプレートにして使うらしい.

以下は親側の入力フォーム.

<!-- app/views/orders/_form.html.erb -->
...
  <div class="details">
    <%= link_to_add_association '行を追加', form, :order_details %>
    <table>
      <%= form.fields_for :order_details do |detail| %>
        <%= render 'order_detail_fields', f: detail %>
      <% end %>
    </table>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
...

以下が子側(明細). 部分テンプレート名は規約があるっぽい([モデル名]fields.html.erb)

規約外の名前や共通テンプレートを使う方法は公式に記載されている

<!-- app/views/orders/_order_detail_fields.html.erb -->
<tr class="nested-fields">
  <td><%= f.text_field :item %></td>
  <td><%= f.text_field :price %></td>
  <td><%= f.text_field :quantity %></td>
  <td><%= link_to_remove_association "行削除", f %></td>
</tr>

部分テンプレート名に規約がある以外はわかりやすいと思う.

追加するタグの位置を調整したい

公式に解説がある.

上述の例だとテーブルにはやはりヘッダが欲しくなる.

cocoonは行追加する際にはデフォルトでは行追加のlink_to_add_associationの親要素へ追加しようとする.

ここを制御するにはオプションを指定する必要がある.

例を見るとすぐわかる.

親側のビューのコードが以下(子は変わっていない)

  <div class="details">
    <!-- #order-detailsの下に追加するように指定 -->
    <%= link_to_add_association '行を追加', form, :order_details,
        data: {
          association_insertion_node: '#order-details',
          association_insertion_method: 'append'
        } %>
    <table>
      <thead>
        <tr>
        <th>品名</th>
        <th>数量</th>
        <th>単価</th>
        <th></th>
        </tr>
      </thead>
      <tbody id="order-details">
        <%= form.fields_for :order_details do |detail| %>
          <%= render 'order_detail_fields', f: detail %>
        <% end %>
      </tbody>
    </table>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>

他にも制御できるので公式を一度見るとよさそう.

行の追加・削除前後にコールバックを挟みたい

  • 最小・最大の行数を設定したい
  • 行追加時に合計金額を計算したい

こういう事をしたいときにコールバックを挟めると便利.

公式サポートされているコールバックがかかれている.

例として最大5行までしか追加できない明細にしてみた.

5行目が出来ると追加ボタンを隠す.

まずビュー側のボタンへidを付ける.

<!-- idを加えた -->
    <%= link_to_add_association '行を追加', form, :order_details, id: 'add-link',
        data: {
          association_insertion_node: '#order-details',
          association_insertion_method: 'append'
        } %>

次にjs(Coffee)で制御をかける.

# app/asset/javascript/orders.coffee
$ ->
    # 5行以上ある場合は追加ボタンを隠す
    $('#order-details').on 'cocoon:before-insert', ->
        if $('#order-details .nested-fields').length >= 5 
            $('#add-link').hide();
        else
            $('#add-link').show();

まとめとか

公式読めば大抵の事ができるので非常に便利です.

後で自分で参照しそうなのでgithubにいれておきました.

github.com

あと、公式読むと気付きづらいのですが, ERBのサンプルはwikiにありました.