【Rails6】親モデルと一緒に複数の子レコードを一括で登録/編集するフォームの作り方


Ruby on Railsで複数のモデル、複数のレコードを1つのフォームで一括登録/編集したい、Javascriptはあまり書かずに...という機会があったので、その実装について今回まとめてみました。


## 完成イメージ


- レシピのレコードと材料のレコードを同時に作成/編集できること
- 材料レコードは一括で複数作成/編集できること

![sample_complete_recipe.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/60770925-96bf-a5a6-6c86-99f7a9856a3c.gif)


この記事を終えると、こんなアプリが出来るようになります。

## 前提

#### バージョン

- ruby 2.7.2
- rails 6.1

#### 扱うモデル

- Recipe(レシピ) Model
- Ingredient(材料) Model

レシピは複数の材料を持っている

![スクリーンショット 2021-03-13 22.05.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/ee896a30-b9b5-6131-f948-f13f8dbeba4b.png)

このような構成はすでに出来ている状態から始めます。具体的なmodelは以下を参考。

#### app/models/recipe.rb

```ruby
# == Schema Information
#
# Table name: recipes
#
#  id         :integer          not null, primary key
#  name       :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Recipe < ApplicationRecord
  has_many :ingredients, dependent: :destroy
end
```

#### app/models/ingredient.rb

```ruby
# == Schema Information
#
# Table name: ingredients
#
#  id         :integer          not null, primary key
#  recipe_id  :integer          not null
#  name       :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
# Indexes
#
#  index_ingredients_on_recipe_id  (recipe_id)
#
# Foreign Keys
#
#  recipe_id  (recipe_id => recipes.id)
#
class Ingredient < ApplicationRecord
  belongs_to :recipe
end
```



## 手順

さてここから、完成を目指して一つずつ手順を記していきます

### 1. accepts_nested_attributes_forを追加

app/models/recipe.rbにaccepts_nested_attributes_forを追加します。

```ruby
class Recipe < ApplicationRecord
  has_many :ingredients, dependent: :destroy
  # ↓追加
  accepts_nested_attributes_for :ingredients, allow_destroy: true, reject_if: :all_blank
end
```

参考：ActiveRecord::NestedAttributes::ClassMethods

https://api.rubyonrails.org/v6.1.0/classes/ActiveRecord/NestedAttributes/ClassMethods.html

### 2. cocoonを追加

cocoonとは、複数レコードをインタラクティブに追加、編集、削除を簡単に行うことが出来るようにするGem, Packageです

