Vue.js のカスタムディレクティブを使って動くメニュー作る

フロント開発がもっと楽しくなる!Vue.js 学習メモ。カスタムディレクティブのお勉強します。

前回の描画関数に続いて今まであまり触っていないカスタムディレクティブを使ってみます。

カスタムディレクティブの使い所

フォームの disable などデータバインディングだけで操作が出来ない場合に使ったり、他のライブラリとの連携で JavaScript プロパティやメソッドに直接アクセスしたい場合にシンプルに書けるみたいです。でもカスタムディレクティブで出来ることはコンポーネントの細分化やウォッチャーでも大体出来るので使い分けは曖昧なところです。プラグインを作る場合にも使ったりするみたいですが、これは複雑になりそうなのでまた別の機会にやろうと思います。

カスタムディレクティブの書き方

Vue.directive('name', { ... }) で登録するとグローバルに、コンポーネントの directives プロパティでローカルで登録できます。

Vue.component('my-component', {
  directives: {
    example: {
bind: function () { ... },
update: function () { ... }
}
  }
})

コンポーネントの bindinsertedupdatecomponentUpdatedunbind のイベントにフック出来ます。省略して関数を渡すと bindupdate に同じ処理が登録されます。

ちなみに this は使えないので代わりに、3番目の引数の vnode.context で呼んでいるコンポーネントを参照できます。

Vue.component('my-component', {
  directives: {
    example: function (el, binding, vnode) {
vnode.context.$emit('event')
    }
  }
})

フォームの属性操作に便利

フォームアイテムの属性や audiovideo などの属性を操作したい時に便利です。例えば入力内容によってボタンの disable を切り替えたい時、disable="boolean" とは書けないのでそんな時カスタムディレクティブを使います。

<p><input v-model="data1"> <button v-disable="data1.length>5">ボタン</button></p>
<p><input v-model="data2"> <button v-disable="data2.length>5">ボタン</button></p>
new Vue({
  el: '#app',
  data: function () {
    return {
      data1: 'サンプル',
      data2: 'サンプルデータ',
    }
  },
  directives: {
    disable: function (el, binding) {
      el.disabled = binding.value
    }
  }
})

入力の長さが5文字以下の時だけボタンを有効にします。

要素の幅を取得し直す

いまいち良い使い方が浮かばなかったので妙な例なんですけど、内容が変わると要素の幅を取得してデータにセットするディレクティブです。数値を使って移動するアンダーバーを付けるメニューを作ってみました。ウォッチャーを使っても同じことが出来るんですけどちょっとだけけシンプルに書けます!

コンポーネントのデータの内容が変わると数値を補正します。

<div id="app">
  <ul class="menu-box-list">
    <menu-box-item v-for="(val, idx) in menu"
:val="val" :idx="idx" key="val.id"
@set-size="setSize" v-model="current"></menu-box-item>
  </ul>
  <div class="menu-box-underbar" :style="barStyle"></div>
</div>
<script type="text/x-template" id="tpl-menu-box-item">
  <li class="menu-box-item" @click="$emit('input', idx)" v-width="idx"> {{ val.label }} </li>
</script>
var MenuBoxItem = Vue.extend({
  template: '#tpl-menu-box-item',
  props: ['val', 'idx'],
  directives: {
    width: function (el, binding, vnode) {
      Vue.nextTick(function () {
        var width = el.getBoundingClientRect().width
        vnode.context.$emit('set-size', binding.value, width)
      })
    }
  },
})
new Vue({
  el: '#app',
  data: {
    menu: [
      { id: 1, label: 'item1-x', width: 0 },
      { id: 2, label: 'item2-xx', width: 0 },
      { id: 3, label: 'item3-xxx', width: 0 },
      { id: 4, label: 'item4-xxxx', width: 0 },
    ],
    current: 0,
    temp: '',
  },
  computed: {
    barStyle: function () {
      var width = this.menu[this.current].width
      var left = !this.current ? 0 : this.menu.slice(0, this.current).reduce(function (a, b) {
        return { width: a.width + b.width }
      }).width
      return { width: width + 'px', transform: 'translateX(' + left + 'px)' }
    },
  },
  methods: {
    setSize: function (idx, width) {
      this.$set(this.menu[idx], 'width', width)
    },
    add: function () {
      var last = this.menu[this.menu.length - 1].id
      this.$set(this.menu, this.menu.length, { id: last + 1, label: this.temp || 'アイテム' + (last+1), width: 0 })
    },
    remove: function (idx) {
      if(this.menu.length > 1) {
        this.current = 0
        this.menu.splice(idx, 1)
      }
    }
  },
  components: { 'menu-box-item': MenuBoxItem }
});

ウォッチャーと似たように自分で持っているデータが変わったら update が実行されます。リスト部分の menu-box-item コンポーネントでは各アイテムの val を受け取っているので val の内容が変わったら更新されます。

リスト部分をコンポーネントにしているのは、他のデータが同じコンポーネント内にあると自分以外のデータが変わった時にも毎回 update されて無駄なためです。逆に絶対位置を取得したりとか全部同期して更新したい場合は同じコンポーネントにデータを置くといいですね。

まとめ

どんな時に使えるか少し分かってきたのでこれから色々作っていく時に「これはカスタムディレクティブを使ったら出来そう」ってなる事が出てくるかも。使いこなすととても便利そうです。そのうち何か簡単なプラグインも作ってみたいと思います(•ө•)ノ

Edited on 2017.04.17 Created on 2017.04.12 Vue.js JavaScript Webデザイン