1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】タグ付け機能について

Last updated at Posted at 2025-07-22

記事概要

Ruby on Railsにタグ付け機能を実装する方法について、まとめる

前提

  • Ruby on Railsでアプリケーションを作成している
  • 投稿機能を実装済みである

サンプルアプリ(GitHub)

タグ付け機能とは

Image from Gyazo

データベース設計

多対多のテーブル設計を行う

テーブル名 目的
postsテーブル 投稿を保存する
tagsテーブル タグを保存する
post_tag_relationsテーブル 中間テーブル

データの保存方法

複数のモデルに同時に保存する必要があるので、Formオブジェクトを利用する

Image from Gyazo

インクリメンタルサーチ

文字入力の都度、自動的に検索が行われる検索機能
逐次検索機能のこと

例)「ruby」というタグがすでにDBに存在するうえで、検索欄に「r」の文字が入力されたとする。このとき、「r」の文字と一致する「ruby」を検索結果の候補として、リアルタイムで画面上に表示する

手順1(Formオブジェクトを用いた投稿機能を実装する)

  1. app/models/post_form.rbを手動作成する
  2. post_form.rbを編集する
    app/models/post_form.rb
    class PostForm
      include ActiveModel::Model
    end
    
  3. PostFormクラスを使用してpostsテーブルに保存したいカラム名をすべて指定する
    app/models/post_form.rb
    class PostForm
      include ActiveModel::Model
    
      #PostFormクラスのオブジェクトがPostモデルの属性を扱えるようにする
      attr_accessor :text, :image
    end
    
  4. PostFormクラスに、Postモデルのバリデーションを設定する
    app/models/post_form.rb
    # 省略
    
    #PostFormクラスのオブジェクトがPostモデルの属性を扱えるようにする
    attr_accessor :text, :image
    
    # Postモデルのバリデーション
    with_options presence: true do
      validates :text
      validates :image
    end
    
    # 省略
    
  5. Postモデルに記載していたバリデーションを削除する
    app/models/post.rb
    class Post < ApplicationRecord
      has_one_attached :image
      #validates :text, presence: true
      #validates :image, presence: true
    end
    
  6. PostFormクラスにsaveメソッドを追記する
    app/models/post_form.rb
      # 省略
      
      # フォームから送られてきた情報をテーブルに保存する処理
      def save
        Post.create(text: text, image: image)
      end
    end
    
  7. newアクション、createアクションに記述しているインスタンス変数を、PostFormクラスのインスタンス変数@post_formに変更する
    app/controllers/posts_controller.rb
    # 省略
    
    def new
      @post_form = PostForm.new
    end
    
    def create
      @post_form = PostForm.new(post_params)
      if @post_form.valid?
        @post_form.save
        redirect_to root_path
      else
        render :new, status: :unprocessable_entity
      end
    end
    
    # 省略
    
  8. ストロングパラメーターのメソッド名をpost_form_params、requireメソッドのキーを:post_formに変更する
    app/controllers/posts_controller.rb
    # 省略
    
    def create
      @post_form = PostForm.new(post_form_params)
      if @post_form.valid?
        @post_form.save
        redirect_to root_path
      else
        render :new, status: :unprocessable_entity
      end
    end
    
    # 中略
    
    private
    def post_form_params
      params.require(:post_form).permit(:text, :image)
    end
    
    # 省略
    
  9. app/views/posts/new.html.erbを編集し、renderメソッドのlocalsオプションで変数を指定する
    <h3>新規投稿ページ</h3>
    <%#= render partial: "form" %>
    <%= render partial: 'form', locals: {url: posts_path, method: :post} %>
    
  10. app/views/posts/_form.html.erbを編集し、form_withのmodelオプションに渡すオブジェクトを、@post_formに変更する
    <%#= form_with model: @post, id: 'new_post', local: true do |f| %>
    <%= form_with model: @post_form,  url: url, method: method, id: 'new_post', local: true do |f| %>
    
    <%#= 省略 %>
    
  11. リロードし、新規投稿できることをブラウザで確認する
    Image from Gyazo

