【Laravel】vue-tags-inputで補完機能付きタグ入力

えび

Laravelviewで使用できる、タグ入力コンポーネントを実装した
コピペで使用できる、たぶん

vue-tags-inputをインストールする

// npmの場合
npm install --save @frk/vue-tags-input

// yarnの場合
yarn add @frk/vue-tags-input

Vueコンポーネントで使ってみる

今回はLaravelのコンポーネントとして使うので、下記条件を満たすよう実装する

  • どのフォームからも使用できるように、name属性propで指定できるようにする
  • 編集画面などでも使えるように、デフォルト表示のタグたちを指定できるようにする
  • submit時にPOSTするための値をhiddenで保持するようにする

ソースはこんな感じ

  • TagInput.vue
<template>
    <div>
      <vue-tags-input
          v-model="inputTag"
          :tags="selectedTags"
          @tags-changed="newTags => selectedTags = newTags"
          :autocomplete-items="filteredItems"
          @before-adding-tag="checkValidTag"
          :existing-tags="existingTags"
          :name="name"
          placeholder="タグを追加"
      />
      <input type="hidden" :name="name" :value="tagsValue" />
      <div v-if="errorText" class="error-text">{{ errorText }}</div>
    </div>
</template>

<script>
import VueTagsInput from '@frk/vue-tags-input';

export default {
  components: {
    VueTagsInput,
  },
  props: {
    // 既存のタグたち
    existingTags: {
      type: Array,
      default: () => []
    },
    // タグの初期値
    defaultTags: {
      type: Array,
      default: () => []
    },
    // inputのname属性
    name: {
      type: String,
      default: null
    },
  },
  data() {
    return {
      // 入力中のタグ文字列
      inputTag: '',
      // 選択されたタグ
      selectedTags: [],
      // エラー文言
      errorText: '',
    };
  },
  created() {
    // 初期表示したいタグがあれば代入する
    this.selectedTags = this.defaultTags
  },
  computed: {
    /**
     * autoCompleteのフィルタリング
     * @returns {*[]}
     */
    filteredItems() {
      return this.existingTags.filter(tag => {
        return tag.text.toLowerCase().indexOf(this.inputTag.toLowerCase()) !== -1;
      });
    },
    /**
     * hidden属性に持たせるvalueを生成する
     * @return string
     */
    tagsValue() {
      return this.selectedTags.map(function (tag) {
        return tag['text'];
      }).join(',')
    },
  },
  methods:{
    /**
     * タグの形式をチェックする
     * @param obj
     */
    checkValidTag(obj) {
      if (obj.tag.text.length > 30) {
        this.errorText = 'タグは30文字以内で入力してください'
      } else {
        this.errorText = ''
        obj.addTag();
      }
    },
  },
};
</script>

  • Laravelのview側ではこんな感じで呼び出す
<tag-input
  :existing-tags="[{text: 'Cat'}, {text: 'Dog'}, {text: 'Rabbit'}]"
  :default-tags="[{text: 'Cat'}]"
  name="tags"
></tag-input>

使ってみるとこんな感じ

textをkeyにしたオブジェクト配列を渡しているのは、後述の自動補完関連の兼ね合い。
もしModelの値とかを渡してあげたいときはゲッターで整形してあげればいいと思う

propのexisting-tags(=既存のタグ一覧)や
default-tags(=デフォルト表示にしたいタグ) は
指定がない場合は何も渡さなければOK

バリデーションエラー時にold値をデフォルトで渡してあげたい場合

若干力技で書くとこんな感じ

@php
  $defaultTags = [];
  if (old('tags')) {
    foreach (explode(',', old('tags')) as $tag) {
      $defaultTags[] = ['text' => $tag];
    }
  }
@endphp

<tag-input
  :existing-tags="[{text: 'Cat'}, {text: 'Dog'}, {text: 'Rabbit'}]"
  :default-tags="{{ json_encode($defaultTags) }}"
  name="tags"
></tag-input>

