【Rails】Cocoonを用いて親子テーブルの内容を任意の数だけ保存する。

#この記事/メモについて

本記事では"cocoon"というgemを用いて、モデルに紐づいた子モデルの内容を同時に保存できるようにします。

具体的には以下のようにして、
menuの投稿時にhas_manyで紐づいたfoodモデルを、
またfoodモデルにhas_manyで紐づいたmaterialモデルを任意の数だけ保存できるようにします。

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409257/1c95e397-6016-8a32-a49c-ebc9628fb38c.gif)

ここでは、表のような情報を開発者が入れる初期データとして保存していこうと思います。

menuモデル

| ID |   name  　  |     plan                 |     
|:--:|:-----------:|:------------------------:|
| 1  |   献立① 　    |  朝ごはん用　 |    
| 2  |   献立② 　    |  昼ごはん用　 |    


foodモデル

| ID |   name  　  |    menu_id        |     
|:--:|:-----------:|:------------------|
| 1  |   味噌汁 　    |  1　 |  
| 2  |   納豆ご飯 　    |  1　 |   
| 3  |  オムライス 　    |  2　 |  

materialモデル
  
| ID |   name  　  |    food_id        |     
|:--:|:-----------:|:------------------|
| 1  |   味噌 　    |  1　 |  
| 2  |   ねぎ 　    |  1　 |   
| 3  |  納豆 　    |  2　 |  
| 4  |  ご飯　    |  2　 |  
| 5  |  卵　    |  3　 |  
| 6  |  ケチャップ　    |  3　 |  

##前提
rubyのバージョン

```
$ ruby -v
=> ruby 2.7.2p137
```

railsのバージョン

```
$ rails -v
=> Rails 6.1.3.2
```


##手順 

rails newは済んでいる前提です！

⓪gemのインストールなどの下準備

１、必要なgemのインストール

```ruby:Gemfile
gem 'cocoon'
gem 'jquery-rails'
```

```
$ bundle install
```

２、app/javascript/packs/application.jsに以下を追記


```js:app/javascript/packs/application.js
require('jquery')
import "cocoon";

//= require cocoon
//= require jquery
```

３、jqueryの導入

```
$ yarn add jquery 
```

４、config/webpack/environment.jsの編集

**ファイル全体**を以下のように変更。

```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
```

５、cocoonの導入

```
$ yarn add github:nathanvda/cocoon#c24ba53
```

package.jsonに以下が追記されていることを確認

```package.json
"cocoon": "github:nathanvda/cocoon#c24ba53",
```

①menuの大枠を作成。

```
$ rails g scaffold menu name:string plan:string
```

②menuの子モデルとしてfoodモデルを作成

```
$ rails g model food name:string menu_id:integer
```

③foodの子モデルとしてmaterialモデルを作成


```
$ rails g model material name:string food_id:integer
```

④マイグレーション

```
$ rails db:migrate
```

全体のDBは以下の通り。

```ruby:db/schema.rb
create_table "foods", force: :cascade do |t|
    t.string "name"
    t.integer "menu_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "materials", force: :cascade do |t|
    t.string "name"
    t.integer "food_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "menus", force: :cascade do |t|
    t.string "name"
    t.string "plan"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
```

④アソシエーションの整理

```ruby:models/menu.rb
 has_many :foods, inverse_of: :menu
 accepts_nested_attributes_for :foods, allow_destroy: true, update_only: true
```

```ruby:models/food.rb
 belongs_to :menu,inverse_of: :foods
 has_many :materials, inverse_of: :food
 accepts_nested_attributes_for :materials, allow_destroy: true
```

```ruby:models/material.rb
 belongs_to :food,inverse_of: :materials
```

⑤ViewやController周りの整理

１、投稿フォームの作成
＊今回はscaffoldを用いているので、記述場所を`_form.html.erb`にしています。
ご自身のフォルダに合わせてください！

（特にscaffoldを用いているのでform_withのmenuが@menuではなくなっています。）

またedit.html.erbとnew.hrml.erbの内容は一緒で大丈夫です!

```erb:app/views/menus/_form.html.erb
<%= form_with(model: menu, local: true)  do |f| %>

  <div class="menu_field">
    メニューのname<%= f.text_field :name %>
    メニューのplan名<%= f.text_field :plan %>
  </div>

## 以下の部分によってmenuの子モデルであるfoodのフォームを非同期で追加することが可能です。
## renderによってこれから作成する別ファイルの内容を引っ張ってきています。

  <div id="foods">
      <%= f.fields_for :foods do |food| %>
          <%= render 'food_fields',f: food %>
      <% end %>

     <div class="links">
          <%= link_to_add_association "Add foods",f,:foods %>
     </div>
  </div>

##  

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
```

`app/views/menus`の中に`_food_fields.html.erb`を作成します。（上記のrenderで用いるため）



```erb:app/views/menus/_food_fields.html.erb
 <div class="nested-fields">
    フードのname<%= f.text_field :name %>

 ## 以下の部分によってfoodの子モデルであるmaterialのフォームを非同期で追加することが可能です。
        <%= f.fields_for :materials do |material| %>
            <%= material.text_field :name %>
            <%= render "material_fields", f: material %>
        <% end %>
        <div class="links">
            <%= link_to_add_association "Add materials",f,:materials %>
        </div>

## 以下のコードは追加したfoodのフォームを削除するためのもの。
 <%= link_to_remove_association "Delete", f %>
 </div>
```

孫モデルを作りたい時は`app/views/menus`の中に`_material_fields.html.erb`を作成します。（上記のrenderで用いるため）



```erb:app/views/menus/_material_fields.html.erb
<div class="nested-fields">
   <%= f.hidden_field :_destroy %>
   マテリアルのname<%= f.text_field :name %>

## 以下のコードは追加したmaterialのフォームを削除するためのもの。
   <%= link_to_remove_association "Delete", f %>
</div> 
```

２、コントローラーの修正

private以下のストロングパラメーターの記述について変更します。

```ruby:app/controllers/menus_controller.rb

 private
    # menu_paramsのストロングパラメータの記述を変更。

    def menu_params
      params.require(:menu).permit(:name, :plan,
        foods_attributes: [:id, :name, :_destroy,
        materials_attributes: [:id, :name, :_destroy]])
    end

```


#参考

Rails6でのcocoon
https://qiita.com/ashketcham/items/87e3db665ca9c66ce673