手順2(Formオブジェクトを用いた編集機能を実装する)

  1. コントローラーのeditアクションを変更する
    app/controllers/posts_controller.rb
    # 省略
    
    def edit
      # @postから情報をハッシュとして取り出す
      post_attributes = @post.attributes
      # @post_formとしてインスタンス生成
      @post_form = PostForm.new(post_attributes)
    end
    
    # 省略
    
  2. コントローラーのupdateアクションを変更する
    app/controllers/posts_controller.rb
    # 省略
    
    def update
      # paramsの内容を反映したインスタンスを生成する
      @post_form = PostForm.new(post_form_params)
    
      if @post_form.valid?
        # バリデーションに問題なければ、値を更新
        @post_form.update(post_form_params, @post)
        redirect_to root_path
      else
        render :edit, status: :unprocessable_entity
      end
    end
    
    # 省略
    
  3. データを更新する処理を変更する
    app/models/post_form.rb
      # 省略
      
      # フォームから送られてきた情報を使用して、レコードを更新する処理
      def update(params, post)
        post.update(params)
      end
    end
    
  4. app/views/posts/edit.html.erbを編集し、renderメソッドのlocalsオプションで変数を指定する
    <h3>編集ページ</h3>
    <%#= render partial: 'form' %>
    <%= render partial: 'form', locals: {url: post_path(@post.id), method: :patch} %>
    
  5. ブラウザで編集画面を確認すると、下記エラーが発生している
    Image from Gyazo
    PostFormクラスでは、attr_accesorを使ってカラム名を扱えるように設定していたが、idは指定していない。これがエラーの原因
  6. PostFormクラスに、idなどのカラム名を指定する
    app/models/post_form.rb
    class PostForm
      include ActiveModel::Model
    
      #PostFormクラスのオブジェクトがPostモデルの属性を扱えるようにする
      attr_accessor(
        :text, :image,
        :id, :created_at, :updated_at
      )
      
      # 省略
    
    • 投稿日時に関連したカラムであるcreated_at updated_atの値も追加
  7. ブラウザにて、確認する
    1. テキストを修正し、画像を選択しなおせることを確認する
      Image from Gyazo
    2. テキスト・画像を修正しない場合、エラーが発生することを確認する
      Image from Gyazo
      • 画象を選択し直さなかった場合、コントローラーで画像の情報を受け取ることができていない
      • 投稿に紐づく画像の情報が呼び出せておらず、パラメーターとして画像の情報を送信できていないことが原因
  8. @post_form.imageが定義されていない場合、自己代入演算子||=を使用して@postのimage情報を代入する
    app/controllers/posts_controller.rb
    # 省略
    
    def update
      # paramsの内容を反映したインスタンスを生成する
      @post_form = PostForm.new(post_form_params)
    
      # 画像を選択し直していない場合は、既存の画像をセットする
      @post_form.image ||= @post.image.blob
      
      # 省略
    
  9. テキスト・画像を修正しない場合でも、正常にデータ保存できることを確認する
    Image from Gyazo

