【Rails】nested formの実装方法をcocoonからstimulusに変更

# はじめに
cocoonで実装していたnested formを、cocoonを使わずにstimulusで実装した際の記録です。


# 環境
- Rails 7.0.8
- ruby 3.3.5
- propshaft
- importmap-rails

#### cocoon関連のライブラリ/パッケージ

https://github.com/nathanvda/cocoon

https://github.com/oddcamp/cocoon-vanilla-js

# 手順
1\. cocoonをアンインストールする
2\. フォームを変更する
3\. stimulusコントローラを作成する

## 1\. cocoonをアンインストールする
- gemのcocoon
```
$ gem uninstall cocoon　
$ bundl install　# Gemfileから削除後に実行
```
- npmパッケージのcocoon-vanilla-js
```
$ ./bin/importmap unpin @oddcamp/cocoon-vanilla-js
```
```application.js
import "@oddcamp/cocoon-vanilla-js" /*この行を削除する
```

## 2\. フォームを変更する

### 構造　
- `_form.html.erb` 内で `_fuga_fields.html.erb` をレンダリング
- 親モデルがHoge、子モデルがFuga

### _form.html.erb
- 変更前

```erb
<%= form_with model: @hoge do |form| %>
  
  <%= form.fields_for :fuga do |f| %>
    <%= render 'fuga_fields', f: f %>
  <% end %>
  
  <div id="detail-association-insertion-point"></div>
  
  <%= link_to_add_association "追加", form, :fuga,
  class: 'btn btn-info',
  data: { association_insertion_node: '#detail-association- 
  insertion-point', association_insertion_method: 'append'} %>

  <%= form.submit '保存', class:'btn btn-success' %>

<% end %>
```

- 変更後
```erb
<%= form_with model: @hoge do |form| %>

  <div data-controller="nested-form">
    <template data-nested-form-target="template">
      <%= form.fields_for :fugas, Fuga.new, child_index: 'TEMPLATE_RECORD' do |f| %>
        <%= render 'fuga_fields', f: f %> 
      <% end %>
    </template>
  
    <%= form.fields_for :fugas do |f| %>
      <%= render 'fuga_fields', f: f %> 
    <% end %>
    <div data-nested-form-target="addLicense">
      <%= link_to "追加", "#", data: {action: "click->nested-form#addAssociation"}, class: 'btn btn-info'%>
    </div>
  <%= form.submit '保存', class:'btn btn-success' %>
  </div>
  
<% end %>
```
#### 解説
- `<div "data-controller="nested-form">...</div>` ：divタグ内の範囲をnested_form_controller.jsのファイルに紐づける
- templateタグ：JavaScriptを使用する前提のタグ（単体では表示されない）
- `data-nested-form-target="xxx"`：nested_form_controller.js内のstatic targets = ["xxx"]と紐づける
- `child_index: TEMPLATE_RECORD`：新しく生成されたFugaオブジェクトを識別するために用意
- `data: {action: "click->nested-form#addAssociation"}` ：clickでnested_form_controller.jsのaddAssociationメソッドを呼び出す

### _fuga_fields.html.erb
- 変更前
```
<%= link_to_remove_association "削除", f, class:"btn btn-outline-danger" %>
```
- 変更後
```
<%= link_to "削除", "#", data: {action: "click->nested-form#removeAssociation"}, class:"btn btn-outline-danger" %></div>
<%= f.hidden_field :_destroy %>
```
#### 解説
- `data: {action: "click->nested-form#removeAssociation"}`：clickでnested_form_controller.jsのremoveAssociationメソッドを呼び出す
- `_destroy` ： `accepts_nested_attributes_for` に `allow_destroy: true`が指定されている場合にtrueが返ると関連付けられているオブジェクトが削除される

## 3\. stimulusのコントローラを作成する
```
$ rails generate stimulus nested_form
      create  app/javascript/controllers/nested_form_controller.js
```

```nested_form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {

  static targets = ["addLicense", "template"]

  addAssociation(event){
    event.preventDefault()
    var content = this.templateTarget.innerHTML.replace(/TEMPLATE_RECORD/g, Math.floor(Math.random()*100))
    this.addLicenseTarget.insertAdjacentHTML('beforebegin', content)
  }

  removeAssociation(event){
    event.preventDefault()
    let item = event.target.closest(".nested-fields")
    item.querySelector("input[name*='_destroy']").value = 1
    item.style.display = 'none'
  }
}
```
#### 解説
- `this.templateTarget.innerHTML.replace(/TEMPLATE_RECORD/g, Math.floor(Math.random()*100))` ：`TEMPLATE_RECORD` を、オブジェクトを識別できるようにランダムな数字で置き換える

# おわりに
stimulusを使用することで、JavaScriptをあまり書かずに実装することができました。
nested formを実装できるstimulus componentも用意されています。こちらを利用すると記述量がさらに少なくできるようです。

https://www.stimulus-components.com/docs/stimulus-rails-nested-form


# 参考
https://www.driftingruby.com/episodes/nested-forms-from-scratch-with-stimulusjs