inuinu blog(開発用)

BOT @wagagun の開発ノウハウや、IT向け?の雑記ブログです。

【Ruby on Rails7】cocoonを導入(その9:最終行からコピーしたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は行追加時に、最終行から値をコピーする手法を考えます。

前回の続き。

inuinu-tech.hatenablog.jp

今回は、失敗をもとに、ビューで細工するのをやめ、JQueryでコードを実装していきます。

HTMLの中身を調査し、仕様を決める

INPUTタグののidを覗いてみる

INPUTタグを覗いてみたところ、name属性には[]が入っており、取り扱うには厳しい気がします。
idはキャメルケースのようですので、こちらを活用したほうが良さそうです。

idを見てみると、ビューで描画された行は0から始まりますが、行追加ボタンで追加された行は、13桁程度の数字がセットされていますね。
なので、配列等にセットする際、添字としては使用できないでしょうね。

どの行からコピーするか?

cocoonが決めたJQueryのエントリーポイントに実装することになるのですが、ビューのみの場合と比較して、多少の自由度は増します。

今回は、行追加リンクを押下する直前時点での最終行の値を、新たに追加する行にコピーするようにします。

別ボタンを用意するか?

行追加ボタンの他に、行コピー(追加&複写)といったボタンを別途用意した場合、JQueryのロジックを2つ用意する必要がありそうですね。ちょっと冗長だと思います。

ですので、ボタンの横に「□行コピー」といったチェックボックスを用意し、チェックされたらコピーの処理を追加します。

コピーロジックを実装する

明細行計算のロジックを改良する

inuinu-tech.hatenablog.jp

で、紹介した行計算ロジックを改良あるいはコピーします。
(どちらでも構いません。)

一例をあげます。
cocoon:after-insertにペーストするか、適当なファンクションにして、cocoon:after-insertから呼び出します。

let obj = document.querySelectorAll('#codedetails .nested-fields');
let flds_id = []; // 多次元配列用
obj.forEach(function(field, idx) {
  if (field.style.display !== 'none') {
      let work_id = [];
      let input_obj = field.querySelectorAll("input");
      input_obj.forEach(function(field2, idx2) {
        if (field2.id.indexOf('_destroy') == -1) {
         work_id.push(field2.id);
        }
      })
      flds_id.push(work_id);
  }
})
return flds_id;

flds_idは2次元配列(行、列)になります。
(Rubyと同様)Javascriptは初っ端で2次元配列を定義できないので、work_idを別途用意し、列の配列をflds_idにプッシュしています(flds_id[idx]に直接プッシュしていっても構いません)。
行削除ボタン自体もINPUTタグですので、_destroyが含まれるINPUTは除外します。

上記例はINPUTですが、INPUTの中でもvalueで値がコピーできないものもあります。そういった項目が行に含まれる場合は、type属性で除外することになります。
また、TEXTAREAやSELECTタグもvalueで行けそうですので、こちらに含めてもいいかもしれません。
要は、valueで複写できる、できないで、配列を別にし、それぞれのコピーロジックを作成を別途作成していきます。

コピーしたくない項目があった場合

例えば、ユニークキーとなるような項目をコピーした場合、重複チェックで引っかかるので、そこは空欄にしたい…のであれば、

if (field2.id.indexOf('_destroy') == -1) {

の箇所を工夫すれば良いと思います。

コピーロジックを実装する

次にコピーロジックを実装するのですが、先程の配列ごと(valueで複写できるもの、できないもの)にロジックを作っていきます。

その前に、配列の何番目から、どこにコピーするかを整理します。
もうすでにコピー先の行は作成されているので…

  • コピー元の添字:配列の長さ − 2
  • コピー先の添字:配列の長さ − 1

となるはずです。
以下はコピーロジック例です。cocoon:after-insertに挿入するか、適当なファンクションにして、cocoon:after-insertから呼び出します。
(前提条件:INPUTタグ等でvalueコピーが可能なもの、かつ、行コピーにチェックが入っていて、行が2行以上存在していること。)

let copy_moto = flds_id.length - 2;
let copy_saki = flds_id.length - 1;

// 各行のinputタグ(削除するボタンを除く)でループ
for (let idx=0; idx<flds_id[0].length; idx++) {
  document.getElementById(flds_id[copy_saki][idx]).value = document.getElementById(flds_id[copy_moto][idx]).value;
}

こちらで、最終行(実行段階では1行前の行)からのコピーが可能になります。

次回

次回は、最終行ではなく、任意の行をコピーしたい場合を考えてみます。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その8:行コピーしたい(失敗編))

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は行追加時に、値をコピーする手法を考えます。
(本記事は失敗編です。)

前回の続き。

inuinu-tech.hatenablog.jp

今回は、子(明細)行の追加をする際、内容を複写して表示できるようにします。
今回は特に、明細行単位にコピーボタンが用意できないか考えます。

今回はcocoonの機能外の実装になります。
正式サポートされない実装は、避けたほうが良いかと思います。

行追加時に値をコピーすることは、機能として備わっているか?

調べてみましたが、答えはノーかと思います。
正式にサポートされているのであれば、チュートリアル系の記事にあると思いますので。

裏技的なものは、見たことがあるが…

リンクはすべて質問サイトより

stackoverflow.com

stackoverflow.com

stackoverflow.com

上記のサイトでは、明細単位に行コピーボタンを付ける手法が載っているようですが、ただ「無限ループになる」とも書いていますね。

裏技を試してみる

まずは、親側のフォームに(質問サイトの裏技的な)parent_f: fを追加します。

●親のフォーム

render 'actuation_setup_fields', f: act_setup_builder, parent_f: f

実行するとparent_f: fの箇所がエラーとなるようです。
もともとサポートされてないからなのでしょうが、なぜこういったものを紹介するのか、理解に苦しみます。
とりあえず、別行を追加します。

●親のフォーム

    - @parent_f = f

とりあえず、これで、親のfが子で参照できるようになりそうです。
さらに子のフォームにコピー用のボタンを追加します。

●子のフォーム

      - code_to_clone = f.object
      = link_to_add_association 'clone', @parent_f, :子のモデル名複数形 , wrap_object: Proc.new { |new_code| new_code.フィールド名 = code_to_clone.フィールド名; new_code }"

これで試しましたが、描画段階で無限ループになりますね。

ですので、この手法は、却下かなと。

ここまでのまとめ

link_to_add_associationは親側に置くことを前提にしており、子側に置くことを想定していないのでは?と思います。

今後、cocoonにclone機能が追加されることでもない限り、この手法は避けたほうが良さそうです。

input/textareaのコピーはvalueのコピーなので比較的楽ですが、選択肢のある入力タグや属性は、難易度が高い。ですので、公式でサポートしてくれると、とても助かりますね。

Proc.newの本来の利用方法

親側のフォームでProc.newを書いておけば、追加時の初期表示などをすることはできます。

●親のフォーム

      = link_to_add_association 'add', f, :子のモデル名複数形, wrap_object: Proc.new { |d| d.フィールド名 = 'hoge'; d }

行追加時に、任意のフォールド名に「hoge」が初期表示されると思います。

次回

親側のフォームにて、行コピーを実現するには?を考えてみたいと思います。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その7:子の件数にJQueryで制限をかけたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は行追加時に、子の件数について、JQueryで制限をかける手法を考えます。

前回の続き。

inuinu-tech.hatenablog.jp

前回はRailsの基本機能で子の入力件数に規制をかけましたが、エラー表示が更新ボタン押下時なので、タイミングが遅く、明細行追加時に制御したいという要求が出た際の解決策を考えていきます。

モデル制御時の不満点

Rails標準機能によるモデル制御の場合でも、結果的に明細件数を制御できますので、問題でも障害でも不具合でもなく「不満」という表現にいたしました。

モデル制御でのメリットとデメリットをご参照いただき、将来的なリスクよりも現状の不満のほうが優先されると判断された場合は、この記事を採用されることをおすすめいたします。

Javascript制御では何ができるか?

エラーチェック時での制御ではなく、ボタン押下時での判断ができる

前回の記事のデメリットでも書きましたが、Javascriptでは、きめ細かい制御が可能です。

Javascriptでの解決方法

Javascriptでの解決方法その1:明細追加ボタン押下時にアラートを出すことは可能か?

以前の記事、

inuinu-tech.hatenablog.jp

より、JQueryのロジックを抜粋してみます。

$('#tasks').on('cocoon:before-insert', function() {
  console.log("cocoon:before-insert!");
})
.on('cocoon:after-insert', function() {
  console.log("cocoon:after-insert!");
})
.on('cocoon:before-remove', function() {
  console.log("cocoon:before-remove!");
})
.on('cocoon:after-remove', function() {
  console.log("cocoon:after-remove!");
});

上記のcocoon:before-insertで超過しそうなときにalert()を出すことができれば良いのですが、cocoon:before-insertは単なる前の処理なので、alert()を出したとしても、行は追加されます。

こちら
https://github.com/nathanvda/cocoon/issues/113
を読んだ感じでは、return false;による抑制は却下されちゃったみたいですね。

ですので、他のアイディア(githubにある、ボタン側の表示抑制)を考えてみます。

Javascriptでの解決方法その2:明細追加ボタン押下時にボタン自身を消す

cocoon:after-insertで行のMAX値(例えば5件)に達した場合、明細追加ボタンを非表示にすることはできそうです。

その場合、cocoon:after-removeで5件を割った場合、明細追加ボタンを復活させます。

$('#tasks').on('cocoon:before-insert', function() {
})
.on('cocoon:after-insert', function() {
  let cnt = 子件数カウント処理
  if (cnt >= 5) {
    document.querySelector('ボタン周りのidまたはclass').style.display = 'none';
  }
})
.on('cocoon:before-remove', function() {
})
.on('cocoon:after-remove', function() {
  let cnt = 子件数カウント処理
  if (cnt <= 5) {
    document.querySelector('ボタン周りのidまたはclass').style.display = 'block';
  }
});

OAOO的には「子件数カウント処理」が共通化できそうですので、function化しましょう。

子件数カウントfunctionを実装する

そのまま件数をカウントすると、削除済み件数も加算してしまう

前回までに使用した例題をサーバーから実行し、F12を押下し「#codedetails .nested-fields」で検索すると、件数がわかります。

let cnt = document.querySelectorAll('#codedetails .nested-fields').length;

しかし、件数が予想よりも多いことがわかります。
削除ボタンで削除した行もカウントされています。

削除した子が残るケースは、あくまでもこの画面で削除した場合です。
更新の都度、削除した子は消えてなくなります。

削除済を除いてカウントしていく

JQueryなどを利用すればもっと簡単に件数がカウントできるのかもしれませんが、Javascriptでカウントしていきます。

let obj = document.querySelectorAll('#codedetails .nested-fields');
let cnt = 0;
obj.forEach(function(field, idx) {
  if (field.style.display !== 'none') {
    cnt++;
  }
})
return cnt;

querySelectorAll()で一発で取得できる方法もあるのかもしれませんが、あまり凝らないで求める方法はこんな感じでしょうか。

あとはfunction化して、cntをreturnすればよいかと思います。

次回

次回は、行コピーを考えたいと思います。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その6:子の件数にモデル定義のみで制限をかけたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は行追加時に、子の件数について、モデルの記述のみで制限をかける手法を考えます。

前回の続き。

inuinu-tech.hatenablog.jp

前回は子の0件を防ぎましたが、今回は子の件数に制限をかける方法を考えていきたいと思います。

やり方にはモデルで制御する方法と、Javascript(JQuery)側で制御をかける方法がありますが、今回はモデルで制御ををかけていきます。

モデルでの制御方法

モデルに実装する

モデルapp/models/project.rbに以下を追記します。

  # 子の上限
  MAX_TASK_LENGTH = 5
(中略)
  validates :tasks, length: {maximum: MAX_TASK_LENGTH}

これだけです。

これで、子の明細件数に制限をかけることができあます。
(上記例では6件以上入力するとエラーとなる。)

i18nに追記する(i18nをinstallしている前提)

バリデーションのメッセージには、「tasksは5文字までしか入力できません」となりますね。
これでは意味がわかりません。

config/locales/〜/ja.ymlに以下の行を追記します。
(すでにactiverecord:attributes:がある場合は、project:以降を追記してください。)

ja:
  activerecord:
    attributes:
      project: 
        tasks: 'タスク'
    errors:
      models:
        project:
          attributes:
            tasks:
              too_long: 'は%{count}件までしか登録できません'

これで「タスクは5件までしか登録できません」となります。

レイアウトによっては、「タスクは5行までしか登録できません」と「行」表現のほうが良いかもしれません。

モデル制御でのメリットとデメリット

モデル制御でのメリット

  • Railsの基本機能である
  • Javascriptよりも簡単に実装できる
  • Javascriptでの制御は、将来的に保証されない可能性もあるので、こちらのほうが確実
  • cocoon以外のパッケージに切り替えた場合でも対処できる

モデル制御でのデメリット

  • 「登録」ボタン押下時にチェックをするため、余計に入力するリスクは有る
  • エラーが発生した場合、削除ボタンを押して行を減らすしかない

メリット・デメリット、どちらを優先するか悩ましいところではある

この手法はRailsの基本機能であることから、メリット・デメリットを説明し、納得してもらうことになりますが、うるさいクライアントもいると思います。

個人的にはパッケージの機能を不完全だと言い、細かくいじりたがる日本のIT業界やクライアントは、だから海外に劣るのだと思っています。
(お金をかける部分はそこではないだろ…と思います。)

その場合のJavascript(JQuery)の手法については、次回にまわしたいと思います。

次回

次回は、それ以外の方法について考えていきます。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その5:子の0行入力はエラーとしたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は登録時に、子の件数が0件にならないようにする手法を考えます。

前回の続き。

inuinu-tech.hatenablog.jp

今回は、子が0件でも更新されてしまうことを防ぐ方法を考えます。

子が0件でもエラーにならない

多くの紹介記事のとおりに導入すると子が0件でも通ってしまう

掲題のとおりですが、なぜエラーにならないのでしょうか?

モデルapp/models/project.rbを覗いてみます。

●app/models/project.rb

class Project < ApplicationRecord
  has_many :tasks, dependent: :destroy
  accepts_nested_attributes_for :tasks, reject_if: :all_blank, allow_destroy: true
end

何が問題なのでしょうか?

解決方法(モデル側の制御)

一般的なエラーロジックを追加しても解決はしない

すべての項目がブランクの場合はエラーにするといったバリデーションを入れても上手くは行かないです。

●app/models/project.rb

  # 子のバリデーション
  validate :check_tasks

  private

  # 子供側のバリデーション
  def check_tasks
    tasks.each_with_index do |task, idx|
      if idx == 0
        if task.description.blank? && task.他の入力項目.blnak?
          errors.add(:base, '1行目は入力してください。')
        end
      end
  end

このロジックは除去します。

バリデーションの前にすでに行が除去されていることに気づく

他の障害が発生した際に気が付きましたが、未入力行はバリデーションの前に除去されていることに気が付きました。

ということは、それ以前の定義の問題でしょうか?

reject_if: :all_blankを除去すれば0件出力は防げる

モデル内にあるreject_if: :all_blankの記述を除去すれば、バリデーション前の未入力行の除去が防げます。

●app/models/project.rb

class Project < ApplicationRecord
  has_many :tasks, dependent: :destroy
  accepts_nested_attributes_for :tasks, allow_destroy: true
end

このロジックは除去します。

あとは、このモデル側でvalidates :カラム名, presence: trueを追加してあげれば、良いでしょう。

解決方法(追記1:コントロール側の制御)

新規時に1行目(空白行)を表示させたい

新規時に1行目(空白行)を表示させたいは以下のようにします。

●app/models/project.rb

  # GET /projects/new
  def new
    @project = Project.new
    # 子モデルも作成
    @project.tasks.build
  end

@project.tasks.buildを挿入してあげれば、1行空行が表示されます。

解決方法(追記2:ビュー側の制御)

1行目のみ削除ボタンを表示させない

全行に削除ボタンを表示させてしまうと、(0件エラーは表示できても)なんかちょっと…と言われる可能性はあります。

この場合、1行目のみ削除ボタンを表示させないという手法がありそうです。

●app/views/projects/_task_fields.html.haml

    - if f.options[:child_index] != 0
      .field
        %br
        = link_to_remove_association "明細削除", f

if f.options[:child_index] != 0:child_indexが行番(0〜)なので、0を除いて削除ボタンを表示させるようにします。

次回

次回はこのモデルの件数に制限をかける手法を考えます。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その4:変更時に子が二重登録されてしまう場合の解決方法)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は変更時に、子のレコードがダブってしまう現象についての解決方法を考えてきます。