手順3(タグを保存するモデルを作成する)

  1. Tagモデルを作成する
    % rails g model tag
    
  2. マイグレーションファイルを編集する
    db/migrate/20XXXXXXXXXXXX_create_tags.rb
    class CreateTags < ActiveRecord::Migration[7.1]
      def change
        create_table :tags do |t|
          t.string :tag_name, null: false # タグの重複を避ける
          t.timestamps
        end
        add_index :tags, :tag_name, unique: true # 検索を簡単にする
      end
    end
    
  3. 中間テーブルのPostTagモデルを作成する
    % rails g model post_tag_relation
    
  4. マイグレーションファイルを編集する
    db/migrate/20XXXXXXXXXXXX_create_post_tag_relations.rb
    class CreatePostTagRelations < ActiveRecord::Migration[7.1]
      def change
        create_table :post_tag_relations do |t|
          t.references :post, foreign_key: true
          t.references :tag, foreign_key: true
          t.timestamps
        end
      end
    end
    
  5. マイグレートする
    % rails db:migrate
    
  6. 各モデルのアソシエーションを設定する
    1. Postモデル
      app/models/post.rb
        # 省略
        
        has_many :post_tag_relations
        has_many :tags, through: :post_tag_relations
      end
      
    2. Tagモデル
      app/models/tag.rb
      class Tag < ApplicationRecord
        has_many :post_tag_relations
        has_many :posts, through: :post_tag_relations
      end
      
    3. PostTagRelationモデル
      app/models/post_tag_relation.rb
      class PostTagRelation < ApplicationRecord
        belongs_to :post
        belongs_to :tag
      end
      
  7. バリデーションを記述する
    一意性の制約はモデル単位で設ける必要があるため、FormオブジェクトのPostFormクラスではなく、Tagモデルに記述する
    app/models/tag.rb
    class Tag < ApplicationRecord
      has_many :post_tag_relations
      has_many :posts, through: :post_tag_relations
      validates :tag_name,  uniqueness: true
    end
    

手順4(タグを一緒に投稿できるようにする)

  1. attr_accessorにtag_nameを追加する
    app/models/post_form.rb
    #PostFormクラスのオブジェクトがPostモデルの属性を扱えるようにする
    attr_accessor(
      :text, :image,
      :id, :created_at, :updated_at,
      :tag_name
    )
    
    :updated_atの後ろの,を忘れないように注意
  2. saveメソッドの内容を編集し、タグの情報と、タグと記事の紐付けの情報を保存する処理を追加する
    app/models/post_form.rb
    # フォームから送られてきた情報をテーブルに保存する処理
    def save
      #Post.create(text: text, image: image)
      post = Post.create(text: text, image: image) # 保存したpostのレコードを変数postに代入
      if tag_name.present?
        tag = Tag.where(tag_name: tag_name).first_or_initialize # tagが重複して保存されることを防ぐため、first_or_initializeメソッドを使用
        tag.save
        PostTagRelation.create(post_id: post.id, tag_id: tag.id) # tagとpostの紐付けの情報を、中間テーブルに保存
      end
    end
    
  3. ストロングパラメーターにtag_nameを追加する
    app/controllers/posts_controller.rb
    private
    def post_form_params
      params.require(:post_form).permit(:text, :tag_name, :image)
    end
    
    imageの前に追加するように注意
  4. タグを入力するフォームを追加するため、app/views/posts/_form.html.erbを編集する
    <%#= form_with model: @post, id: 'new_post', local: true do |f| %>
    <%= form_with model: @post_form,  url: url, method: method, id: 'new_post', local: true do |f| %>
    <%= render 'shared/error_messages', model: f.object %>
      <div class="message-field">
        <%= f.text_field :text, placeholder: 'type a message' %>
      </div>
    
      <%#= タグ入力フォーム%>
      <div class="tag-field">
        <%= f.text_field :tag_name, placeholder: 'add tags' %>
      </div>
    
  5. 詳細ページでタグの情報を表示できるようにするため、app/views/posts/show.html.erbを編集する
    <tbody>
      <tr>
        <th>名前</th>
        <td><%= @post.text%></td>
      </tr>
      <!-- タグ項目を追加 -->
      <tr>
        <th>タグ</th>
        <td><%= @post.tags.first&.tag_name %></td>
      </tr>
    </tbody>
    
    ※タグは任意項目なので、タグが存在している時のみ、タグ名を表示する
  6. ブラウザ確認
    1. 新規投稿時に、タグを一緒に投稿できる
      Image from Gyazo
    2. 同じタグで複数投稿している場合、登録されているタグは1つである
      Image from Gyazo
      Image from Gyazo
    3. タグを追加していない場合、詳細画面がエラーにならない
      Image from Gyazo

