TojiTech No Programming, No Life.

Rails(6系)でcocoonを使用して動的にフォームを追加する

背景

  • 動的フォームを簡単に実装したい

サンプルリポジトリ

こちらから、実装済みソースコードが見られます。

注意

サンプルのリポジトリは、Dockerで起動することを前提に作成されています。
Dockerを使用しない場合は、appフォルダ以下で通常のrailsの起動処理を行ってください。

なお、記事ではDockerを使用せず導入する手順で記載していますのでご注意ください。

Rails6で記述していますが、Rails5でもロジックは同様に動作すると思います。

ソースコードのコメントについて

基本的にコメントを多めに記載しています。こちらを参考に実装して、ロジックの理解を深めてください。

実装

今回、jQueryを使用するので、jQueryをまず導入します。

yarn add jquery

続いて、jQueryをRailsに認識させます。

const { environment } = require('@rails/webpacker')

// ここから追加
const webpack = require('webpack')
environment.plugins.prepend('Provide',
    new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
    })
)
// ここまで追加

module.exports = environment

次に、cocoonを追加します。追加後にbundle installも忘れずに!

# cocoon
gem "cocoon"

必要なcocoonのjsライブラリを追加します。

yarn add @nathanvda/cocoon

cocoonのjsライブラリとjQueryをRails側に認識させます

// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

// ここから追加
require("jquery")
require("@nathanvda/cocoon")
// こまで追加

Rails.start()
Turbolinks.start()
ActiveStorage.start()

続いて、モデルやコントローラーなど作ってしまいます。

# モデルの作成
rails g model album
rails g model album_track

# コントローラーの作成
rails g controller albums

今回は、以下のようなテーブル構成にします。

テーブル構成図

マイグレーションファイルは、以下のような感じ

class CreateAlbums < ActiveRecord::Migration[6.1]
  def change
    create_table :albums do |t|
      t.string :title, null: false
      t.string :artist, null: false
      t.timestamps
    end
  end
end
class CreateAlbumTracks < ActiveRecord::Migration[6.1]
  def change
    create_table :album_tracks do |t|
      t.references :album, foreign_key: true
      t.string :title, null: false
      t.timestamps
    end
  end
end

モデル

class Album < ApplicationRecord
  # バリデーション
  validates :title, presence: true
  validates :artist, presence: true

  # アソシエーション
  #   dependentは、親のオブジェクトを削除した際に、子のオブジェクトを削除するために必要
  has_many :album_tracks, dependent: :destroy

  # reject_ifは、入力フォームを追加しているもののすべてが空白の場合にリジェクトする
  # allow_destroyは、入力フォームでこのオブジェクトが削除された際に削除を許可する
  accepts_nested_attributes_for :album_tracks, reject_if: :all_blank, allow_destroy: true
end
class AlbumTrack < ApplicationRecord
  # バリデーション
  validates :title, presence: true

  # アソシエーション
  belongs_to :album
end

コントローラー

class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end

  def new
    @album = Album.new
  end

  def create
    @album = Album.new(album_params)
    if @album.save
      redirect_to albums_path
    else
      render :new
    end
  end

  def show
    @album = Album.find(params[:id])
  end

  def edit
    @album = Album.find(params[:id])
  end

  def update
    @album = Album.find(params[:id])
    if @album.update(album_params)
      redirect_to album_path(params[:id])
    else
      render :edit
    end
  end

  def destroy
    Album.destroy(params[:id])
    redirect_to albums_path
  end

  private

  def album_params
    # album_tracks_attributesが子のモデルに保存する要素
    #   :id, :_destroyをつけることで、編集と削除が可能になる
    params.require(:album).permit(
      :title, :artist,
      album_tracks_attributes: [:id, :title, :_destroy])
  end
end

ビュー

まずは、部分テンプレートから

<%# 以下の.nested-fieldsは、必須です。 %>
<div class="nested-fields">
  <div class="field">
    <%# link_to_add_associationのform_nameで指定した名前。(初期値 : f)  %>
    <%= album_track.label :title, 'トラック名' %>
    <%= album_track.text_field :title %>
  </div>
  <div class="field">
    <%= link_to_remove_association "トラックを削除", album_track %>
  </div>
  <hr>
</div>

link_to_add_associationの詳しいオプションは、こちらの公式ドキュメントを参照してください。

<div class="field">
  <%= album.label :title, 'アルバムタイトル' %><br>
  <%= album.text_field :title %><br>
  <%= album.label :artist, 'アルバムアーティスト' %><br>
  <%= album.text_field :artist %><br>