前回の続き。

inuinu-tech.hatenablog.jp

今回は、導入した際に引っかかった障害と、その解決方法です。

変更時の二重登録について

cocoonを導入し、変更すると子が削除されずに残る(二重登録)

とあるチュートリアルを見つつ、cocoonを導入し、モデル、ビュー、コントローラーの順に追記・変更し、実行したところ、追加・削除は上手く行ったのですが、変更すると、前に入力した子がそのまま残ってしまいました。

親子関係の子の更新について(一般論)

親子を一括入力する場合の、更新ロジックについて、一般的には、

  • 追加
    • 親子ともにinsert
  • 変更
    • 親はupdate
    • 子は親に関わるレコードを全てdeleteした後、insert
  • 削除
    • 親子ともにdelete

ですが、なぜか変更のdeleteが上手く行かないようです。
検索してもヒットしない現象なので、私の初歩的なミスなのかもしれません。

updateには何も記載がない…

app/controllers/projects_controller.rbを見たところ…

●app/controllers/projects_controller.rb

  # PATCH/PUT /projects/1 or /projects/1.json
  def update

    respond_to do |format|
      if @project.update(project_params)
        format.html { redirect_to project_url(@project), notice: "Project was successfully updated." }
        format.json { render :show, status: :ok, location: @project }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end