1. Gemのcocoonをインストール

    ↓のようにGemfileに[cocoon](https://github.com/nathanvda/cocoon)を追加して `bundle install`します

     ```ruby
   gem "cocoon"
   ```

2.  cocoonのpackage追加

    ```s
    yarn add @nathanvda/cocoon 
    ```  
3.  app/javascripts/packs/application.js の編集

    ```js
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
// これを追加
import "@nathanvda/cocoon"
Rails.start()
Turbolinks.start()
ActiveStorage.start()
```

### 3. jqueryを使えるようにする

1.  config/webpack/plugins/jquery.jsを作成

    ```sh
$ bundle exec rails webpack:install
$ mkdir config/webpack/plugins/
$ touch config/webpack/plugins/jquery.js
```
2. config/webpack/plugins/jquery.jsに以下を記述

    ```js
const { environment } = require('@rails/webpacker')
const jquery = require('./plugins/jquery')
environment.plugins.prepend('jquery', jquery)
module.exports = environment
```

3. config/webpack/environment.js でjqueryを読み込む

    ```js
const { environment } = require('@rails/webpacker')
const jquery = require('./plugins/jquery')
environment.plugins.prepend('jquery', jquery)
module.exports = environment
```


### 4. RecipeController作成とRoutesを設定

1. `bundle exec rails g controller recipes`でController作成

    ```s
$ bundle exec rails g controller recipes
Running via Spring preloader in process 38240
      create  app/controllers/recipes_controller.rb
      invoke  erb
      create    app/views/recipes
      invoke  test_unit
      create    test/controllers/recipes_controller_test.rb
      invoke  helper
      create    app/helpers/recipes_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/recipes.scss
```

2. config/routes.rbにrecipeのpathを追加

    ```ruby
    Rails.application.routes.draw do
      root "recipes#index"
      resources :recipes, only: [:index, :new, :create]
    end
```

### 5. Recipeの一覧ページを作成

1. app/controller/recipes_controller.rbにindexアクション追加

    ```ruby
class RecipesController < ApplicationController
  def index
    @recipes = Recipe.all
  end
end
```

2. Recipeの一覧ページを作成

```shell
$ touch app/views/recipes/index.html.erb
```

```erb
<h1>レシピ一覧</h1>
<ul>
  <% @recipes.each do |recipe| %>
    <li><%= link_to recipe.name, edit_recipe_path(recipe) %></li>
    <ul>
      <% recipe.ingredients.each do |ingredient| %>
        <li><%= ingredient.name %></li>
      <% end %>
    </ul>
  <% end %>
</ul>
<%= link_to "レシピの新規作成", new_recipe_path %>
```

するとこんなページが出来上がります

![スクリーンショット 2021-03-14 2.19.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/d0937558-3db9-07b9-78f7-c9bd30bf6fb3.png)


### 6. Recipeの新規作成ページを作成

1. app/controller/recipes_controller.rbにnewアクション追加

    ```ruby
class RecipesController < ApplicationController
  ~~ 省略 ~~~
  def new
    @recipe = Recipe.new
  end
end
```

2. app/views/recipes/new.html.erb を作成

     ```shell
$ touch app/views/recipes/new.html.erb
```
```erb
<h1>レシピの新規作成</h1>
<%= render "recipes/form", model: @recipe %>
```
3. app/views/recipes/_form.html.erbを作成

    ```shell
$ touch app/views/recipes/_form.html.erb
```

    ```erb
    <%= form_with model: model do |form| %>
      <div>
        <%= form.label :name, "レシピ名" %>
        <%= form.text_field :name %>
     </div>
     <div id="ingredients">
       <%= link_to_add_association '材料の追加', form, :ingredients,
         data: { association_insertion_method: 'append' } %>
       <%= form.fields_for :ingredients do |ingredient| %>]
         <%# ファイル名はingredient_fieldsにしてください %>
         <%= render "recipes/ingredient_fields", f: ingredient %>
       <% end %>
      </div>
      <%= form.submit "作成する" %>
  <% end %>
```

4. app/views/recipes/_ingredient_fields.html.erbを作成

    ```shell
$ touch app/views/recipes/_ingredient_fields.html.erb
```

```ruby
<div class="nested-fields">
  <%= f.label :name, "材料名" %>
  <%= f.text_field :name %>
  <%= link_to_remove_association "削除", f %>
</div>
```

ここまで行うと、以下のような画面になります

![sample_recipe.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/0f43a399-4ebf-bc0f-3a72-d33aba101a5c.gif)


### 7. レシピ、材料の作成(Create)アクションを作成

```ruby
class RecipesController < ApplicationController

  def new
    @recipe = Recipe.new
  end
  # ここから
  def create
    @recipe = Recipe.new
    if @recipe.update(recipe_params)
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def recipe_params
    # :idがないと毎回新しくレコードが作られてしまいます
    # :_destroyがないと削除ができないです
    params.require(:recipe).permit(:name, ingredients_attributes: [:id, :name, :_destroy])
  end
  # ここまで追加
end

```

createアクションの追加が完了すると、レシピと材料を一括で作成してDBに保存する処理まで出来るようになります！


### 8. レシピの編集ページを作成

1. app/views/recipes/edit.html.erbの作成

    ```sh
$ touch app/views/recipes/edit.html.erb
```

2. app/views/recipes/edit.html.erbの編集

    ```erb
<h1><%= @recipe.name %>の編集</h1>
<%= render "recipes/form", model: @recipe %>
```

3. app/controllers/recipes_controller.rbの編集

```ruby
class RecipesController < ApplicationController
~~ 省略 ~~
  def edit
    @recipe = Recipe.find(params[:id])
  end
~~ 省略 ~~
end 
```

こんなページが出来上がります
![スクリーンショット 2021-03-14 2.28.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/f4876641-3ae1-8afe-59b5-bb26a17dc1da.png)


### 9. レシピのupdateアクションを作成

app/controllers/recipes_controller.rbにupdateアクションを追加

```ruby
class RecipesController < ApplicationController
~~ 省略 ~~
  def update
    @recipe = Recipe.find(params[:id])
    if @recipe.update(recipe_params)
      redirect_to root_path
    else
      render :edit
    end
  end
~~ 省略 ~~
end 
```

これで一括の更新もできるようになりました！

## 終わりに


長かったですが、これで以上になります。
あまりJavaScriptを書かずに動的にフォームの追加や削除をしたいということがあったので、今回のような実装について書きました。

同じようなことがあればぜひご参考ください！