LoginSignup
17
5

More than 1 year has passed since last update.

Rails + Vue.jsによる動的タグ付け機能

Last updated at Posted at 2022-05-10

はじめに

社会人1年目の問題意識・問題解決の習慣化と情報共有を補助するツールとして意習(issue)というアプリを作りました。日頃の業務の中で遭遇するイシューをアウトプット、新人同士で情報共有し、メンターがフォロー/サポートすることで問題意識と解決能力を養う、というアプリです。

タグ機能を作成するときに、acts-as-taggable-onによる動的タグ付け(複数選択可)とUIにvue-multiserectを使用しました。この組み合わせでの記事が見当たらなかったので、記載することとしました。
完成版は、下図の通りです。

tags.gif

環境

  • Ruby: 3.0.1
  • Ruby on Rails: 6.0.3
    • acts-as-taggable-on: 9.0.1
  • Vue.js: 2.6.14
    • Vue-multiselect: 2.1.4
  • jQuery: 3.5.1
  • postgreSQL: 14.2

:pushpin:前置き

注意点

現在vue.jsの最新版は3系ですが、使用しているライブラリ(vue-multiselect)が対応していないため(実際3系ではコードを変えても動きませんでした)、2系を使用しています。

説明しないこと

  • yarnなどを用いたjQuery, Bootstrap, Vue.jsのインストール方法(CDNでの実装を説明します)。
  • Vue.jsの細かな説明。
  • Ransackでのタグの検索方法。

前提

Userモデル(なくてもOK)とIssueモデル(≒記事投稿のモデル)はすでに作成済み、Issueのテーブル及びCRUD機能も実装済みとします。

tags.jpg

acts-as-taggable-on導入手順

acts-as-taggable-onは動的タグ付けが可能となるgemです。記事投稿と同時に新たなタグを生成し登録することも、既存のタグをつけることも可能です。
acts-as-taggable-onに関して詳しく知りたい方は、gemのReadMeや、 【Rails】タグ管理機能(acts-as-taggable-on使用)をご確認下さい。

acts-as-taggable-onのインストール

Gemfileに以下を追加。

Gemfile
gem 'acts-as-taggable-on'

ターミナルにて以下を実行しインストール.

ターミナル
rails acts_as_taggable_on_engine:install:migrations
rails db:migrate

これにより、tags, taggingsの2つのテーブルが作成されます。

注: MySQL使用の場合は、migrate前に以下を実行する必要があるようです。

ターミナル
rake acts_as_taggable_on_engine:tag_names:collate_bin

詳細は、gemのReadMeをご確認下さい。ただ、うまくいかない場合があるようなので、MySQL使用の方は、こちらの記事が参考になるかもしれません。 -> 【Rails】タグ管理機能(acts-as-taggable-on使用)

モデルの設定

タグをつけたいモデル(Issue)に以下を記載。タグの名前をtagとする場合の設定です。タグの名前をskillとしたければ、tagsskillsに置き換えて下さい。

issue.rb
acts_as_taggable_on :tags

この設定により、Taggingモデルのtaggable_idIssueモデルのidが繋がります。
Userモデルとtagを結びつける必要はなかったため、今回、UserモデルとTaggingモデルの連携は作成しません。

tags.jpg

コントローラーの設定

タグ付けするモデルコントローラーのストロングパラメーターに:tag_listを追加します。

issues_controller.rb
private
def issue_params
  params.require(:issue).permit(..., :tag_list)
end

ビューの設定

後ほど、vue-multiselectの設定に置き換えます。一旦、動作するかの確認のため、以下を追記します。
Issueの新規投稿、編集はパーシャルの_form.html.erbを使用している設定です。

_form.html.erb
<%= form_with(model: issue, class: "form", local: true) do |form| %>
  <%# 省略 %>
  <div class="form-group">
    <%= form.label :tag_list %>
    <%= text_field_tag "issue[tag_list]", issue.tag_list.join(","), class: "form-control" %>
  </div>
 <%# 省略 %>
<% end %>

Issueのshowビューに以下を追加。

show.html.erb
<%# 省略 %>
<div class="issue-tag-container">
  <% @issue.tag_list.each do |tag_name| %>
    <%= tag_name %><br>
  <% end %>