updateに限らずcreateもdestroyも特に子に対する更新処理は定義されてないのですよね。
Railsは一般的な更新処理を自動でやってくれると思っているので、なんかちょっと変ですね…

正しい解決方法

もう一度コントローラーを見てみる

もう一度、app/controllers/projects_controller.rbを見てみます。

●app/controllers/projects_controller.rb

  private
  
  # Use callbacks to share common setup or constraints between actions.
  def set_project
    @project = Project.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def project_params
    params.require(:project).permit(:name, :description, tasks_attributes: [:description, :done, :_destroy])
  end

もしかして、子の:idって必要?

●app/controllers/projects_controller.rb

  private
  
  # Use callbacks to share common setup or constraints between actions.
  def set_project
    @project = Project.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def project_params
    params.require(:project).permit(:name, :description, tasks_attributes: [:id, :description, :done, :_destroy])
  end

子の方に:idを追記して、変更したところ、二重登録がなくなりました。

めでたしめでたし。

参考:間違った解決方法

参考までに、試行錯誤した段階でのソースも載せておきます。

間違った解決方法(その1)

●app/controllers/projects_controller.rb

  # PATCH/PUT /projects/1 or /projects/1.json
  def update
    @tasks = Task.where(project_id: params[:id])
    @tasks.each do |task|
      task.delete
    end

    #@tasks.destory_all #if @tasks.size == nil
    respond_to do |format|
      if @project.update(project_params)
        format.html { redirect_to project_url(@project), notice: "Project was successfully updated." }
        format.json { render :show, status: :ok, location: @project }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end