</div>
<h3>Details --></h3>
<div class="details">
  <%# トラックの追加フォームをするボタン %>
  <%# 詳しいオプション : https://github.com/nathanvda/cocoon#link_to_add_association %>
  <%= link_to_add_association 'トラックの追加',
                              # form_withのformタグ変数
                              album,
                              # フォームのデータを追加するモデル名
                              :album_tracks,
                              # 追加するフォームの部分テンプレート
                              partial: "album_track_fields",
                              # オプション
                              data: {
                                # フォームを追加する場所の指定
                                association_insertion_node: '#album-track-forms',
                                # フォームを前後どの位置に追加するか。(初期値: before)
                                association_insertion_method: 'after'
                              },
                              # フォームオブジェクトを指定する場合(初期値 : f)
                              form_name: 'album_track',
                              # 部分テンプレートに渡す変数があれば記述
                              render_options: {
                                locals: {
                                  # 通常の部分テンプレートと同じ記述
                                }
                              }
  %>

  <hr>

  <%# フォームを追加する場所。%>
  <%#   link_to_add_associationのassociation_insertion_nodeで指定 %>
  <div id="album-track-forms">
    <%= album.fields_for :album_tracks do |album_track| %>
      <% render 'album_track_fields', album_track: album_track %>
    <% end %>
  </div>

</div>
<div class="field">
  <%= album.submit %>
</div>
<% model.errors.any? %>
<ul>
  <% model.errors.full_messages.each do |message|  %>
  <li>
    <%= message %>
  </li>
  <% end %>
</ul>

通常のテンプレート

<h1>Albums#index</h1>
<p>Find me in app/views/albums/index.html.erb</p>

<h2>Actions --></h2>
<ul>
  <li><%= link_to 'New Album Data Create', new_album_path %></li>
</ul>

<h2>Album Lists --></h2>
<%# 現在の登録数を表示する。“#{}”は、Rubyの式展開 %>
<%= "登録アルバム数 : #{@albums.count}" %>

<%# @albumsでレコードが1件もない場合の分岐 %>
<% if @albums.count == 0 %>
  <%# @albumsにデータがない場合の表示 %>
  <p>
    アルバムが登録されていません
  </p>
<% else %>
  <%# @albumsにレコードがある場合、タイトルを表示する %>
  <% @albums.each do |album| %>
    <p>
      <%= link_to album.title, album_path(album) %>
    </p>
  <% end %>
<% end %>
<h1>Albums#show</h1>
<p>Find me in app/views/albums/show.html.erb</p>

<h2>Actions --></h2>
<ul>
  <li><%= link_to 'Edit Album', edit_album_path(@album) %></li>
  <li><%= link_to 'Delete Album', album_path(@album), method: :delete %></li>
</ul>

<h3>Title -> <%= @album.title %></h3>
<h3>Artist -> <%= @album.artist %></h3>
<hr>

<h4>Tracks --></h4>

<%# トラックの存在確認 %>
<% if @album.album_tracks.count == 0 %>
  <%# トラックなしの場合の表示 %>
  <p>
    該当トラックなし
  </p>
<% else %>
  <%# トラックがある場合の表示 %>
  <p>
    <%= "#{@album.album_tracks.count} Tracks" %>
  </p>
  <ul>
    <% @album.album_tracks.each do |track| %>
      <li>
        <%= track.title %>
      </li>
    <% end %>
  </ul>
<% end %>
<h1>Albums#new</h1>
<p>Find me in app/views/albums/new.html.erb</p>

<%# バリデーションメッセージの部分テンプレート %>
<%= render 'layouts/error_messages', model: @album %>

<%# フォーム部分 %>
<%= form_with model: @album do |album| %>
  <%# フォームのテンプレート %>
  <% render 'form', album: album %>
<% end %>
<h1>Albums#edit</h1>
<p>Find me in app/views/albums/edit.html.erb</p>

<%# バリデーションメッセージの部分テンプレート %>
<%= render 'layouts/error_messages', model: @album %>

<%= form_with model: @album do |album| %>
  <%= render 'form', album: album %>
<% end %>

最後に

以上で、実装は完了のはずです。うまく動かない場合は、サンプルリポジトリを確認してください。

今回、実装にあたりハマったポイントなど細かめにコメントに残しています。
また、今回、できる限り部分テンプレートに切り出ししました。

ぜひ、実装の参考にしてください!

Profile

Yuki Tojima

RubyやPhp、JavaScriptまわりのことを徒然と記録に残す技術ブログです。

至らぬところもあると思いますが、見守っていただけると幸いです。

記事のリクエストや、間違いなどありましたら X (旧 Twitter) のDMなどでお気軽にご連絡ください。

ytojima @TojiTech
プロフィール画像