shuhei-T / reptiles_log

爬虫類飼育者のためのかんたん飼育記録サービス「レプログ!」
https://reptileslog.com/
8 stars 1 forks source link

カレンダーの記録登録モーダルの中でcocoonのcallbackが発火しない #95

Closed shuhei-T closed 2 years ago

shuhei-T commented 2 years ago

質問内容

Fullcalendarのクリックイベント後のモーダル内で、cocoonのcallback関数内の処理が発火しません。 どうゆう処理かというと、下記の三点になります。

実装に際して参考にした記事と、公式GitHubを載せておきました。

以下正常な動作のGIF動画になります。 Image from Gyazo

こちらが今回問題のフルカレンダー内の動作になります。 Image from Gyazo

アプリのURL https://reptileslog.com/

現状発生している問題・エラーメッセージ

エラーは特に起きませんが、console.log()を仕込ませて発火していないことは確認できています。

どの処理までうまく動いているか

Fullcalendarではなく、app/controllers/logs_controller.rbでも同じ機能を使っているのですが、こちらは問題なく動いています。

該当のソースコード

app/javascript/src/cocoon.js

  let $addFieldBtn = $('.js-add-log_feeds-field-btn');
  let counter = 1;
  let $cocoonField = $('#activity-logs');
  let $headerFeedName = $('#js-feed-name');
  let $feedAmount = 5;

  // cocoonのコールバック
  $cocoonField
  .on('cocoon:after-insert', function() {
    counter++;
    checkCount(counter);
  })
  .on('cocoon:before-remove', function(event) {
    // 削除ボタンを押すとアラートメッセージが入る
    let confirmation = confirm('給餌記録を削除します。よろしいですか?');
    if(!confirmation) {
      event.preventDefault();
    }
  })
  .on('cocoon:after-remove', function() {
    // 給餌数のカウントを減らす
    counter--;
    checkCount(counter);

    // 新しいnameListを定義
    let nameList = [],
    $nestedFields = $cocoonField.find('.nested-fields'),
    selectForms = [];

    // 編集時には、削除したフォームがdisplay: noneで画面内に残るので
    // $nestedFieldから計算用の配列を作成する
    selectForms = filterVisibleField($nestedFields, selectForms)

    let listToCheck = getNameList(selectForms, nameList);

    // エラー関連のcssを削除する
    if (checkSameValue(listToCheck)) {
      removeErrorClasses();
    }
  });

  // 一定の数以上にフィールドを生成しない処理 -------------------------
  function checkCount(count) {
    if (count >= $feedAmount) {
      // 要素はdisabledのpropで動作しなくなるが、見た目はdisabledを付けないと変化しない
      $addFieldBtn.prop('disabled', true);
      $addFieldBtn.addClass('disabled');
    } else if (count < $feedAmount) {
      $addFieldBtn.prop('disabled', false);
      $addFieldBtn.removeClass('disabled');
    }
  }

  // 見えないフィールドをnameList作成から除外するメソッド
  function filterVisibleField($nestedFields, selectForms) {
    $nestedFields.each(function(i, field) {
      if ($(field).css('display') !== 'none') {

        let $selectForm = $($(this).find('select'))
        selectForms.push($selectForm)
      }
    })
    return selectForms
  }

  // 名前が重複していることを通知する処理----------------
  $(document).on("change", ".js-select-form", function(event) {
    let blankList = [];
    alertMessage = document.createElement('span');
    alertContent = document.createTextNode('給餌が重複しています');
    $nestedFields = $cocoonField.find('.nested-fields');
    selectForms = [];

    // エラーメッセージを作成する
    alertMessage.appendChild(alertContent);
    alertMessage.setAttribute('class', 'c-error-message');

    selectForms = filterVisibleField($nestedFields, selectForms)
    let listToCheck = getNameList(selectForms, blankList);

    if (checkSameValue(listToCheck)) {
      removeErrorClasses();
    } else {
      if (!$('.c-error-message').length) {
        $headerFeedName.append(alertMessage);
      }
    }
  });

  // エラー関連のcss削除
  function removeErrorClasses() {
    $('.c-error-message').remove();
  }

  // セレクトフォームの選択中の項目の配列を作る
  function getNameList(selectForms, nameList) {
    selectForms.filter(form => {
      let name = $(form).children("option:selected").text()
      if (name != '選択してください') {
        nameList.push(name);
      }
    })
    return nameList
  }

  // 作った配列の中に、重複した名前が無いか判定
  function checkSameValue(array) {
    let uniqArray = new Set(array);
    if(array.length === 1) {
      return true;
    } else if (uniqArray.size === array.length) {
      return true;
    } else {
      return false;
    }
  }

