Just do IT

思うは招く

Rails 6 で Cocoon を使ってネストしたフォームを作る方法

こんなものを作る記事です💪

f:id:K_Koh:20200628103115j:plain

やりたいこと

  • 2つのモデルが紐付いたフォームをつくりたい
    • ネストしたフォームをつくりたい
  • 複数のデータを一度に追加したい

動画だとこんな感じです👇

youtu.be

「とりあえずソースコード見せて!」という方はこちらへ。

GitHub - kotakanazawa/cocoon-test: set up cocoon gem on Rails 6

環境

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.6

cocoon gem を使う

cocoon という、ネストしたフォームを簡単に実現する gem を使う。

nathanvda/cocoon: Dynamic nested forms using jQuery made easy; works with formtastic, simple_form or default forms

テストアプリを作る

まずは cocoon-test というアプリをrails newする。

$ rails new cocoon-test
$ cd cocoon-test

念の為、rails sをして「Yay! You’re on Rails!」が表示されるか確認しておく。

slim を入れる

テンプレートエンジンは slim を使う。

Gemfile.rb

# Gemfileの一番下に書く
gem "slim-rails"
gem "html2slim"

rails newのときにデフォルトで生成された erb ファイルを slim に変換する。

$ bundle exec erb2slim app/views/ --delete

これで、以降生成される view ファイルは slim になる。

Rails で erb を slim に変換する方法 - Just do IT

モデルやコントローラーを作成

次に、以下のscaffoldコマンドを実行する。

$ bin/rails g scaffold Project name:string

マイグレーションファイルにnull: falseを追加する。(テストアプリにこれをする必要はないかもしれないが、念の為)

class CreateProjects < ActiveRecord::Migration[6.0]
  def change
    create_table :projects do |t|
      t.string :name, null: false #追加

      t.timestamps
    end
  end
end

マイグレーションを実行。

$ bin/rails db:migrate

次に、Project モデルに紐づく Task モデルもscaffoldコマンドで作成する。

$ bin/rails g scaffold Task name:string project:belongs_to

こちらもnull: falseを追加。

class CreateTasks < ActiveRecord::Migration[6.0]
  def change
    create_table :tasks do |t|
      t.string :name, null: false #追加
      t.belongs_to :project

      t.timestamps
    end
  end
end

マイグレーションを実行。

$ bin/rails db:migrate

やっていること

  • Project と Task をscaffoldで作成する
  • Project と Task は一対多の関係にする
    • project:belongs_toで Project と Task のアソシエーションをつくる

テストアプリなので、どちらもname:stringだけとシンプルな構成にしている。

routes.rb を修正

root "projects#index"をしてホーム画面をプロジェクトの一覧画面にしておく。

Rails.application.routes.draw do
  root "projects#index"
  resources :tasks
  resources :projects
end

現時点で、こんな状態になっていると思う。

f:id:K_Koh:20200627105413j:plain

f:id:K_Koh:20200627105425j:plain

Rails 6 に jQuery を入れる

Cocoon を使うには、jQuery が必要になる。

今回は Webpacker を使って導入するため、jQueryyarn addする。

$ yarn add jquery

package.jsonjQuery が追記される。