手順5(タグを一緒に編集できるようにする)

  1. tagが存在する場合、編集画面の入力フォームにタグ名が表示されるようにするため、editアクションを編集する
    app/controllers/posts_controller.rb
    # 省略
    
    def edit
      # @postから情報をハッシュとして取り出す
      post_attributes = @post.attributes
      # @post_formとしてインスタンス生成
      @post_form = PostForm.new(post_attributes)
      # tagがあれば、入力フォームにタグ名を表示
      @post_form.tag_name = @post.tags.first&.tag_name
    end
    
    # 省略
    
  2. データ更新処理を変更する
    app/models/post_form.rb
      # 省略
      
      # フォームから送られてきた情報を使用して、レコードを更新する処理
      def update(params, post)
        #post.update(params)
        
        #一度タグの紐付けを消す
        post.post_tag_relations.destroy_all
    
        #paramsの中のタグの情報を削除。同時に、返り値としてタグの情報を変数に代入
        tag_name = params.delete(:tag_name)
    
        #もしタグの情報がすでに保存されていればインスタンスを取得、無ければインスタンスを新規作成
        tag = Tag.where(tag_name: tag_name).first_or_initialize if tag_name.present?
    
        #タグを保存
        tag.save if tag_name.present?
        post.update(params)
        PostTagRelation.create(post_id: post.id, tag_id: tag.id) if tag_name.present?
      end
    end
    
  3. ブラウザで確認する
    1. 投稿済みのタグが編集できることを確認する
      Image from Gyazo
    2. 未投稿のタグに対して編集できることを確認する
      Image from Gyazo