とりあえず、応急処置的に、コントローラーに削除処理を組み込みましたが、この手法ではバリデーションエラーが発生した時点で、子が削除されてしまうので、(エラーを直し更新完了せずに)処理を中断し他の画面に移った場合に、原状回復できないという問題が発生します

間違った解決方法(その2)

なので、上記のロジックは除去し、モデル側(app/models/project.rb)に以下のコードを追加しました。

●app/controllers/projects_controller.rb

before_validation :delete_tasks
(中略)
def delete_tasks
  # こちらは子供のレコードを一度削除する必要がある
  @tasks = task.where(project_id: self.id)
  @tasks.each do |task|
    task.delete
  end
end

この手法ですと、バリデーションエラーの場合に自動回復(rollback)しますので、子が消えてしまった…ということはなくなります。

とはいえ、「正しい解決方法」を実践すれば、記述する必要は全くありません。
あくまでも原因不明で困った困った…という場合の、代替案として頭の隅においてくださいね。

次回

次回は、子が0件でも更新できてしまう現象を解決します。

inuinu-tech.hatenablog.jp

【Ruby on Rails7】cocoonを導入(その3:バリデーション問題解決:Stimulusへ移動編)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回もバリデーションエラー時に発生する問題の解決策なのですが、もっと根本的に解決する手法を考えていきます。

