LoginSignup
1
0

Javascriptを使用してフォームを何個も追加

Last updated at Posted at 2024-06-20

はじめに

ポートフォリオ制作中です!
書こう書こうとずっと思ってはいたのですが、なかなか手がつけられず…
忘れないうちに書き始めようと決心しました…!

間違えているところ等ございましたら、ご指摘いただけますと幸いです🙇

今回はJavascriptでフォームを何個でも追加できるような実装をアウトプットします✍️
accepts_nested_attributes_forを使用して子要素を親要素で保存や変更できるようにしています!
(後々調べると、accepts_nested_attributes_forは使用しない方がいいかもという記事をちらほら見かけますが、ポートフォリオも終盤で今更変えられないのでこのままでいきます!)

完成図

旅行やデートのプランを共有するSNSサイト「Go out Planning」を絶賛作成中です
プラン概要の中に何個でもスケジュールフォームを追加できるようにするため、
今回は動的フォーム追加を実装します!
Gemの利用はしていません!
cocoonというGemがあるようなので、
気になる方は調べてください!(最初の頃はGemの存在を知らんかったんや…!)

スクリーンショット 2024-06-20 0.30.23.png

ER図

Plan:PlanDetailで1:Nの関係です
Active Record Nested Attributesを使用して、
Planのnew画面でPlanDetailも合わせて保存できるようにしています

Active Record Nested Attributesとは

RailsのActive Recordライブラリにおける機能の一つです
親子関係にあるオブジェクトの属性を一緒に保存、更新するためのもの
フォームから親オブジェクトとその子オブジェクトを同時に、保存変更することができます

事前準備

注意⚠️
こちらのポートフォリオはbootstrapを導入しているため、
jqueryをインストール済みです!
Javascriptでjqueryを使用しているため、もしこの記事を参考にされる方がいらっしゃいましたら、インストールしてから実装をするようにお願いいたします🙇

jqueryについてはこちらの記事がわかりやすかったです!

実装

modelに記述

アソシエーションを組み、ActiveRecordNestedAttributesを使用できるよう記述

親要素(Planモデル)

plan.rb
class Plan < ApplicationRecord

  # アソシエーション
  belongs_to :user
  has_many :plan_details, dependent: :destroy
  :
  
  # 子モデル(plan_details)の属性を受入れ、更新や削除を許可する
  accepts_nested_attributes_for :plan_details, allow_destroy: true
  :
end

親要素にはaccepts_nested_attributes_forを記述し、子要素の属性を受け入れます

子要素(PlanDetailモデル)

plan_detail.rb
class PlanDetail < ApplicationRecord

  # アソシエーション
  belongs_to :plan

end

新規投稿編

controllerに記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def new
    @plan = Plan.new
    # PlanDetailsモデルのインスタンス作成
    @plan_detail = @plan.plan_details.build
  end
  
  def create
    @plan = current_user.plans.new(plan_params)
    @plan.save
    redirect_to plan_path(@plan)
  end
  
  private
  
  def plan_params
    params.require(:plan).permit(:title, :body, :is_draft, :description, plan_details_attributes: [:id, :title, :body, :_destroy, :address, :latitude, :longitude])
  end
end

@plan.plan_details.buildを記述することで、
ネストされたplan_detailのインスタンス変数を作成することができます

plan_paramsストロングパラメーターの指定
通常はplanのカラムのみ許可しますが、今回はplan_detailのカラムも一緒に許可します!

:description, plan_details_attributes:の後に、配列でplan_detailのカラムを記載します

:_destroy
のちに投稿済みをedit画面で編集する際、子要素を削除して更新ボタンを押した場合に
子要素を削除するために:_destroyの記述が必要です

view(new)作成

レイアウトの部分や関係のない機能は省略しています!

new.html.erb
<%= form_with model: plan do |f| %>

  <h4>プラン概要</h4>
  <%= f.text_field :title %>
  <%= f.text_area :body %>

  <h4>スケジュール</h4>
  <%= f.fields_for :plan_details do |plan_detail| %>
    <%= plan_detail.text_field :title %>
    <%= plan_detail.text_area :body %>
    <%= plan_detail.text_field :address %>
  <% end %>

  <div id="plan_details_wrapper"></div>
    <%= link_to '+', '#', id: 'add_plan_details' %>
  </div>
  
  <%= f.submit '投稿' %>
<% end %>

planのform_withの中にplan_detailのf.fields_forが入っています

<%= link_to '+', '#', id: 'add_plan_details' %>
この+ボタンをクリックすることにより<div id="plan_details_wrapper"></div>
新しいplan_detailのフォームが追加されることになります(Javascriptで)
また、Javascriptでフォームを追加するため、リンクをクリックしたときどこにも飛ばないようにするため'#'の記述があります