</div>
<%# 省略 %>

以下のように、テキスト入力で登録できていれば成功です。
tag_test.gif

実際に、コントローラーに送られるパラメーターは、以下のように "," 区切りの文字列なります。

<ActionController::Parameters {..., "issue"=>
<ActionController::Parameters {..., "tag_list"=>"DI,抗がん剤,こんにちは!", ...} ...>, ...>

acts-as-taggable-onのメソッド紹介

使用したメソッドを一部紹介します。タグ付けしたモデルをIssue, タグをtagとして記載します。

メソッド 説明
Issue.tagged_with(name) nameのタグ付けがされた全Issueを返す
Issue.tags_on(:tags) そのモデルに登録されている全タグを返す 
Issue#tags_on(:tags) そのインスタンスに登録されている全タグを返す 
Issue#tag_list そのインスタンスに登録されている全タグのnameを配列で返す
Issue#tag_list=(value) そのインスタンスのタグにvalueを登録・上書き(登録・上書きにはsaveも必要)。valueはタグのnameを配列orカンマ区切りの文字列で渡す。例: ["east", "south"] or "east, south"
ActsAsTaggableOn::Tag.most_used(value=20) 登録数が多い順でタグを返す。デフォルトは20個。
ActsAsTaggableOn::Tag#taggings_count そのタグの登録数を返す

vue-multiselect導入手順

vue-multiselectは、見た目が良いセレクトボックスを実装できるVueのライブラリです。ドロップダウンで、単一選択、複数選択、タグ付け等ができます。詳細は、リンクを参照して下さい。
ただ、リンク先の記述ではうまく動作しなかったため、以下に記載するコードはリンク先とは異なります。

Vueのインストール(CDNの設定)

Vue.js, vue-multiselect, jQueryのCDNを設定し使用できるようにします。
jQueryをすでにインストール済みの場合は、jQueryの部分は削除して下さい。

application.html.erb
<head>
  <%# 省略 %>
  <%# jQueryをインストールしている場合は最初の記述は不要 %>
  <script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14" defer></script>
  <script src="https://unpkg.com/vue-multiselect@2.1.4" defer></script>
  <link rel="stylesheet" href="https://unpkg.com/vue-multiselect@2.1.4/dist/vue-multiselect.min.css">
  <%# 省略 %>
</head>

formの記述変更

Issueの新規投稿、編集はパーシャルの_form.html.erbを使用している設定です。
_form.html.erbに先ほど動作確認用に記述したコードを削除し、以下のコードに書き換えます。

_form.html.erb
<%= form_with(model: issue, class: "form", local: true) do |form| %>
  <%# 省略 %>
  <%= content_tag(:div, id:"app", data:{tag_list: Issue.tags_on(:tags).pluck(:name), my_tag_list: issue.tag_list}) do %>
    <multiselect v-model="myTagList" tag-placeholder="Add this as new tag" placeholder="Search or add a tag"  :options="tagList" :multiple="true" :taggable="true" @tag="addTag" id="vue-tag-input"></multiselect>
    <pre class="language-json" hidden="hidden">
      <input type="text" name="issue[tag_list]" id="issue_tag_list" v-model="myTagList">
    </pre>
  <% end %>
  <%# 省略 %>
<% end %>

解説

1. Rails -> Vueへのデータ受け渡し

Vue.jsで処理する範囲の目印としてid: "app"とします。
RailsからVue.jsへ、既存のタグデータ、および、編集時には、その記事に登録されているタグデータの受け渡しが必要です。
今回は、html内にデータを記述し、それをjavascriptで取得する方法で実装しました。data:{ }内の記述が受け渡しデータになります。

# モデル.tags_on(:タグ名)でそのモデルに紐づく全タグを取得できます。
# pluck(:name)でタグを配列で取得します。
tag_list: Issue.tags_on(:tags).pluck(:name)

# モデルのインスタンス.tag_listでそのモデルインスタンスに紐づくタグのnameを配列で取得できます。
my_tag_list: issue.tag_list
2. Vue -> Railsへのデータ受け渡し

記事投稿、編集画面で操作されたタグデータを今度はRails側に渡さなければなりません。
<pre>...</pre>の中がデータ受け渡し用の記述となります。hiddenを設定することで、画面には現れないようにしています。

<%# 所謂Railsの "hidden_field" の状態 #>
<pre class="language-json" hidden="hidden">
  <input type="text" name="issue[tag_list]" id="issue_tag_list" v-model="value">
</pre>
3. Vue.jsの説明

ディレクティブ(接頭辞v-が付いたVue.jsの特別な属性)などを少しだけ紹介します。詳細は、公式ドキュメントをご参照下さい。Vue.jsをそれなりに知っている方は、読み飛ばすことをお勧めします。

ディレクティブ 説明
v-model 入力イベントで要素を自動的に更新します。v-model="myTagList"とすることで、myTagListのデータを表示し、入力により変化したときにはmyTagListを更新します。
@ インスタンス変数ではありません。v-on:の省略記法です。イベントの発火時にjavascriptのメソッドを実行します。@tag="addTag"tagイベント(新規のタグ作成)時にaddTagメソッド(tagList, myTagListにタグ追加)が実行されます。
: v-bind:の省略記法です。 属性を動的に設定できます。 :options="tagList"は動的に変化するtagListの値を渡しています。

javascript(jQuery/Vue.js)設定

tags.jsファイル新規作成

app/javascript/packsに、tags.jsファイルを作成し、以下を記述します。
javascriptは行末にセミコロンを入れますが、Vueの公式ドキュメントではセミコロンが省略されています。また、以下のVue.jsのコードはVue3系によせた記述にしています。

tags.js
// turbolinkによりVue.jsがうまく動作しないことがあるため$(function(){});ではなく以下の記述
$(document).on('turbolinks:load', function() {
  // id="app"のタグにあるdata-tag-list, data-my-tag-listの情報をjQueryで取得しています。
  // _form.html.erbでは、"tag_list"とアンダースコアで記載していますが、htmlではハイフン表示になります。
  const tagList = $("#app").data("tag-list");
  const myTagList = $("#app").data("my-tag-list");

  // Vueコンポーネントのmultiselectを使用
  Vue.component('multiselect', window.VueMultiselect.default)
  new Vue({
    el: "#app",
    data() {
      return {
        // tagListとmyTagListの値をVue側の変数に代入し初期値(配列)にしています。
        // form側でv-modelを設定しているのでタグが追加・削除されるとmyTagListも追加・削除されます。
        tagList: tagList,
        myTagList: myTagList,
      }
    },
    methods: {
      // 新規のタグを、"tagList"及び"myTagList"に追加するメソッドを定義しています。
      // このメソッドがなくても、既存のタグは登録できます。新規のタグを生成したい場合は必要となります。
      addTag(newTag) {
        this.tagList.push(newTag)
        this.myTagList.push(newTag)
      },
    },
  })
});

application.jsにtags.jsファイルをインポート

app/javascript/packs/application.jsに以下を追記し、tags.jsが読み込まれるよう設定。

application.js
// 省略
import "./tags.js";

showビューの変更

Issueのshowビューを変更。先ほど記述したコードを削除し、以下に置き換え。

show.html.erb
<%# 省略 %>
<div class="issue-tag-container">
  <% @issue.tag_list.each do |tag_name| %>
    <%= content_tag(:span, tag_name, class: "tag") %>
  <% end %>
</div>
<%# 省略 %>

cssを追加。

aplication.css
.tag {
  display: inline-block;
  font-size: 12px;
  padding: 2px 10px;
  border-radius: 5px;
  margin-right: 5px;
  background-color: #41b883;
  color: #fff;
}

完成

以下のような動作になっていると思います。
新規作成した場合にshowビューに表示されているかまで確認して下さい。

tags.gif

おまけ:vue-multiselectに対するsystem spec

system specでは、入力フォームでのタグ入力は以下のコードで実装できます。
複数のタグを選択・登録したい場合は、その数だけ記述して下さい。

issue_spec.rb
# multiselectタグのidを"vue-tag-input"にしています。
fill_in("vue-tag-input", with: "タグのname", visible: false).send_keys :return

:green_book:参考資料

acts-as-taggable-on

vue-multiselect

vue-multiselectのrspec

17
5
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
17
5