エラーから考えられる原因

FullcalendarのJSと干渉しているのかなと推測しました。

試したこと

Fullcalendar内のクリックイベント後の中に書く必要があるのかと、試しに書いてみても、発火しません。 https://github.com/shuhei-T/reptiles_log/pull/130 app/javascript/packs/calendar/event.js

eventClick: function(info){
      // 表示されたイベントをクリックしたときのイベント
      let $showId = (info.event.id);
      $.ajax({
        type: 'GET',
        url: 'events/' + $showId,
      }).done(function(res) {
      // 成功処理
      $('#modal').html(res);
        let modal = document.getElementById('modal')
        let modalObj = new Modal(modal)
        modalObj.show();

      let $addFieldBtn = $('.js-add-log_feeds-field-btn');
      let counter = 1;
      let $cocoonField = $('#activity-logs');
      let $headerFeedName = $('#js-feed-name');
      let $feedAmount = 5;

      $cocoonField
      .on('cocoon:after-insert', function() {
        counter++;
        checkCount(counter);
      });

参考記事

gemcocoon公式 callback https://qiita.com/tanutanu/items/a9f435e33b2d4533b3a2

murata0705 commented 2 years ago
document.addEventListener('DOMContentLoaded', function() {
  let $addFieldBtn = $('.js-add-log_feeds-field-btn');
  let counter = 1;
  let $cocoonField = $('#activity-logs');
  let $headerFeedName = $('#js-feed-name');
  let $feedAmount = 5;

  console.log($cocoonField) // 追加

リスナーをセットする際に$('#activity-logs')がまだ存在しない気がするので、リスナーがセットできていない気がしますね。(カレンダーページでは ajax で#activity-logsを作っているので)

DOMContentLoaded は、HTMLが読み込まれたあとで一度だけ実行されるイベントだった気がします。

上記の場所にconsole.logを仕込んで、リスナーがセットできているか確かめてみましょうか。

// app/javascript/packs/calendar/event.js

dateClick: function(info){
      // クリックした日付の情報を取得
      const year = info.date.getFullYear();
      const month = (info.date.getMonth() + 1);
      const day = info.date.getDate();
      // ajaxでevents/newを着火させ、htmlを受け取る
      $.ajax({
        type: 'GET',
        url: 'events/new',
      }).done(function (res) {
        // 成功処理
        // 受け取ったhtmlをさっき追加したmodalのbodyの中に挿入
        $('#modal').html(res);
        let modal = document.getElementById('modal')
        let modalObj = new Modal(modal)
        modalObj.show();

        // フォームの年、月、日を自動入力
        $('#log_logged_at_1i').val(year);
        $('#log_logged_at_2i').val(month);
        $('#log_logged_at_3i').val(day);

        //// このあたりでリスナーセット(#activity-logs が生成されて、取得できることを確かめて)

      }).fail(function (result) {
        // 失敗処理
        alert("new failed");
      });
    },

仮説が正しかった場合、このあたりにリスナー設定処理を入れれば、動くような気がします。

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます! ご指摘のお通り、クリックイベントの箇所でリスナーを設定し、下記コミットで変更を加えました。 bd08259981120a9b0b4ee37c72732a6edf93289e

結果、下記の要件だけ正常に動きました。(よく見るとこちらの処理の関数はcallback内で呼んでいるものではありませんでした。)

残り、下記2点は動いていません。

やはりcallbackは動いていないのかと思い、console.log()を仕込んでみるものの、動いていませんでした。

現状の以下動作になります。console.log($cocoonField)はモーダルをクリックした際に表示されました。

Image from Gyazo

murata0705 commented 2 years ago
require("@nathanvda/cocoon")

event.js にこれ追加してみるとどうです?このページで cocoon が読み込まれているか確かめたい感じです。

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます。下記コミットで変更を加えました。 be97ee09d387e241bd07070b00e3e60bfcd4b51f event.jsにrequire("@nathanvda/cocoon")を追加したところ、以下のような動作になりました。 +ボタンを押すとセレクトフォームが2つ増えて挙動がおかしいですが、callbackが呼ばれているようです。 それぞれcallback内にconsole.log()を差し込みました。 Image from Gyazo

murata0705 commented 2 years ago

コールバックがちゃんと動いているならあとは関数のスコープとかの問題じゃないですかね?

console.log(checkCount(counter));

とかでちゃんと関数が呼べているか見ましょうか?エラーがでているようだったら、関数をコールバック設定の上にかいたり、dateClick 関数から出してみるとどうですかね?

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます!

console.log(checkCount(counter));

undefinedと出てますが、正常に動く飼育記録画面(app/views/logs/new.html.erb)でも試してみたところundefinedと出ていたので、問題ないのかなと思いました。

event.jsにrequire("@nathanvda/cocoon")を追加しましたが、このままでは二重に呼ばれている状態で動作がおかしいので、対処法はどのようにしたら良いですか?

murata0705 commented 2 years ago

あ、戻り値が無いのか。checkCount内でconsole.logしてみて、関数が実行されているか見てもらっていいですか?

event.jsにrequire("@nathanvda/cocoon")を追加しましたが、このままでは二重に呼ばれている状態で動作がおかしいので、対処法はどのようにしたら良いですか?

これはどういうことですか?require("@nathanvda/cocoon")event.js内に設定しなくてもコールバックが動くということですか?

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます。返事が遅れてしまい申し訳ございません。下記コミットにてcheckCount関数内にconsole.logを仕込みました。 b75e782

以下がその後の動作になります。 Image from Gyazo 動画の通り、+ボタンをクリックする毎にcocoonコールバックが呼ばれ、その中のcheckCount関数内のconsole.logが表示されているのが分かります。 ですが、クリックする毎に.nested-fieldが2つずつ増えています。

.nested-fieldというのは、+ボタンをクリックした際に増えるエリアのことです。app/views/logs/_log_feeds_fields.html.erbがまるまる該当のコードになっています。renderでパーシャルを呼んでいます。

以下、app/views/logs/_form.html.erb

<div class="mb-3 feed-area", id='activity-logs'>
  <%= f.fields_for :log_feeds, local: true, id: 'js-log_feeds-field' do |log_feeds_f| %>
    <%= render 'logs/log_feeds_fields', f: log_feeds_f %>
  <% end %>
  <div class="links">
    <div id="js-feed-name"></div>
    <%= link_to_add_association f, :log_feeds, partial: 'logs/log_feeds_fields', class: 'js-add-log_feeds-field-btn' do %>
      <i class="fas fa-plus-circle"></i>
    <% end %>
  </div>
</div>

たしかにrequire("@nathanvda/cocoon")event.jsに設定したことでcocoonのコールバックが動くようになりました。しかし同時に、+ボタンをクリックした際に.nested-fieldが2つずつ増えるようになってしまいました。

require("@nathanvda/cocoon")はもともと、app/javascript/packs/application.js内に設定されています。それが関係しているのかなと思うのですが、どうでしょうか。

.nested-fieldが2つずつ増えないようにするためにはどのように対処したらよいのでしょうか。

以下がevent.jsからrequire("@nathanvda/cocoon")をコメントアウトした際の動きになります。 コールバックが動かなくなっていますが、.nested-fieldも1つずつ増えています。 Image from Gyazo

murata0705 commented 2 years ago
// app/javascript/packs/calendar/event.js
$(document).on("change", ".js-select-form", function(event) {});
$(document).on("change", ".js-select-form", function() {};

この2つのコードを削除すれば動きそうな気がします。

そもそもの不具合の原因をまず整理しましょうか。

// app/javascript/src/cocoon.js
document.addEventListener('DOMContentLoaded', function() {})

ここで定義しているDOMContentLoadedは、DOMが読み込まれたタイミングで発火し、一度しか発火しません。 その中で、

// app/javascript/src/cocoon.js
let $cocoonField = $('#activity-logs');
$cocoonField
  .on('cocoon:after-insert', function() {
    counter++;
    checkCount(counter);
  })
...

cocoon:after-insertをなどを定義していますが、$cocoonFieldは、カレンダーページではDOMが読み込まれたタイミングでは undefined です。そのためリスナーを設定できていません。これがそもそもの不具合の原因です。

// app/javascript/src/cocoon.js
$(document).on("change", ".js-select-form", function(event) {})
$(document).on("change", ".js-select-form", function() {})

しかし、これらは、document にリスナーをセットしているので、問題なくセットできています。 このコードを、

// app/javascript/packs/calendar/event.js

$.ajax({
  type: 'GET',
  url: 'events/new',
}).done(function (res) {
  // ここ
}

上記にセットすると、changeイベントに二重にコールバックがセットされます。これがおそらく二重に処理が走っている原因です。

app/javascript/packs/calendar/event.jsで、cocoonrequireしてみてもらったのは、

$cocoonField
  .on('cocoon:after-insert', function() {
    counter++;
    checkCount(counter);
  })

のようにリスナーをセットしたにも関わらず、コールバックが実行されなかったからです。

cocoon:after-insertイベントのリスナーをセットしているにも関わらず、コールバックが実行されない場合は、cocoon:after-insertイベントが定義されていない可能性があります。

cocoonは使ったことがないので詳細は知りませんが、おそらくライブラリの中でcocoon:after-insertイベントを定義しているはずです。 https://www.hypertextcandy.com/javascript-custom-events

そのため、カレンダーページでcocoonを読み込めていないことを疑いました。

上記解説したあたりの javascript の仕様を再確認して、event.jscocoon.js内に書くべき処理を再検討してみてください。

一部処理が二重になっている以外問題ないのであれば、

// app/javascript/packs/calendar/event.js
$(document).on("change", ".js-select-form", function(event) {});
$(document).on("change", ".js-select-form", function() {};

の削除だけでいけそうな気はします。

murata0705 commented 2 years ago

event.jscocoon.js内のそれぞれの

// event.js
$(document).on("change", ".js-select-form", function(event) {
  console.log(1)
})
// cocoon.js
$(document).on("change", ".js-select-form", function(event) {
  console.log(2)
})

みたいに仕込めば、二重にセットされているか確かめられそうです。

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます!下記の給餌の画像を出す処理に関しては、そもそも最初から(cocoon.jsのみに書いてある状態のときから)処理が動いてましたので、event.jsに入れておりません。

  // 給餌の画像を出す処理
  $(document).on("change", ".js-select-form", function() {
    // 選択したoptionのvalueを取得
    let val = $(this).val();
    // 親要素から画像リストを取得
    let $imageList = $(this).parent().parent().parent().find('ul li');
    // 先頭に#を付けてvalueの値をidに変換
    let selectFeedId = '#' + val;
    // 一度すべてのブロックを非表示にする
    $imageList.hide();
    // 選択したブロックのみを表示
    if (selectFeedId === '#') {
      return 
    } else {
      $(this).parent().parent().parent().find('ul').find(selectFeedId).show(); 
    }
  });

event.jscocoon.jsそれぞれの名前が重複していることを通知する処理の中にconsole.logを仕込みました。

// 名前が重複していることを通知する処理----------------
$(document).on("change", ".js-select-form", function(event) {
  console.log("event.js側の名前が重複していることを通知する処理");
// 名前が重複していることを通知する処理----------------
$(document).on("change", ".js-select-form", function(event) {
  console.log("cocoon.js側の名前が重複していることを通知する処理");

以下がその後の動作になります。両方から処理が呼ばれていることが分かりました。 Image from Gyazo

event.js名前が重複していることを通知する処理をコメントアウトしてみました。 以下がその後の動作になります。 Image from Gyazo 赤文字で「給餌が重複しています」と出なくなったので名前が重複していることを通知する処理が正常に動作しなくなりました。中の処理でさらに原因があるのかと思います。 また、event.jsから名前が重複していることを通知する処理を削除しても、+ボタンをクリックした際に.nested-fieldが2つずつ増えています。 名前が重複していることを通知する処理はセレクトボックスを増やす処理に関しては直接関係ないので、コメントアウトしても.nested-fieldが2つずつ増えていることには関係ないのかなと思います。

link_to_add_associationを押すことで.nested-fieldsが増える仕組みについてですが、cocoon内部で用意されているものだと認識しています。 https://github.com/nathanvda/cocoon#examples

ですので、やはりevent.jsapp/javascript/packs/application.js両方にrequire("@nathanvda/cocoon")を書いたことが関係しているのかなと思います。

cocoon公式GitHub

murata0705 commented 2 years ago

ああ、手元で動かしてみたらおっしゃっている意味がわかりました。

<%= javascript_pack_tag 'calendar/event', 'data-turbolinks-track': 'reload' %>

ここで、event.js だけ application に読み込まずに pack_tag で読んでるので、cocoon のモジュールを参照できなそうですね。

なので、飼育記録ページでも javascript_pack_tag で js を呼ぶようにして、event.js と cocoon をそれぞれ import するか、リスナーを設定する #activity-logs-containers みたいなタグを作って、カレンダーページではこのタグ内に ajax で受け取った HTML を append するとかですかねぇ。

あとは、カレンダー処理自体もcocoon.jsのようにグローバルに設定しちゃう方法もありますね。

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます! カレンダー画面の方と飼育記録画面の方それぞれでrequire("@nathanvda/cocoon")を呼び出すやり方を体現してみたく思い、

app/javascript/packs/application.jsrequire("@nathanvda/cocoon")をコメントアウトし、中身がrequire("@nathanvda/cocoon")のみのapp/javascript/packs/nathanvda_cocoon.jsというファイルを作成し、 app/views/logs/new.html.erbのほうで<%= javascript_pack_tag 'nathanvda_cocoon', 'data-turbolinks-track': 'reload' %>として呼び出す方法を試してみました。 以下コミットが具体的な変更箇所になります。 f458169

結果、カレンダーページの方では、セレクトフォームが二重になることも無く、コールバックも動いているので、大体期待通りになりました。以下動作になります。 Image from Gyazo

しかし、飼育記録ページの方では、コールバックが効かなくなりました。なぜこのような結果になるのか分かりませんが、以下動作になります。 Image from Gyazo

リスナーを設定する #activity-logs-containers みたいなタグを作って、カレンダーページではこのタグ内に ajax で受け取った HTML を append する

こちらのやり方は、具体的にどのようにするのか分かりませんでした。

カレンダー処理自体もcocoon.jsのようにグローバルに設定しちゃう方法

こちらはFullcalendarの実装方法としてズレてくるのと、他に影響が出そうなので今回試していません。 https://fullcalendar.io/docs/initialize-es6

murata0705 commented 2 years ago
<%= javascript_pack_tag 'nathanvda_cocoon', 'data-turbolinks-track': 'reload' %>

でやる場合、nathanvda_cocoon.js内に、cocoon.jsの内容を移さないと動かないですね。 cocoon.js内のコードから、require("@nathanvda/cocoon")が参照できないと思います。 (webpacker の仕様よくわかっていないので、もしかしたらなにか方法があるかもしれませんが)

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます! 下記コミットでnathanvda_cocoon.js内に、import "../src/cocoon"を追加する変更を加えました。 5044173

結果、飼育記録ページの方でもcocoonコールバックが動くようになり、期待通りの動きになりまりました。ありがとうございます!

カレンダーページの方は、application.jsに書いてあるimport "../src/cocoon"を参照してcocoonコールバックが動いているのかと思い、 飼育記録画面の方も同じくapplication.jsに書いてあるimport "../src/cocoon"を参照してcocoonコールバックが動くものかと思ったので、 なぜnathanvda_cocoon.js内に、import "../src/cocoon"を書く必要があるのか腑に落ちませんが。。

murata0705 commented 2 years ago

@shuhei-T HTML を見ると、

みたいな js があると思いますが、これはそれぞれ、webpack でビルドした js になります。(たぶん) 今 webpacker を使っていると思いますが、webpacker は webpack をラップしたもので、実際は webpack がビルドしています。

application-[hash].js をビルドするときに src/cocoon.js を読み込んできますが、ビルドするときにrequire("@nathanvda/cocoon")していたので、依存関係は問題ありません。

nathanvda_cocoon-[hash]-.js のビルドはまた application-[hash].js のビルドと別でビルドするので、その際にrequire("@nathanvda/cocoon")していないために依存しているライブラリがなく、うまくリスナーを設定できなかったのだと思います。(webpack と webpacker の仕様をちゃんと確認していないですが)

link_to_add_associationに関しては、ライブラリの中身を見てみないとわかりませんが、スコープの違いだと思います。

ちゃんと理解したい場合は webpack と webpacker の仕様を確認して、 cocoon の中身も見てみましょう。

shuhei-T commented 2 years ago

@murata0705 コメントありがとうございます! webpackやwebpackerについての理解も深めようと思います!

長々と対応して頂き本当にありがとうございました^^

murata0705 commented 2 years ago

一応まとめておきます。

一番最初のケース

カレンダーページの方は、application.jsに書いてあるimport "../src/cocoon"を参照してcocoonコールバックが動いているのかと思い、

../src/cocoon内のコールバック設定時に#active-logのタグが存在しないのでコールバックを設定できず

飼育記録画面の方も同じくapplication.jsに書いてあるimport "../src/cocoon"を参照してcocoonコールバックが動くものかと思ったので、

#active-logのタグが存在するので、コールバックを設定できる

event.js 内に cocoon.js をコピーし、event.js 内でも require するケース

require もしているので event-[hash].js をビルドするときに問題なくコールバックも設定できる。ただし、二度 require しているので、グローバルにアクセスできるような処理が二重にセットされてしまう。link_to_add_association など。

nathanvda_cocoon.js 内で require のみ、cocoon.js 内ではコールバック実装を行い、pack_tag で nathanvda_cocoon を利用するケース

cocoon.js を import して、application-[hash].js をビルドするときに、require していないので、cocoon.js のコールバックのカスタムイベントを参照できず、コールバックが設定できない。