[Rails]ネストされたフォーム
はじめに
stimulus-rails-nested-form
でネストされたフォームを実装していきます。
一つのTodo
に複数のタスクをネストされるイメージです。Todo
はすでに実装されています。
環境
Rails 7.0.7
ruby 3.2.1
流れ
- scaffoldでTaskを作成する
-
stimulus-rails-nested-form
をインストールする - stimulusコントローラーを作成する
- Todoフォームにstimulusコントローラーの設定を追加する
- TodoフォームにTaskフォームとそのテンプレを追加する
- Todoの一覧・詳細にTaskパーシャルを読み込む
Taskモデルを作成する
bin/rails g scaffold task descriptions:text status:integer todo:references
bin/rails db:migrate
== 20230901143525 CreateTasks: migrating ======================================
-- create_table(:tasks)
-> 0.0050s
== 20230901143525 CreateTasks: migrated (0.0051s) =============================
モデルの関連付けを設定する
class Todo < ApplicationRecord
...
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks, allow_destroy: true, reject_if: :all_blank
end
accepts_nested_attributes_for
は、特定のモデル間の関連性に関するオプションの1つです。このオプションを使用することで、親モデルが子モデルの属性を一緒に保存できるようになります。
accepts_nested_attributes_for
を使うと、親モデルが子モデルの属性を許可し、子モデルのレコードを親モデルと一緒に保存できます。
allow_destroy: true
オプションを使用して関連付けられたタスクを削除できるようにしています。
reject_if: :all_blank
オプションを使用して、空白の属性を持つタスクを拒否する設定を行っています。親モデルにネストされた子モデルの属性がすべて空白の場合、その子モデルのレコードは作成されません。
class Task < ApplicationRecord
belongs_to :todo
validates :descriptions, presence: true
enum status: { planned: 0, started: 1, completed: 2 }
end
ストロングパラメーターを追加する
class TodosController < ApplicationController
...
def new
@todo = Todo.new
@todo.tasks.build
end
private
def todo_params
params.require(:todo).permit(:title, :status, tasks_attributes: %i[id descriptions status _destroy])
end
end
stimulus-rails-nested-form
をインストールする
bin/importmap pin stimulus-rails-nested-form
Pinning "stimulus-rails-nested-form" to https://ga.jspm.io/npm:stimulus-rails-nested-form@4.1.0/dist/stimulus-rails-nested-form.mjs
Pinning "@hotwired/stimulus" to https://ga.jspm.io/npm:@hotwired/stimulus@3.2.2/dist/stimulus.js
stimulusコントローラーを作成する
bin/rails g stimulus nested_form
create app/javascript/controllers/nested_form_controller.js
import NestedForm from 'stimulus-rails-nested-form'
export default class extends NestedForm {
connect() {
super.connect()
}
}
todoフォームにstimulusコントローラーの設定を追加する
DOMがネストされるフォーム(Task)を操作できるようにするためCSSのクラス名"nested-form-wrapper"
を追加します。
Taskフォームにも同じクラス名を追加します。
<%= form_with(model: todo, id: "#{dom_id(todo)}_form",
data: { controller: "nested-form",
nested_form_wrapper_selector_value: '.nested-form-wrapper' }
do |form| %>
# テンプレ
<template data-nested-form-target="template">
<%= form.fields_for :tasks, Task.new, child_index: 'NEW_RECORD' do |task_fields| %>
<%= render "todos/task_form", form: task_fields %>
<% end %>
</template>
# タスクフォーム
<%= form.fields_for :tasks do |task_fields| %>
<%= render "todos/task_form", form: task_fields %>
<% end %>
<%= form.submit %>
<% end %>
HTMLの<template>
要素を使用して、Taskフォームのテンプレートを定義しています。
部分テンプレートを使用し新しいTaskオブジェクトを含むフォーム要素を生成します。
data-nested-form-target="template"
:JavaScriptからテンプレート要素を特定するための識別子として定義します。
child_index
は、フォーム内でネストされた属性(またはオブジェクト)を識別するために使用されます。ネストされたフォーム内で同じ属性を持つ複数のオブジェクトがある場合、child_index
を使用してそれらを区別します。
child_index: 'NEW_RECORD'
は、新しいTaskオブジェクトを識別するためのchild_index
を設定しています。NEW_RECORD
などの特別な文字列を使用して、新しいオブジェクトを識別することがあります。
デベロッパーツールを開いて、実際に生成されるhtmlを見ると分かりやすいです。
<div data-nested-form-target="target"></div>
<button type="button" data-action="nested-form#add">タスクを追加する</button>
タスクを追加する
ボタンがクリックされたときに実行されるアクションが指定されています。
nested_form
コントローラーのadd
アクションを実行し、テンプレによる新しいTaskフォームをdata-nested-form-target="target"
の前に追加されることになります。
Taskフォーム
<div class="nested-form-wrapper" data-new-record="<%= form.object.new_record? %>">
<%= form.label :descriptions %>
<%= form.text_field :descriptions %>
<%= form.label :status %>
<%= form.select :status, Task.statuses.keys.map { |status| [I18n.t("enums.task.status.#{status}"), status] }, include_blank: true %>
<button type="button" data-action="nested-form#remove">削除</button>
<%= form.hidden_field :_destroy %>
</div>
新規タスクの場合data-new-record="true"
を返します。
タスクの編集の場合data-new-record="false"
になります。
削除ボタンをクリックしたらnested_form
コントローラーのremove
アクションを実行しTaskをDOMから削除されます。
Taskパーシャル
<% todo.tasks.each do |task| %>
<%= task.descriptions %>
<%= t("enums.task.status.#{task.status}") %>
<% end %>
終わりに
stimulus-components
を使ってネストしたフォームを簡単に実装できましたが、使わないやり方も試してみたいと思います。
Discussion