前回の続き。

inuinu-tech.hatenablog.jp

前回は、すでにcocoonを導入し、Rails7へアップグレードした方向けの内容ですが、今回は、Rails7ではじめてcocoonを導入した方向けの内容になります。

前回のおさらい

一見して解決したように見えるが、取ってつけた感が強い

前回の手法は、すでにcocoonを導入し、Rails7へアップグレードしたのであれば、当座の障害解決として、これでゴールなのかもしれません。
ただ、作法的に、これで良いのか? ちょっとしっくりこない気分はあります。

Stimulusのconnect()内にcocoonのロジックを移動

Stimulusだと、どんな状況でも発火するのであれば、cocoonのロジックをまるごと移動させればいいのでは?

この考え方で上手く動作するのか?確認してみましょう。

各ファイルを変更・追加する

app/javascript/controllers/index.jsを書き換えます。

// app/javascript/controllers/index.js
//import CcReloadController from "controllers/ccreload_controller"
//application.register("ccreload", CcReloadController)
import TaskFormController from "controllers/task_form_controller"
application.register("task_form", TaskFormController)

前回のCcReloadControllerの2行はコメントにし、TaskFormControllerの2行を追加します。

次に、app/views/tasks/_form.html.hamltask_formに変更します。

.div{data: {controller: "task_form"}}

