🔖

付けられるタグの個数を制限する(Ruby on Rails)計画⇒実証

2023/08/11に公開

やりたいこと

PFを作成した際にタグ機能を付けたのですが、作りがチェックボックスを応用した作りになっており、すでに準備された既存のタグから選択する使用になっています。
現状は付けられる個数に制限をかけていないため、準備している個数分のタグをつけられてしまう状態です。

実装目標:タグは3個までしかつけられないようにする。

コントローラー、モデル、ビュー


タグに関するデータはSentimentモデルに入っています。

ビュー画面の記述です。Ruby on Railsのチェックボックスを使い実装しています。

デザインを通常のチェックボックスから変更したため、jQueryも使用しました。cssもあります。

投稿で使用しているdiarysコントローラーのcreateアクションを使用して、チェックボックスを動作させています。Sentimentsコントローラーは使用していません。

計画(なぜ、どのように実装するか)

  1. モデルにバリテーションを記述する事で個数制限をする
  2. コントローラーに制限を記述する
  3. jsファイルに制限を記述し、3個以上は同時選択できないようにする

【1・2】モデルでのバリデーションにより、データベース内の整合性が確保され、コントローラーでのパラメーター制限によってセキュリティを強化し、クライアント側での制御によってユーザーにフィードバックを提供することができます。
【3】フロントエンドでのUI制御。
JavaScriptを使用して、クライアント側での選択制限を実装し、ユーザーにフィードバックを提供します。セキュリティ向上とユーザーエクスペリエンス向上のための補完的な手段という認識です。

バックエンドとフロントエンドの両方で選択個数を制限することができます。データの整合性を保ちつつ、セキュリティを向上させ、ユーザーにも使いやすいUIを提供することを目指します。

うまくいかなかった例

  • モデルにバリテーションを記述した


これでブラウザ上で試しましたが、3個以上付けれてしまいバリテーションがききませんでした。
おそらくSentiment.countはSentiment モデルのレコードの総数を返すので、既存のタグが3個以上存在する現在の状況では、バリテーションが動作しなかったと思います。
現在のコードでは、Sentiment.count が常に max_tags 以上になるため、バリデーションが常に失敗している。
やってしまいました😵

※ほかにも理由はあるかもしれません。初学者の見解ですので、ご参考までに。

1番(model)のバリテーションを実装

class Diary < ApplicationRecord


  enum is_draft: { posted: false, draft: true }
  belongs_to :customer
  has_many :comments
  has_many :favorites
  has_many :favorited_customers, through: :favorites, source: :customer

  # ...
  has_many :diary_sentiments, dependent: :destroy
  has_many :sentiments, through: :diary_sentiments, dependent: :destroy
  # ...

// チェックできる個数の制限
  validate :validate_sentiment_count
  validates :title,presence:true,length:{maximum:50}
  validates :body,presence:true,length:{maximum:100}

  def favorited_by?(customer)
    favorites.exists?(customer_id: customer.id)
  end


#view画面上も、非公開になっている投稿を表示しないようにするときに使う予定
  # def posted?
    # status == 'posted'
  # end

  # 投稿検索機能(ワード検索)
  def self.search_for(search_word, method)
    if method == "parfect"
      @diary = Diary.where("title LIKE?","#{search_word}")
    elsif method == "forward"
      @diary = Diary.where("title LIKE?","#{search_word}%")
    elsif method == "backward"
      @diary = Diery.where("title LIKE?","%#{search_word}")
    elsif method == "partial"
      @diary = Diary.where("title LIKE?","%#{search_word}%")
    else
      @diary = Diary.all
    end
  end


// 個数の制限
  private

  def validate_sentiment_count
    max_tags = 3
    if sentiment_ids.count > max_tags
      errors.add(:base, "選択できる個数は#{max_tags}個までです")
    end
  end
end

今回はこのような記述に変更しました。
sentiment_idsは関連するモデル(テーブル)のIDを保存する配列です。sentiment_ids.countにすることで、現在Diary モデルが Sentiment モデルと多対多の関連を持っている状態であるため、Diary モデルのインスタンスの sentiment_ids 配列には関連する Sentiment レコードのID(選択されたチェックボックスの数)を取得します。

2番(controller)パラメーター制限による実装


diaryparamsの中に、sentiment_ids:[]が含まれているのでこのまま実装します。

補足:複数のチェックボックスから選択された値をフォームから送信する場合、それらの値を配列として受け取るために [] を使用します。これにより、同じ名前のチェックボックスから選択された値が複数存在する場合でも、正しくそれらの値を受け取ることができます。

3番(js)でフロントエンドでのUI制御をを実装

現在のjsファイルの記述です。
ここに同時選択できる個数の制限について記述していきます。

document.addEventListener("turbolinks:load", function() {
  // Turbolinksがページの読み込みを完了したときに実行されるコード

  var sentiment_checkboxes = document.querySelectorAll('.sentiment_checkbox');
  // クラス名が "sentiment_checkbox" の要素を取得

  sentiment_checkboxes.forEach(function(element) {
    // 取得した要素それぞれに対して以下の処理を行う

    element.addEventListener('click', function() {
      // 要素がクリックされたときの処理を定義

      var siblingElements = this.parentNode.children;
      // 要素の親要素内の子要素すべてを取得

      for (var i = 0; i < siblingElements.length; i++) {
        // 兄弟要素すべてに対して以下の処理を繰り返し

        if (siblingElements[i] !== this && siblingElements[i].classList.contains('form-check-text')) {
          // クリックされた要素以外で、クラス名が "form-check-text" のものを見つけた場合

          siblingElements[i].classList.toggle('clicked');
          // 見つけた要素の "clicked" クラスを切り替える(追加するか削除するか)
        }
      }
    });
  });
});

こう記述してみました。

document.addEventListener("turbolinks:load", function(){
  var maxSelection = 3; // 最大選択個数を設定
  var sentiment_checkboxes = document.querySelectorAll('.sentiment_checkbox');

  sentiment_checkboxes.forEach(function(element) {
    element.addEventListener('click', function() {
      var selectedCount = 0;

      sentiment_checkboxes.forEach(function(checkbox) {
        if (checkbox.checked) {
          selectedCount++;
        }
      });

      // 選択個数が制限を超えた場合、選択を無効化
      if (selectedCount > maxSelection) {
        this.checked = !this.checked; // チェックを戻す
        return; // クリックを無効化して終了
      }

      var siblingElements = this.parentNode.children;
      for (var i = 0; i < siblingElements.length; i++) {
        if (siblingElements[i] !== this && siblingElements[i].classList.contains('form-check-text')) {
          siblingElements[i].classList.toggle('clicked');
        }
      }
    });
  });
});
 // 選択個数が制限を超えた場合、選択を無効化
      if (selectedCount > maxSelection) {
        this.checked = !this.checked; // チェックを戻す
        return; // クリックを無効化して終了
      }

この記述がポイントで、1度目に試した記述では3個以上つけられてしまいましたが、この記述を追加したことで、画面上も3個以上はチェックボックスがつかない状態にできました。

実装後所感

これで、モデル・コントローラー・viewでのタグ付の個数制限が実装できました。
個人的には、jsファイルの記述がまだまだ、調べながらでしかコーディングできないことなどが課題だと感じます。練習あるのみですね。

参考にしたサイト

https://qiita.com/Uchikoba/items/fc716669d43bf1a15fa9

Discussion