手順6(インクリメンタルサーチを実装する)

  1. インクリメンタルサーチの実装準備を行う
    1. コントローラーに、searchアクションを定義する
      app/controller/posts_controller.rb
      # 省略
      
      def search
        # フォームの入力内容が空であれば、Javascriptにnilを返す
        return nil if params[:keyword] == ""
        # フォームの入力内容をもとに、あいまい検索。DB内に一致するものがあれば、変数tagに情報を代入
        tag = Tag.where(['tag_name LIKE ?', "%#{params[:keyword]}%"] )
        # json形式で、変数tagの情報をJavascriptに返す
        render json:{ keyword: tag }
      end
      
      private
      
      # 省略
      
    2. searchアクションのルーティングを設定する
      config/routes.rb
      Rails.application.routes.draw do
        root "posts#index"
        resources :posts, only: [:new, :create, :show, :edit, :update] do
          collection do
            get 'search'
          end
        end
      end
      
    3. インクリメンタルサーチの結果を表示するスペースを作るため、app/views/posts/_form.html.erbを編集する
      <%#= タグ入力フォーム%>
      <div class="tag-field">
        <%= f.text_field :tag_name, placeholder: 'add tags' %>
        <div id="search-result"></div><%#= インクリメンタルサーチの結果を表示するスペース%>
      </div>
      
    4. CSSファイルを編集する
      app/assets/stylesheets/show.css
      /* 省略 */
      
      #search-result{
        width: 30%;
      }
      
      .child{
        margin: 5px;
        border: 1px solid #dedede;
        background-color: #F4F4F4;
        padding: 5px;
        font-size: 12px;
      }
      
    5. インクリメンタルサーチが実装できているか確認するため、タグを複数登録する
  2. 非同期通信を行うまでの実装を行う
    1. app/javascript/tag.jsを作成する
    2. JSファイルを読み込むため、config/importmap.rbを編集する
      config/importmap.rb
      # 最終行に追記
      pin "tag", to: "tag.js"
      
    3. JSファイルを読み込むため、app/javascript/application.jsを編集する
      app/javascript/application.js
      // 最終行に追記
      import "tag"
      
    4. JSファイルが読み込めているかを確認するため、JSファイルを編集する
      app/javascript/tag.js
      console.log("読み込み完了");
      
    5. ブラウザのコンソール画面にて、「読み込み完了」が表示されることを確認する
    6. JSファイルの読み込み対象を限定するため、JSファイルを編集する
      app/javascript/tag.js
      document.addEventListener("turbo:load", () => {
        // タグの入力フォームのidを指定し、タグの要素を取得
        const tagNameInput = document.querySelector("#post_form_tag_name");
        
        if (tagNameInput){
          // tagNameInputが存在する画面の場合のみ、読み込まれる
          console.log("読み込み完了");
        };
      });
      
    7. 新規投稿画面・編集画面のみJSファイルが読み込まれることを確認する
      Image from Gyazo
      Image from Gyazo
    8. タグの検索に必要な情報を取得する
      app/javascript/tag.js
        // 省略
        if (tagNameInput){
          // inputElementを定義
          const inputElement = document.getElementById("post_form_tag_name");
          // フォームに文字が入力されたときに発火する関数を定義
          inputElement.addEventListener("input", () => {
            // フォームに入力されている文字列を変数keywordに代入
            const keyword = document.getElementById("post_form_tag_name").value;
            // 変数keywordの中身を確認する
            console.log(keyword);
          });
        };
      });
      
    9. 変数keywordの中身をブラウザで確認する
      Image from Gyazo
    10. XMLHttpRequestオブジェクトを生成する
      app/javascript/tag.js
            // 省略
            
            // フォームに入力されている文字列を変数keywordに代入
            const keyword = document.getElementById("post_form_tag_name").value;
            // 非同期通信に必要なXMLHttpRequestオブジェクトを生成し、変数XHRに代入
            const XHR = new XMLHttpRequest();
          });
        };
      });
      
    11. searchアクションへのリクエストを定義する
      app/javascript/tag.js
            // 省略
            
            // 非同期通信に必要なXMLHttpRequestオブジェクトを生成し、変数XHRに代入
            const XHR = new XMLHttpRequest();
            // searchアクションへのリクエストを定義
            XHR.open("GET", `/posts/search/?keyword=${keyword}`, true);
          });
        };
      });
      
    12. レスポンスの形式を指定する
      app/javascript/tag.js
            // 省略
            
            // searchアクションへのリクエストを定義
            XHR.open("GET", `/posts/search/?keyword=${keyword}`, true);
            // コントローラーから返却されるデータの形式は、jsonを指定
            XHR.responseType = "json";
          });
        };
      });
      
    13. リクエストを送信する記述を追記する
      app/javascript/tag.js
            // 省略
            
            // コントローラーから返却されるデータの形式は、jsonを指定
            XHR.responseType = "json";
            // リクエストを送信
            XHR.send();
          });
        };
      });
      
    14. ブラウザで文字を入力すると、サーバーにてリクエストが送られていることを確認する
      Image from Gyazo
  3. 非同期通信後の処理を実装する
    1. 非同期通信が成功したときに呼び出される関数を設定する
      app/javascript/tag.js
            // 省略
            
            // リクエストを送信
            XHR.send();
            // 非同期通信が成功したときに呼び出される関数を設定
            XHR.onload = () => {
              console.log("非同期通信成功");
            };
          });
        };
      });
      
    2. ブラウザでタグの入力フォームに文字を入力すると、「非同期通信成功」と表示されることを確認する
      Image from Gyazo
    3. サーバーサイドからのレスポンスを受け取る
      app/javascript/tag.js
            // 省略
            
            // 非同期通信が成功したときに呼び出される関数を設定
            XHR.onload = () => {
              // サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取る
              const tagName = XHR.response.keyword;
            };
          });
        };
      });
      
    4. タグを表示させる処理を記述する
      app/javascript/tag.js
              // 省略
              
              // サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取る
              const tagName = XHR.response.keyword;
      
              // HTMLを作成し、タグを表示する
              const searchResult = document.getElementById("search-result"); // idを取得
              // 検索結果があるだけ繰り返す
              tagName.forEach((tag) => {
                const childElement = document.createElement("div"); // タグを表示させるための要素を生成
                childElement.setAttribute("class", "child"); // 生成した要素にclassを指定
                childElement.setAttribute("id", tag.id); // 生成した要素にidを指定
                childElement.innerHTML = tag.tag_name; // 生成した要素の内容に検索結果のタグ名を指定
                searchResult.appendChild(childElement); // 生成したchildElement要素をsearchResult要素に挿入
              });
            };
          });
        };
      });
      
    5. ブラウザにて、インクリメンタルサーチが実装されたことを確認する
      Image from Gyazo
    6. クリックしたタグ名がフォームに入力されるようにする
      app/javascript/tag.js
                // 省略
                
                searchResult.appendChild(childElement); // 生成したchildElement要素をsearchResult要素に挿入
      
                // クリックしたタグ名がフォームに入力される
                const clickElement = document.getElementById(tag.id);
                // clickイベントを指定
                clickElement.addEventListener("click", () => {
                  // フォームにタグ名を入力
                  document.getElementById("post_form_tag_name").value = clickElement.textContent;
                  // タグを表示している要素を削除
                  clickElement.remove();
                });
              });
            };
          });
        };
      });
      
    7. ブラウザで表示されたタグをクリックすると、クリックしたタグが消えて、フォームにクリックしたタグ名が入力されることを確認する
      Image from Gyazo
    8. 直前の検索結果を消すように修正する
      app/javascript/tag.js
      // 省略
      
      // HTMLを作成し、タグを表示する
      const searchResult = document.getElementById("search-result"); // idを取得
      // 直前の検索結果を消す
      searchResult.innerHTML = "";
      // 検索結果があるだけ繰り返す
      tagName.forEach((tag) => {
      
      // 省略
      
    9. ブラウザで確認
      1. ブラウザでタグの入力フォームに文字を入力すると、直前の検索結果が消えている
        Image from Gyazo
      2. 入力した文字を削除すると、コンソール上でエラーが発生する
        Image from Gyazo
    10. レスポンスにデータが存在する場合のみ処理を実行するように編集する
      app/javascript/tag.js
            // 非同期通信が成功したときに呼び出される関数を設定
            XHR.onload = () => {
              // HTMLを作成し、タグを表示する
              const searchResult = document.getElementById("search-result"); // idを取得
              // 直前の検索結果を消す
              searchResult.innerHTML = "";
      
              // レスポンスにデータが存在する場合のみ、タグを表示させる
              if (XHR.response) {
                // サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取る
                const tagName = XHR.response.keyword;
                // 検索結果があるだけ繰り返す
                tagName.forEach((tag) => {
                  const childElement = document.createElement("div"); // タグを表示させるための要素を生成
                  childElement.setAttribute("class", "child"); // 生成した要素にclassを指定
                  childElement.setAttribute("id", tag.id); // 生成した要素にidを指定
                  childElement.innerHTML = tag.tag_name; // 生成した要素の内容に検索結果のタグ名を指定
                  searchResult.appendChild(childElement); // 生成したchildElement要素をsearchResult要素に挿入
      
                  // クリックしたタグ名がフォームに入力される
                  const clickElement = document.getElementById(tag.id);
                  // clickイベントを指定
                  clickElement.addEventListener("click", () => {
                    // フォームにタグ名を入力
                    document.getElementById("post_form_tag_name").value = clickElement.textContent;
                    // タグを表示している要素を削除
                    clickElement.remove();
                  });
                });
              }
            };
          });
        };
      });
      
    11. ブラウザでタグ項目に入力した文字を削除しても、コンソール上でエラーが発生しない
      Image from Gyazo
1
0
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?