Stimulusの習作: Tagifyでのタグ入力(クライアント側)

外観

Stimulusの習作としてTagifyを用いたタグ入力を行います。入力したタグをRailsのバックエンドに保存したいと考えているため、画面のHTMLはRailsで生成しています。サーバ側と組み合わせた画面は次のようになります。

※今回やっていることとしては特にTagifyを動的に操作する必要がないため、別のページに簡素な実装方法を書きました。こちらと比較すると良いかもしれません。

画像1

画像2

環境

macOS 10.15.4
Ruby 2.6.5
Rails 6.0.2.2
Yarn 1.22.4
Node 13.12.0
stimulus@1.1.1
@yaireo/tagify@3.7.1

リポジトリ

参照

Tagifyの追加

yarnでTagifyを追加する。
SCSSファイルをコピーする。今回はstylesheet_pack_tagを用いませんでした。

yarn add @yaireo/tagify
cp node_modules/@yaireo/tagify/src/tagify.scss app/assets/stylesheets

Stimulusコントローラ

仕様は次のようになります。

Stimulusのコントローラは、TagifyControllerとする。
Stimulusのターゲットは、tagifytagNamesとする。
Stimulusのアクションは、使用しない。

HTMLの例

<div class="field" data-controller="tagify" data-whitelist="キャラクター,グルメ,...">
 <label for="landmark_tag_names">タグ(5つまで)</label>
 <input type="text" name="tagify" id="tagify" value="" data-target="tagify.tagify" />
 <input data-target="tagify.tagNames" type="hidden" value="駅,公共施設,観光"
   name="landmark[tag_names]" id="landmark_tag_names" />
</div>

ターゲットtagifyTargetは、Tagifyでのタグ入力で使用する。
従ってtagifyTargetは、<input>要素(または<textarea>要素)でなければならない。
Tagifyは、ターゲットtagifyTargetにShadow DOMを追加する。
従ってアプリは、Tagify生成後、tagifyTargetの内容に依存してはならない。Tagifyが提供するインタフェース経由で内容を操作する。
Tagifyは、新規にtags(クラスはtagify)要素とその配下にtag要素を生成し、タグ入力を可能とする。
上記のタグ入力は、スタイルシートtagify.scssを必要とする。

ターゲットtagNamesTargetは、Railsの隠しフィールドとする。
ターゲットtagNamesTargetで、カンマ区切りのタグ名をサーバとやり取りする。
ターゲットtagNamesTargetの値は、Tagifyのタグの初期値とする。

コントローラ要素のdata-whitelist属性からTagify用のwhitelistを取得する。
※名称に違和感がありますがホワイトリスト以外のタグも自由に入力できます。

実装
app/javascript/controllers/tagify_controller.js

tagifyをimportしてTagifyを参照可能にする。
Stimulusのターゲットを設定する。

import { Controller } from "stimulus"
import Tagify from '@yaireo/tagify'

export default class extends Controller {
 static targets = [ "tagify", "tagNames" ]

tagifyTargetを渡してTagifyを生成する。
data-whitelistをwhitelistに設定する。
タグの最大数を5つに設定した。
ドロップダウンの設定はREADME.mdのSuggestions selectboxを参考にした。
タグの追加/削除イベントで隠しフィールドにタグ名を保存する。
tagNamesTargetをaddTagsでタグに設定する。

  connect() {
   const whitelist = this.element.getAttribute('data-whitelist').split(',')
   this.tagify = new Tagify(this.tagifyTarget, {
     whitelist: whitelist,
     maxTags: 5,
     dropdown: {
       classname: "color-blue",
       enabled: 0,
       maxItems: 30,
       closeOnSelect: false,
       highlightFirst: true,
     },
   })
   this.tagify.on('add', e => this.saveTagNames(e.detail.tagify))
   this.tagify.on('remove', e => this.saveTagNames(e.detail.tagify))

   const tagNamesStr = this.tagNamesTarget.value
   if (tagNamesStr.length > 0) {
     this.tagify.addTags(tagNamesStr.split(','))
   }
 }
  saveTagNames(tagify) {
   this.tagNamesTarget.value = tagify.value.map(v => v.value)
  }

切断されたら、Turbolinksでの冪等性を担保するために、
入力された全てのタグを削除する。
Tagifyが生成した要素を削除する。

  disconnect() {
   this.tagify.removeAllTags()
   const classes = this.element.getElementsByClassName('tagify')
   Array.from(classes).forEach(e => e.remove())
 }
}

以上です。

付録

こちらに変更(改良?)版を書きました。

Rails new
rails new KyotikaLandS

Landmark
Landmark scaffold
bin/rails g scaffold Landmark name hiragana latitude:float longitude:float url question answer1 answer2 answer3 correct:integer author

Landmark validation
vim db/migrate/*_create_landmarks.rb

  t.string :name, null: false
  t.string :hiragana, null: false

vim app/models/landmark.rb

 validates :name, :hiragana, presence: true, uniqueness: true
 validates :hiragana, format: { with: /\A([ぁ-ん]|ー)+\z/,
                                message: 'は、ひらがなを入力してください。' }
 validates :correct, numericality: { greater_than_or_equal_to: 1,
                                     less_than_or_equal_to: 3 }

bin/rails db:migrate

Landmark Seed
vim db/seeds.rb

landmark = Landmark.create!(
 name: 'JR京都駅',
 hiragana: 'じぇーあーるきょうとえき',
 latitude: 34.9855,
 longitude: 135.758,
 url: 'https://ja.wikipedia.org/wiki/京都駅',
 question: '日本一長いホームとして知られるJR京都駅のホームの長さは?',
 answer1: '558m',
 answer2: '672m',
 answer3: '773m',
 correct: 1,
 author: '臼谷泰弘'
)

bin/rails db:seed

Tag
Tag scaffold

bin/rails g scaffold Tag name

Tag validation
vim db/migrate/*_create_tags.rb

  t.string :name, null: false

vim app/models/tag.rb

  validates :name, presence: true, uniqueness: true

bin/rails db:migrate

Tagging
Tagging scaffold

bin/rails g model Tagging landmark:belongs_to tag:belongs_to

Tagging relation
vim app/models/landmark.rb

  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings

vim app/models/tag.rb

  has_many :taggings, dependent: :destroy
  has_many :landmarks, through: :taggings

bin/rails db:migrate

Tagging seed
vim db/seeds.rb

landmark.tags.create!(
 [
   { name: '駅' },
   { name: '公共施設' },
   { name: '観光' }
 ]
)

Tag.create!(
 [
   { name: 'キャラクター' },
   { name: 'グルメ' },
   { name: '出町柳' },
   { name: '動物' },
   { name: '博物館' },
   { name: '商業施設' },
   { name: '国宝' },
   { name: '平家' },
   { name: '橋' },
   { name: '流鏑馬' },
   { name: '漫画' },
   { name: '神社' },
   { name: '祭り' },
   { name: '聖徳太子' },
   { name: '葵祭' },
   { name: '賀茂川' },
   { name: '重要文化財' },
   { name: '音楽' },
   { name: '鴨川' }
 ]
)

bin/rails db:seed:replant

Stimulus
bin/rails webpacker:install:stimulus

Add Tagify
yarn add @yaireo/tagify
cp node_modules/@yaireo/tagify/src/tagify.scss app/assets/stylesheets

Tagify MVC
app/javascript/controllers/tagify_controller.js
app/models/landmark.rb
app/views/landmarks/_form.html.erb
app/controllers/landmarks_controller.rb

この記事が気に入ったらサポートをしてみませんか?