{
  "name": "cocoon_test",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.2.2",
    "jquery": "^3.5.1", #追記される
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

今までは jquery-rails といった gem を使っていたが、JavaScript は Webpacker で管理できるようになった。

environment.js に追記

config/webpack/environment.js に以下を追記する。

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

本家の webpack だと webpack.config.js に設定を書くが、webpacker では webpacker/environment.js に設定を書く。

application.js

app/javascript/packs/application.js で jQueryrequireする。

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
# 追記
require("jquery")

これで jQuery の導入は完了。

Cocoon を導入する

jQuery を入れたら、次に Cocoon gem をインストールする。

Gemfile に以下を追記。

gem "cocoon"

次に、以下コマンドを実行。

$ yarn add github:nathanvda/cocoon#c24ba53

本来ならば普通にyarn add cocoonなどとしたいところだが、cocoon は開発が止まってるようで、Rails 6 に今のところ対応していない💦(2020年7月4日)

よって、 よって、上記のやり方でyarn addする。

package.json を確認すると cocoon が追記されている。

{
  "name": "cocoon_test",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.2.2",
    "cocoon": "github:nathanvda/cocoon#c24ba53", #追記される
    "jquery": "^3.5.1",
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

次に、app/javascript/packs/application.js で require をしておく。

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("jquery")
# 追記
require("cocoon")

これで Cocoon を使う準備は完了💪

モデルの修正

モデルに設定を記述していく。

Project モデル

Project モデルに設定を記入する。

class Project < ApplicationRecord
  has_many :tasks
  accepts_nested_attributes_for :tasks, reject_if: :all_blank, allow_destroy: true
  validates :name, presence: true
end

has_many :tasksで、Project と Task モデルを一体多の関係に設定。

accepts_nested_attributes_for :tasksで、ネストの子モデル側を指定する。これで Project に紐付いた Task を同時に保存できる。

reject_if: :all_blankallow_destroy: trueは、accepts_nested_attributes_forでサポートされてるオプション。

  • reject_if: :all_blank:入力がすべて空の場合は登録できない
  • allow_destroy: true:削除できるようにする

ActiveRecord::NestedAttributes::ClassMethods

Task モデル

Task モデルに以下を追記。

class Task < ApplicationRecord
  belongs_to :project
  validates :name, presence: true
end

ストロングパラメーターの設定

app/controllers/projects_controller.rb に、tasks_attributes: [:id, :name, :_destroy]を追記する。

def project_params
  params.require(:project).permit(:name, tasks_attributes: [:id, :name, :_destroy])
end

これで、Project に関するフォームでも tasks テーブルの属性が取得できるようになる。ここでは id, name, _destroy を記述している。:_destroyを記入することで属性の削除ができるようになる。

もし、Project に紐づくテーブルが members だった場合、members_attributesとなる。

フォーム作成

いよいよフォームをつくっていく。

app/views/projects/_form.html.slim

= form_for @project do |f|
  - if @project.errors.any?
    #error_explanation
      h2 = "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
      ul
        - @project.errors.full_messages.each do |message|
          li = message

  .field
    = f.label :name
    = f.text_field :name

  #追記
  h3 Tasks
  #tasks.field
    = f.fields_for :tasks do |task|
      = render 'task_fields', f: task
    .links
      = link_to_add_association 'add task', f, :tasks
  #ここまで

  .actions
    = f.submit

次に、_task_fields.html.slim というファイルを projects ディレクトリ直下に作成する。cocoon を使う上で、子モデルに登録するフォームはモデル_fieldsといったように名前を付けるルールになっている。

$ touch app/views/projects/_task_fields.html.slim

app/views/projects/_task_fields.html.slim

#追記
.nested-fields
  .field
    = f.label :name
    = f.text_field :name
  = link_to_remove_association "remove task", f

これでプロジェクトに紐づくタスクが登録ができるようになった。

詳細ページにタスクを表示する

このままではタスクが表示されないため、プロジェクトの詳細ページを修正する。

app/views/projects/show.html.slim

p#notice = notice

p
  strong Name:
  = @project.name

#追記
h2 Tasks
ul
  - @project.tasks.each do |task|
    li = task.name
#ここまで

=> link_to 'Edit', edit_project_path(@project)
'|
=< link_to 'Back', projects_path

完成画面

youtu.be

詳細画面からタスクの更新もできるようになっていることを確認してみてください。

ネストした場合のバリデーションエラーメッセージを修正する

実際の開発になるとバリデーションのエラーメッセージを設定することがある。その場合、ロケールファイルには以下のように書くと期待通りに表示される。

ja:
  activerecord:
    attributes:
      project/tasks:
        name: 名前

i18nなどで日本語化がされている前提。

リポジトリ

ソースコードはこちらです。

GitHub - kotakanazawa/cocoon-test: set up cocoon gem on Rails 6

cocoon は使うべきか?

フロントとバックエンドを提供していて、だいぶ密結合なgem。フロントとバックエンドを切り離すのが当たり前になっている現代で、cocoon を使うのがベストプラクティスなのかは議論が必要。

とはいえ、個人開発目的で、技術力不足を補うために使うならアリだと思う。

参考