Ruby on Railsのバージョン7に、cocoonを導入してみました。 以下にまとめます。
導入時の環境について
導入時の環境は以下の通り。
cocoonとは?
cocoonとは、親子関係をもつモデルの子の行を追加したり削除したりといった、Javascriptで書くととかく厄介な処理の面倒を見てくれるRails用のgemです。
ちなみに、単にcocoonで検索すると、Wordpressの人気無料テーマの記事が多数ヒットします。
可能な限り「rails cocoon」で検索することをおすすめいたします。
cocoonの導入手順
Gemfileに追加し、installする
Gemfile
に以下の行を追加します。
gem 'cocoon'
以下のコマンドを実行します。
bundle install
JQueryを導入する
cocoonを動作した際にJQueryを要求された場合、以下のコマンドを実行します。
bin/importmap pin jquery
config/importmap.rb
を確認し、挿入されていない場合は、追記してください。
# config/importmap.rb pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.7.1/dist/jquery.js"
さらに、app/javascript/application.js
を確認し、挿入されていない場合は、追記してください。
// app/javascript/application.js import jquery from "jquery" window.$ = jquery
まずは実行してみる
modelおよびscaffoldの作成
親子のテーブルを作成します。
親はscaffold、子はmodelのみを作成します。
テーブル・カラム名は他のサイトでも見かける名前にしておきます。
●親のテーブルを作成
bundle exec rails g scaffold Project name:string description:string
●子のテーブルを作成
bundle exec rails g model Task description:string done:boolean project:belongs_to
作成された、project.rb
を追記します。
# app/models/project.rb has_many :tasks, dependent: :destroy accepts_nested_attributes_for :tasks, reject_if: :all_blank, allow_destroy: true
task.rb
も追記します。
# app/models/task.rb belongs_to :project
以下のコマンドを実行することで、データベースにテーブルができると思います。
rake db:migrate
本来ならば、バリデーションやテストの記述も必要だと思いますが、割愛します。
ビューの作成
app/views/projects/_form.html.haml
を差し替えます。
= form_for @project do |f| .field = f.label :name = f.text_field :name .field = f.label :description = f.text_field :description %hr %h3 Tasks #tasks = f.fields_for :tasks do |task| = render 'task_fields', :f => task .links = link_to_add_association 'add task', f, :tasks, class: 'btn btn-success' %hr = f.submit 'Save', class: 'btn btn-primary'
行追加リンクは、子の一覧のあとに挿入します。
次にapp/views/projects/_task_fields.html.haml
を作成します。
.nested-fields .field = f.label :description = f.text_field :description .field = f.check_box :done = f.label :done = link_to_remove_association "remove task", f, class: 'btn btn-danger' %hr
行削除リンクは、行単位に設定します。
コントローラーの変更
app/controllers/projects_controller.rb
を変更します。
(省略) # GET /projects/new def new @project = Project.new # 子モデルも作成 @project.tasks.build end (中略) def create @project = Project.new(project_params) respond_to do |format| if @project.save format.html { redirect_to project_url(@project), notice: "Project was successfully created." } format.json { render :show, status: :created, location: @project } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @project.errors, status: :unprocessable_entity } end end end (中略) def update respond_to do |format| if @project.update(project_params) format.html { redirect_to project_url(@project), notice: "Project was successfully updated." } format.json { render :show, status: :ok, location: @project } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @project.errors, status: :unprocessable_entity } end end end (中略) private # Use callbacks to share common setup or constraints between actions. def set_project @project = Project.find(params[:id]) end # Only allow a list of trusted parameters through. def project_params # 子供のパラメータも追加 params.require(:project).permit(:name, :description, tasks_attributes: [:id, :description, :done, :_destroy]) end
def new
の@project.tasks.build
にて、新規登録画面で、1行がデフォルト表示されます。
def create
およびdef update
のバリデーション失敗の場合も行がunprocessable_entity
となっていない場合は、差し替えてください。
さらに、private
以降のストロングパラメータ定義で、tasks_attributes: [:id, :description, :done, :_destroy])
のように子のパラメータも定義してください。
これにより、登録・変更・削除で自動的に子も追加・変更・削除されます。
Javascriptの追記(任意)
「行追加時に任意の行から値をコピーしたい」「ある行数を超えたら行追加のボタンを消したい」という場合は、JQueryでゴリゴリ書いていくことになります。
一例を提示しておきます。
●app/javascript/application.js
// app/javascript/application.js $(document).on('ready page:load turbolinks:load turbo:load', function() { // 共通変数の定義など // こちらにonload処理を記述 // 各アクション毎に処理を挿入することができる $('#tasks').on('cocoon:before-insert', function() { console.log("cocoon:before-insert!"); }) .on('cocoon:after-insert', function() { console.log("cocoon:after-insert!"); }) .on('cocoon:before-remove', function() { console.log("cocoon:before-remove!"); }) .on('cocoon:after-remove', function() { console.log("cocoon:after-remove!"); }); });
ブラウザのF12の操作ログをで動作確認を行ってください。
#tasks
はapp/views/projects/_form.html.haml
の10行目のidを指定します
ready page:load turbolinks:load turbo:load
は、発火するためのおまじないのように見えます。
turbo(以前はturbo-link)はJavascriptの動作が度々停止するようなので、このようになっているようです。
実行
bundle exec rails s
にて、動作を確認しましょう。
致命的?な問題が発生
動作確認を行うと、バリデーションエラー時にJavascriptがうまく実行できない
動作確認では、主に以下の箇所をチェックするはずです。
- 新規追加の場合に空行が1行表示されているか?
- 追加ボタン、削除ボタンの表示
- 同、動作確認
- 新規追加の場合、明細行が正しく追加されるか?
- 変更の場合
- 行削除した行が登録されていないか?
- 新たに追加した行が登録されているか?
- 正しく変更されているか?
- 子が二重登録されないか?
- 削除の場合、親子両方が削除されているか?
reject_if: :all_blank
- 記述を外した場合、子が0件の場合エラーとなるか?
- バリデーションエラー(422:unprocessable_entity)でも、JQuery内に記述した処理が行われるか?
ほとんどの処理はうまく実行されると思いますが、アンダーラインを引いた「バリデーションエラー(422:unprocessable_entity)でも、JQuery内に記述した処理が行われるか?」が、なぜかうまく行かないことに気がつくはずです。
多分に、またTurboの仕業だな!と思ったりしているでしょうが、はい、そのとおりです。
これは回避可能か?
結論を書くと、回避可能です。
Turboを束ねるHotwireのある機能を使えば、ふた通りの解決方法がありそうです。
解決策は下記リンクをご参照ください。