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 %>
最後に
以上で、実装は完了のはずです。うまく動かない場合は、サンプルリポジトリを確認してください。
今回、実装にあたりハマったポイントなど細かめにコメントに残しています。
また、今回、できる限り部分テンプレートに切り出ししました。
ぜひ、実装の参考にしてください!