[Rails6]　動的フォームの追加ボタンが反応しないで困った（cocoon）

#はじめに
動的フォームの導入から`cocoon`というgemに触れて、かなり導入に手間取ったので記事にして整理したいと思う。

#手間取った部分
rails5の記事だが[こちら](https://qiita.com/kmjooh/items/a1613531873a22fa7862)の記事を参考に、rails6の環境で導入を試みたところ、`link_to_remove_association`や`link_to_add_association`のボタンが反応しない事に悩まされた。

#結論として
`cocoon`の導入で問題があった。
[こちら](https://github.com/nathanvda/cocoon/issues/555)の記事を参考して、`app/jacascript/src/cocoon.js`を作成して、`app/javascript/packs/application.js`に`require("src/cocoon")`を追加することで解決した。

#cocoonの導入
`jquery`は[こちら](https://qiita.com/tatsuhiko-nakayama/items/b2f0c77e794ca8c9bd74)を参考に導入。

```Gemfile.
gem "cocoon"
```
`Gemfile`に`cocoon`を追加。
`jquery-rails`は追加せず
`bundle install`をしてgemをインストール。

次にターミナルに移る。

```terminal
$ yarn add @nathanvda/cocoon 
```
`yarn`に`cocoon`を追加。

他の方の記事では`yarn add github:nathanvda/cocoon#c24ba53
`をコミットIDを指定しているやり方もあったのだが、[公式](https://github.com/nathanvda/cocoon)を見てみると`rails6`の導入方法が書かれていたので公式の方を叩く。

補足として`yarn add github:nathanvda/cocoon#c24ba53`は、公式が`rails5`までしか対応していないための手段である事と、自分の環境では`zsh: no matches found: github:nathanvda/cocoon#c24ba53`と出てしまいコマンドが実行されなかった経緯があった。

公式にある導入の続き。

```app/javascripts/packs/application.js
require("jquery")
require("@nathanvda/cocoon")
```
ここまでが公式にある`rails6`に対応した`cocoon`の導入になる。

これだけではボタンが反応してくれなかったので、下記を追加。
まずターミナルに移動してフォルダとファイルを作成

```terminal
# srcを作成
mkdir app/javascript/src

# cocoon.jsを作成
touch app/javascript/src/cocoon.js
```
cocoon.jsの中身を書いていく。

```app/javascript/src/cocoon.js
(function($) {

  var cocoon_element_counter = 0;

  var create_new_id = function() {
    return (new Date().getTime() + cocoon_element_counter++);
  }

  var newcontent_braced = function(id) {
    return '[' + id + ']$1';
  }

  var newcontent_underscord = function(id) {
    return '_' + id + '_$1';
  }

  var getInsertionNodeElem = function(insertionNode, insertionTraversal, $this){

    if (!insertionNode){
      return $this.parent();
    }

    if (typeof insertionNode == 'function'){
      if(insertionTraversal){
        console.warn('association-insertion-traversal is ignored, because association-insertion-node is given as a function.')
      }
      return insertionNode($this);
    }

    if(typeof insertionNode == 'string'){
      if (insertionTraversal){
        return $this[insertionTraversal](insertionNode);
      }else{
        return insertionNode == "this" ? $this : $(insertionNode);
      }
    }

  }

  $(document).on('click', '.add_fields', function(e) {
    e.preventDefault();
    e.stopPropagation();
    
    var $this                 = $(this),
        assoc                 = $this.data('association'),
        assocs                = $this.data('associations'),
        content               = $this.data('association-insertion-template'),
        insertionMethod       = $this.data('association-insertion-method') || $this.data('association-insertion-position') || 'before',
        insertionNode         = $this.data('association-insertion-node'),
        insertionTraversal    = $this.data('association-insertion-traversal'),
        count                 = parseInt($this.data('count'), 10),
        regexp_braced         = new RegExp('\\[new_' + assoc + '\\](.*?\\s)', 'g'),
        regexp_underscord     = new RegExp('_new_' + assoc + '_(\\w*)', 'g'),
        new_id                = create_new_id(),
        new_content           = content.replace(regexp_braced, newcontent_braced(new_id)),
        new_contents          = [],
        originalEvent         = e;


    if (new_content == content) {
      regexp_braced     = new RegExp('\\[new_' + assocs + '\\](.*?\\s)', 'g');
      regexp_underscord = new RegExp('_new_' + assocs + '_(\\w*)', 'g');
      new_content       = content.replace(regexp_braced, newcontent_braced(new_id));
    }

    new_content = new_content.replace(regexp_underscord, newcontent_underscord(new_id));
    new_contents = [new_content];

    count = (isNaN(count) ? 1 : Math.max(count, 1));
    count -= 1;

    while (count) {
      new_id      = create_new_id();
      new_content = content.replace(regexp_braced, newcontent_braced(new_id));
      new_content = new_content.replace(regexp_underscord, newcontent_underscord(new_id));
      new_contents.push(new_content);

      count -= 1;
    }

    var insertionNodeElem = getInsertionNodeElem(insertionNode, insertionTraversal, $this)

    if( !insertionNodeElem || (insertionNodeElem.length == 0) ){
      console.warn("Couldn't find the element to insert the template. Make sure your `data-association-insertion-*` on `link_to_add_association` is correct.")
    }

    $.each(new_contents, function(i, node) {
      var contentNode = $(node);

      var before_insert = jQuery.Event('cocoon:before-insert');
      insertionNodeElem.trigger(before_insert, [contentNode, originalEvent]);

      if (!before_insert.isDefaultPrevented()) {
        // allow any of the jquery dom manipulation methods (after, before, append, prepend, etc)
        // to be called on the node.  allows the insertion node to be the parent of the inserted
        // code and doesn't force it to be a sibling like after/before does. default: 'before'
        var addedContent = insertionNodeElem[insertionMethod](contentNode);

        insertionNodeElem.trigger('cocoon:after-insert', [contentNode,
          originalEvent]);
      }
    });
  });

  $(document).on('click', '.remove_fields.dynamic, .remove_fields.existing', function(e) {
    var $this = $(this),
        wrapper_class = $this.data('wrapper-class') || 'nested-fields',
        node_to_delete = $this.closest('.' + wrapper_class),
        trigger_node = node_to_delete.parent(),
        originalEvent = e;

    e.preventDefault();
    e.stopPropagation();

    var before_remove = jQuery.Event('cocoon:before-remove');
    trigger_node.trigger(before_remove, [node_to_delete, originalEvent]);

    if (!before_remove.isDefaultPrevented()) {
      var timeout = trigger_node.data('remove-timeout') || 0;

      setTimeout(function() {
        if ($this.hasClass('dynamic')) {
            node_to_delete.detach();
        } else {
            $this.prev("input[type=hidden]").val("1");
            node_to_delete.hide();
        }
        trigger_node.trigger('cocoon:after-remove', [node_to_delete,
          originalEvent]);
      }, timeout);
    }
  });


  $(document).on("ready page:load turbolinks:load turbo:load", function() {
    $('.remove_fields.existing.destroyed').each(function(i, obj) {
      var $this = $(this),
          wrapper_class = $this.data('wrapper-class') || 'nested-fields';

      $this.closest('.' + wrapper_class).hide();
    });
  });

})(jQuery);

```

`yarn`にも`cocoon-js`を追加

```terminal
yarn add cocoon-js
```

最後に`cocoon.js`を読み込む記述を追加。

```app/javascript/packs/application.js
import 'cocoon-js';
require ("src/cocoon")
```
中身を理解せずに解決してしまっているので不安なところがあるが、とりあえずこれで`link_to_remove_association`と`link_to_add_association`が反応するようになったので参考までに。

#参考元
[公式ドキュメント](https://github.com/nathanvda/cocoon)
[公式issues#555](https://github.com/nathanvda/cocoon/issues/555)
[【Rails】フォームの追加・削除（cocoon）](https://qiita.com/kmjooh/items/a1613531873a22fa7862)
[Rails6でのjQuery導入方法](https://qiita.com/tatsuhiko-nakayama/items/b2f0c77e794ca8c9bd74)
[](https://github.com/nathanvda/cocoon/issues/555)