Javascript

application.js
// plans/showでplan_detailsの入力フォームを追加するための記述
$(document).ready(function() {
  var wrapper = '#plan_details_wrapper';
  var addButton = '#add_plan_details';
  var x = 1;

  $(document).on("click", addButton, function(e) {
    e.preventDefault();
    x++;

    var formHtml = `
    <div class="row nested-fields">
      <div class="col-10">
        <input type="text" class="form-control mt-4" name="plan[plan_details_attributes][${x}][title]" placeholder="詳細タイトル">
      </div>
      <div class="col-2 d-flex align-items-center">
        <a href="#" class="remove_field btn btn-danger my-3">
          <i class="fa-solid fa-trash" style="color: #ffffff;"></i>
        </a>
      </div>
      <div class="col-12">
        <textarea class="form-control mt-3" name="plan[plan_details_attributes][${x}][body]" placeholder="詳細説明"></textarea>
        <input type="text" class="form-control my-3" name="plan[plan_details_attributes][${x}][address]" placeholder="住所">
      </div>
      <input type="hidden" name="plan[plan_details_attributes][${x}][_destroy]" class="destroy-field" value="false">
    </div>
    `;

    $(wrapper).append(formHtml);
  });
  
// plans/showでplan_detailsの入力フォームを削除するための記述
  $(document).on("click", ".remove_field", function(e) {
    e.preventDefault();

詳しく解説していきます

application.js
// plans/showでplan_detailsの入力フォームを追加するための記述
$(document).ready(function() {
  var wrapper = '#plan_details_wrapper';
  var addButton = '#add_plan_details';
  var x = 1;

(document).ready(function()
ドキュメントを読み込んだら始めるよ!という意味です

var wrapper = '#plan_details_wrapper';
new.html.erbの<div id="plan_details_wrapper"></div>idの部分をwrapperに代入します

var addButton = '#add_plan_details';
new.html.erbの<%= link_to '+', '#', id: 'add_plan_details' %>idの部分をaddButtonに代入します

var x = 1;
xに1を代入します
追加するフォームの各入力フィールドを識別するために、一意のインデックスを付与します

application.js
  $(document).on("click", addButton, function(e) {
    e.preventDefault();
    x++;

$(document).on("click", addButton, function(e) {
先ほど代入したaddButtonをクリックしたときの動作を指定しています

e.preventDefault();
デフォルトの動作を防ぐ
addButtonをクリックした時に、フォームを送信したりページをリロードしたりすることを防いでいます

x++;
x + 1と同じ意味です

application.js
  var formHtml = `
    <div class="row nested-fields">
      <div class="col-10">
        <input type="text" class="form-control mt-4" name="plan[plan_details_attributes][${x}][title]" placeholder="詳細タイトル">
      </div>
      <div class="col-2 d-flex align-items-center">
        <a href="#" class="remove_field btn btn-danger my-3">
          <i class="fa-solid fa-trash" style="color: #ffffff;"></i>
        </a>
      </div>
      <div class="col-12">
        <textarea class="form-control mt-3" name="plan[plan_details_attributes][${x}][body]" placeholder="詳細説明"></textarea>
        <input type="text" class="form-control my-3" name="plan[plan_details_attributes][${x}][address]" placeholder="住所">
      </div>
      <input type="hidden" name="plan[plan_details_attributes][${x}][_destroy]" class="destroy-field" value="false">
    </div>
  `;

    $(wrapper).append(formHtml);
  });

var formHtml =以下略
formHtmlに追加するフォームの記述を代入しています
記述方法はコメントで教えていただいたテンプレートリテラルで記述しました!
バッククォート(`)で囲むことで複数行の文字列をそのまま記述することが可能になります!
こちらの記事を参照しました↓

$(wrapper).append(formHtml);
最初に代入したwrapperの箇所にformHtmlを追加(append)します

application.js
// plans/showでplan_detailsの入力フォームを削除するための記述
  $(document).on("click", ".remove_field", function(e) {
    e.preventDefault();

追加フォームにのみある削除ボタンをクリックすると、追加したフォームを削除することができます

e.preventDefault();
先ほどと同じように、remove_fieldをクリックした時に、フォームを送信したりページをリロードしたりすることを防いでいます

以上で、新規登録編は終了です。
続いて更新編に移ります!

投稿更新編

controllerに記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def edit
    @plan = Plan.find(params[:id])
  end

  def update
    @plan = Plan.find(params[:id])
    @plan.update(plan_params)
    redirect_to plan_path(@plan)
  end
  
  private

  def plan_params
    params.require(:plan).permit(:title, :body, :is_draft, :description, plan_details_attributes: [:id, :title, :body, :_destroy, :address, :latitude, :longitude])
  end

end

ここは至ってシンプルなため説明は割愛させていただきます!

view(edit)作成

レイアウトの部分や関係のない機能は省略しています!

edit.html.erb
<%= form_with model: @plan, url: plan_path(@plan), method: :patch do |f| %>

  <h4>プラン概要</h4>
  <%= f.text_field :title, data: { plan_id: @plan.id } %>
   <%= f.text_area :body %>

  <h4>スケジュール</h4>
  <%= f.fields_for :plan_details do |plan_detail| %>
    <%= plan_detail.text_field :title, data: { plan_detail_id: plan_detail.object.id } %>

    <%= link_to "削除", "#", class: "remove_field" %>

    <%= plan_detail.text_area :body %>
    <%= plan_detail.text_field :address %>

    <%= plan_detail.hidden_field :_destroy, class: 'destroy-field' %>
  <% end %>

  <div id="plan_details_wrapper"></div>
    <%= link_to '+', '#', id: 'add_plan_details' %>

 <%= f.submit "投稿する", class: "btn btn-primary mx-auto" %>
<% end %>

新規投稿と違う部分を解説🌱

<%= link_to "削除", "#", class: "remove_field" %>
すでに投稿している子要素を削除できないと困るので削除ボタンを作っています

<%= plan_detail.hidden_field :_destroy, class: 'destroy-field' %>
隠しフォームで_destroyを送信するフォームを作成しています
変更前にあったはずの子要素を削除して親要素を変更するとエラーが起きてしまうため、
子要素を削除した場合、そのエラーは無視していいですよーとrailsに教えてあげるためにあります

こちらの記事を参考にさせていただきました!

Javascript

application.js
// plans/editでplan_detailsの入力フォームを削除するための記述
   if(confirm('この詳細を削除してもよろしいですか?')) {
     var removeButton = $(e.target);
     var destroyField = removeButton.closest('.nested-fields').find('.destroy-field');
     if (destroyField.length > 0) {
       destroyField.val('true');
       removeButton.closest('.nested-fields').hide();
       console.log('Status: success - The item was marked for deletion.');
     } else {
       removeButton.closest('.nested-fields').remove();
       console.log('Status: success - The new item was removed.');
     }
   } else {
     console.log('Status: cancelled - The item was not deleted.');
   }
 });
});

詳しく解説していきます

application.js
// plans/editでplan_detailsの入力フォームを削除するための記述
   if(confirm('この詳細を削除してもよろしいですか?')) {
     var removeButton = $(e.target);
     var destroyField = removeButton.closest('.nested-fields').find('.destroy-field');

if(confirm('この詳細を削除してもよろしいですか?')) {
削除ボタンをクリックした後、上記の確認ダイアログを表示し、OKをクリックした場合のみ{}の中の処理が実行

スクリーンショット 2024-06-20 2.14.57.png

var removeButton = $(e.target);
クリックされた削除ボタン自身(e.target)removeButtonに代入

var destroyField = removeButton.closest('.nested-fields').find('.destroy-field');

先ほどedit画面で確認した、<%= plan_detail.hidden_field :_destroy, class: 'destroy-field' %>を探してきてdestroyFieldに代入

application.js
if (destroyField.length > 0) {
  destroyField.val('true');
  removeButton.closest('.nested-fields').hide();
  console.log('Status: success - The item was marked for deletion.');

if (destroyField.length > 0) {
destroyField要素が存在するか確認

destroyField.val('true');
destroyFieldの値を 'true' に設定

removeButton.closest('.nested-fields').hide();
nested-fields 要素を非表示にする

console.log
削除が成功したことをコンソールに出力します↓

スクリーンショット 2024-06-20 2.27.40.png

application.js
     } else {
       removeButton.closest('.nested-fields').remove();
       console.log('Status: success - The new item was removed.');
     }

removeButton.closest('.nested-fields').remove();
destroyField が存在しない場合、edit画面で新しく追加されたフィールドであるため、
.nested-fields 要素を完全に削除します

console.log
削除が成功したことをコンソールに出力します

application.js
   } else {
     console.log('Status: cancelled - The item was not deleted.');
   }
 });
});

ユーザーが削除の確認ダイアログで「キャンセル」をクリックした場合、
削除は実行されず、コンソールに削除がキャンセルされたことを示すメッセージを出力します

投稿削除編

controllerに記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def destroy
    @plan = Plan.find(params[:id])
    @plan.destroy
    redirect_to mypage_path
  end
  
end

親要素が削除されれば、子要素は一緒に削除されます!

さいごに

コメントにてご指摘いただき、2件修正・追記しています。
ご指摘いただきありがとうございました!
また、誤り等ございましたら教えていただけますと幸いです🙇

参照記事

1
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0