さらに、app/javascript/controllers/task_form_controller.jsを変更します。

import { Controller } from "@hotwired/stimulus"
//import jquery from "jquery"

//import * as common from './共通部品のJSファイル.js'

export default class extends Controller {

  connect() {
    // 共通変数の定義など

    // stimulusにはonloadの概念がなさそう
    //$(window).on("page:load", function(){
      // こちらにonload処理を記述
    //});

    $('子供のID').on('cocoon:before-insert', function() {
      console.log("cocoon:before-insert!");
    })
    .on('cocoon:after-insert', function() {
      console.log("cocoon:after-insert!");
    })
    .on('cocoon:before-remove', function() {
      console.log("cocoon:before-remove!");
    })
    .on('cocoon:after-remove', function() {
      console.log("cocoon:after-remove!");
    });

  } // connect()

} // export default class extends Controller

実行前に、いくつか解説します。
JQueryはimportしなくても大丈夫はなずです。
//import * as common from './共通部品のJSファイル.js'については、あとで解説します。
$(window).on(〜については、connect()時点ですでにロードされていると思いますので、不要です。

●app/javascript/application.js

// app/javascript/application.js
$(document).on('ready page:load turbolinks:load turbo:load', function() {
(以下略)

にあったready page:load turbolinks:load turbo:load的な行も不要になります。

app/javascript/application.jsのロジックは除去する

テストの邪魔になりますので、前々回に追加したapp/javascript/application.jscocoonのロジックは除去しておきましょう。

動作確認

サーバーを起動し、行追加・削除のボタンを押下し、ブラウザのコンソールに文言が表示されたら、動作確認は完了です!

Stimulusのメリットを再認識する

Stimulusを利用すると、自然と、ビュー単位でJavascriptのファイルを分割できるようになります。

ファイルの分割で悩んでいた方には福音かもしれません。

参考:共通箇所を別のファイルへ移動する

ファイルがビュー毎に分割してしまうと、共通的なfunctionがダブってしまうという副作用が生じると思います。
放っておくと、保守が大変そうですね。

こちらは、cocoonの機能でも、Stimulusの機能でもなく、VanillaなJavascriptの仕様ですが、export/importを使って、共通functionを別ファイルへ移動しましょう。

実装例

以下は、app/javascript/controllers/task_form_controller.jsでの実装例です。

import { Controller } from "@hotwired/stimulus"
//import jquery from "jquery"

import * as cmn from './common.js'

export default class extends Controller {

  connect() {
    // 共通変数の定義など

    // stimulusにはonloadの概念がなさそう
    //$(window).on("page:load", function(){
      // こちらにonload処理を記述
      const foo = cmn.hoge();
    //});

    $('子供のID').on('cocoon:before-insert', function() {
      console.log("cocoon:before-insert!");
    })
    .on('cocoon:after-insert', function() {
      console.log("cocoon:after-insert!");
    })
    .on('cocoon:before-remove', function() {
      console.log("cocoon:before-remove!");
    })
    .on('cocoon:after-remove', function() {
      console.log("cocoon:after-remove!");
    });

  } // connect()

} // export default class extends Controller

●app/javascript/controllers/common.js

export function hoge() {
  // 処理
  return 'abc';
} // function

上記例ではcommon.jsをimportし、common.jsの中にあるhoge() を実行しています。

これでかなりスッキリすると思います。

参考:JQueryではなく、Vanillaな書式に変更したい場合

JQueryはちょっとなあ…という方へ、参考までにVanillaなJavascript表記もアップしておきます。

●app/javascript/controllers/task_form_controller.js

import { Controller } from "@hotwired/stimulus"

//import * as cmn from './common.js'

export default class extends Controller {

  connect() {
    // 共通変数の定義など

    // stimulusにはonloadの概念がなさそう
    // こちらにonload処理を記述

    let id_obj = document.querySelector('#tasks'); // どちらか
    let id_obj = document.getElementById('tasks'); // お好みで

    // 追加処理前
    id_obj.addEventListener("cocoon:before-insert",function () {
      console.log("cocoon:before-insert!");
    }); // cocoon:before-insert

    // 追加処理後
    id_obj.addEventListener("cocoon:after-insert",function () {
      console.log("cocoon:after-insert!");
    }); // cocoon:after-insert

    // 削除処理前
    id_obj.addEventListener("cocoon:before-remove",function () {
      console.log("cocoon:before-remove!");
    }); // cocoon:before-remove

    // 削除処理後
    id_obj.addEventListener("cocoon:after-remove",function () {
      console.log("cocoon:after-remove!");
    }); // cocoon:after-remove

  } // connect()

} // export default class extends Controller

結果、ちゃんと動きました。

いくつかのブログを見ると、「addEventListenerではなく、data-actionというdatasetで定義して…」と書いてあったりして、できないのかな?と思いましたが、connect()にaddEventListenerは書けますね。

cocoonはおおもとの部分はJQueryをインストールしないとダメなのかもしれませんが、ユーザー定義の箇所についてはVanilla表記は可能なようです。

ただ、JQueryのようにメソッドチェーンで記述したら怒られました。
できないんですかね?

次回

次回は、cocoonを導入した際につまずきやすい箇所について、解決方法を考えていきます。

inuinu-tech.hatenablog.jp