はじめに
社会人1年目の問題意識・問題解決の習慣化と情報共有を補助するツールとして意習(issue)というアプリを作りました。日頃の業務の中で遭遇するイシューをアウトプット、新人同士で情報共有し、メンターがフォロー/サポートすることで問題意識と解決能力を養う、というアプリです。
タグ機能を作成するときに、acts-as-taggable-on
による動的タグ付け(複数選択可)とUIにvue-multiserect
を使用しました。この組み合わせでの記事が見当たらなかったので、記載することとしました。
完成版は、下図の通りです。
環境
- 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
前置き
注意点
現在vue.jsの最新版は3系ですが、使用しているライブラリ(vue-multiselect)が対応していないため(実際3系ではコードを変えても動きませんでした)、2系を使用しています。
説明しないこと
- yarnなどを用いたjQuery, Bootstrap, Vue.jsのインストール方法(CDNでの実装を説明します)。
- Vue.jsの細かな説明。
- Ransackでのタグの検索方法。
前提
User
モデル(なくてもOK)とIssue
モデル(≒記事投稿のモデル)はすでに作成済み、Issue
のテーブル及びCRUD機能も実装済みとします。
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に以下を追加。
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
としたければ、tags
をskills
に置き換えて下さい。
acts_as_taggable_on :tags
この設定により、Tagging
モデルのtaggable_id
とIssue
モデルのid
が繋がります。
User
モデルとtag
を結びつける必要はなかったため、今回、User
モデルとTagging
モデルの連携は作成しません。
コントローラーの設定
タグ付けするモデルコントローラーのストロングパラメーターに:tag_list
を追加します。
private
def issue_params
params.require(:issue).permit(..., :tag_list)
end
ビューの設定
後ほど、vue-multiselectの設定に置き換えます。一旦、動作するかの確認のため、以下を追記します。
Issue
の新規投稿、編集はパーシャルの_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ビューに以下を追加。
<%# 省略 %>
<div class="issue-tag-container">
<% @issue.tag_list.each do |tag_name| %>
<%= tag_name %><br>
<% end %>
</div>
<%# 省略 %>
実際に、コントローラーに送られるパラメーターは、以下のように "," 区切りの文字列なります。
<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の部分は削除して下さい。
<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_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系によせた記述にしています。
// 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
が読み込まれるよう設定。
// 省略
import "./tags.js";
showビューの変更
Issue
のshowビューを変更。先ほど記述したコードを削除し、以下に置き換え。
<%# 省略 %>
<div class="issue-tag-container">
<% @issue.tag_list.each do |tag_name| %>
<%= content_tag(:span, tag_name, class: "tag") %>
<% end %>
</div>
<%# 省略 %>
cssを追加。
.tag {
display: inline-block;
font-size: 12px;
padding: 2px 10px;
border-radius: 5px;
margin-right: 5px;
background-color: #41b883;
color: #fff;
}
完成
以下のような動作になっていると思います。
新規作成した場合にshowビューに表示されているかまで確認して下さい。
おまけ:vue-multiselectに対するsystem spec
system specでは、入力フォームでのタグ入力は以下のコードで実装できます。
複数のタグを選択・登録したい場合は、その数だけ記述して下さい。
# multiselectタグのidを"vue-tag-input"にしています。
fill_in("vue-tag-input", with: "タグのname", visible: false).send_keys :return