ざっくり補足

丁寧かつ詳しいドキュメントはこちら

propで受け取っているもの

  • existingTags: 既存のタグ一覧を入れた配列
    文字を入力した際に、この配列の中に文字一致するものがあれば自動補完される仕組み
  • defaultTags: デフォルト表示しておきたいタグ一覧を入れた配列
    編集時などに指定する
  • name: hiddenで生成するPOST用データのname属性

dataで定義しているもの

  • inputTag: v-modelに指定しているので、入力中のタグ文字列はここに入る
  • selectedTags: @tag-changedイベントで指定しているので選択されたタグ一覧がここに入る
  • errorText: 文字数オーバーの時にエラー文言を入れる

入力時の補完機能に関して

自動補完リストcomputedで生成しているfilterdItemsの値

/**
  * autoCompleteのフィルタリング
  * @returns {*[]}
 */
filteredItems() {
  return this.existingTags.filter(tag => {
    return tag.text.toLowerCase().indexOf(this.inputTag.toLowerCase()) !== -1;
  });
},

ここではthis.existingTags(=既存のタグ一覧) の中に
this.inputTag(=入力中の文字列) が含まれていれば、
それをフィルタリングして返している

その値を:autocomplete-itemsに渡してあげれば
自動補完として表示してくれるっていう流れ

:autocomplete-itemsに渡す値は、
必ず下記のようなtextをkeyに指定したオブジェクト配列で渡す必要がある
[{text: 'Cat'}, {text: 'Dog'}, {text: 'Rabbit'}]

hidden属性の生成に関して

ここでhidden属性を埋め込んでる

<input type="hidden" :name="name" :value="tagsValue" />

これが実際Controller側に渡される値になる

name属性はpropで受け取ったnameを指定して、
value属性にはcomputedで定義したtagsValueを入れてる

 /**
  * hidden属性に持たせるvalueを生成する
  * @return string
  */
  tagsValue() {
    return this.selectedTags.map(function (tag) {
      return tag['text'];
    }).join(',')
  },

ここではthis.selectedTags(=選択されているタグ一覧) の
text部分をカンマ区切りの文字列に変換している

実際のタグ表示はこんな感じ

<input type="hidden" name="tags" value="Cat,Dog">

ちなみにタグ選択時に自動で更新されるthis.selectedTagsの値をデバッグしてみると

[ { "text": "Cat", "tiClasses": [ "ti-valid" ] }, { "text": "Dog", "tiClasses": [ "ti-valid" ] } ]

この形式は変更できないので、上記のようにcomputed文字列に組み立て直してる

バリデーションに関して

30文字以上の入力は弾くバリデーションチェックを
checkValidTag()メソッドで行っている

checkValidTag(obj) {
  if (obj.tag.text.length > 30) {
    this.errorText = 'タグは30文字以内で入力してください'
  } else {
    this.errorText = ''
    obj.addTag()
  }
},

これの呼び出し元である@before-adding-tagはその名の通り、
タグが追加される直前に呼び出されるイベント
ここで、文字列の長さをチェックして、NGならエラー表示 / OKならobj.addTag()タグ追加を行っている

(ここから余談)
実はvue-tags-inputには、validationというのが指定できて、下記のようなpropを渡すことでチェックをすることもできる

<template>
    <vue-tags-input
      // (...諸々省略)
      :validation="validation" // ⭐️ここ
    />
</template>
<script>
import VueTagsInput from '@frk/vue-tags-input';

export default {
  // (...諸々省略)
  data() {
    return {
      // (...諸々省略)
      // ⭐️ここ
      validation: [{
        classes: 'max-length',
        rule: tag => tag.text.length > 30,
      }],
    };
  },
}
</script>

しかしこれはあくまで「タグの色」を変えるもので、実際の入力は弾いてくれない
(disableAddを指定すると弾いてはくれるみたいだけど・・)

[DEMO]
http://www.vue-tags-input.com/#/examples/validation

なので今回は使用してない