Vue.jsでカスタムディレクティブを実装する

## はじめに
Vue.jsには、`v-if`や`v-model`、`v-for`などのvから始まる特別な属性があります。これらは**ディレクティブ**と呼ばれ、与えられたデータをもとにスマートなDOM操作を行ってくれます。例えば`v-if`ディレクティブは、与えられたデータを真偽値で評価し、その結果によって、結びついたDOM要素を描画したり削除したりしてくれます。

ディレクティブはDOMの操作において大変便利な機能ですが、Vueがデフォルトで用意してくれているディレクティブでは対応しきれないようなDOM操作を行いたい場面も当然ありますよね。
そのような少々込み入ったDOM操作を行う場合、従来のように対象DOM要素を検索してープロパティを変更してーなどと個別にやってもいいのですが、できればVueのアプリケーションの中に再利用可能な状態で取り込めると便利です。
その需要を満たすべく、Vueには**カスタムディレクティブ**という機能があります。

今回はそのカスタムディレクティブについて、実際に手を動かしながら学習した内容を書いていこうと思います。

## カスタムディレクティブの定義
カスタムディレクティブは、平たく言えばオリジナルのディレクティブを登録できるVueの機能です。
定義したカスタムディレクティブを属性値としてDOM要素に付与することで、特定の振る舞いを持たせることができます。

ディレクティブの定義には、`Vue.directive`というAPIを使用します。
今回は、付与した要素内のテキストを強調表示するディレクティブ`v-important`を定義してみます。

```js
Vue.directive('important', {
 bind(el) {
    el.style.color = 'red';
    el.style['font-weight'] = 'bold';
   }
});
```
第一引数には定義するディレクティブの名称を、第二引数には、DOMの振る舞いを決定する**ディレクティブ定義オブジェクト**を指定します。
ここでは、「ディレクティブが対象の要素に紐づいた時に、要素内のテキストを赤色にして文字を太くする」という振る舞いを定義しています。

定義したカスタムディレクティブを使用してDOM操作を行うには、ディレクティブ名の先頭に`v-`をくっつけて、要素に属性として付与すればOKです。

```html
<div id="app">
  <p>ここ<span v-important>テストに出る</span>ぞー。</p>
</div>
```
結果はこのようになります。

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="shironeko-shobo" data-slug-hash="eqBMev" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="eqBMev">
  <span>See the Pen <a href="https://codepen.io/shironeko-shobo/pen/eqBMev/">
  eqBMev</a> by shironeko-shobo (<a href="https://codepen.io/shironeko-shobo">@shironeko-shobo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

オリジナルのディレクティブがとても簡単に作成できました！

### フック関数
ディレクティブ定義オブジェクトではいくつかの**フック関数**を指定でき、DOM操作を行う関数の実行タイミングを定義できます。
先ほどの例で使用したbindがその1つで、これはディレクティブが初めて対象要素に紐づいた時に１回だけ関数を呼び出す、というフック関数です。

フック関数には以下のようなものがあります。

- `bind` - ディレクティブが初めて対象要素に紐づいた時に１回だけ関数を呼び出す
- `inserted` - 紐づいた要素が親要素に挿入された時に関数を実行する
- `update` - ディレクティブの値の変化等に伴って、紐づいた要素を含む仮想ノードが更新される際に関数を実行する
- `componentUpdated` - 紐づく要素を含むコンポーネントの仮想ノードと、子コンポーネントの仮想ノードが更新された後に関数を実行する
- `unbind` - ディレクティブが紐づく要素から取り除かれた時に、１度だけ関数を実行する

このうち、`update`は値の変化が起きていない場合でも関数を実行してしまう場合があるため、関数の処理の初めで変化前と変化後の値の比較を行うと良さそうです。
また、`unbind`は主に`bind`で登録したイベントリスナーの解除などに使わることが多いみたいです。

これらのフック関数を利用することで、実装したいディレクティブに合わせた任意のDOM操作を行うことができます。

### フック関数の引数
先の例では、フック関数`bind`の引数として`el`を渡していました。
これはディレクティブが紐づく要素を表していて、DOMの操作では主にこいつをいじることになります。

フック関数の引数には、`el`を始め以下のようなものがあります。

- `el` - ディレクティブが紐づく要素
- `binding` - ディレクティブの名前や、渡される値などの情報を含むオブジェクト
- `vnode` - Vueのコンパイラが生成する仮想ノード
- `oldVnode` - updateとcomponentUpdatedで使用できる、更新前の仮想ノード

このうち、`binding`オブジェクトには以下のようなプロパティが含まれます。

- `name` - 「v-」の接頭辞を除いたディレクティブの名前
- `value` - ディレクティブに渡される値
- `oldValue` - updateとcomponentUpdatedで使用できる更新前の値。
- `expression` - ディレクティブに渡された式の文字列表現
- `arg` - ディレクティブに渡される引数
- `modifiers` - ディレクティブに付与された修飾子のオブジェクト

例えば、要素にディレクティブを付与する際に

```vue
<p v-test:foo.bar="3 * 3"><p>
```

と記述すると、各プロパティは以下のようになります。

- binding.name => 'test'
- binding.value => 9
- binding.expression => '3 * 3'
- binding.arg => 'bar'
- binding.modifiers => { bar: true }

## プロパティを活用したディレクティブの定義
bindingのプロパティを活用して、input要素に指定したデフォルト値を設定できるディレクティブ「`v-default-input`」を実装してみます。
しかし、ただデフォルト値を設定するだけでは面白くないので、「フォームからフォーカスが外れた時値が空だった場合にデフォルト値を入れ直す」機能も持たせます。
また、ページの初期描画時に自動でフォーカスがあたるようにするディレクティブ引数「:auto-focus」も設定可能にします。

```js
Vue.directive('default-input', {
  inserted: (el, binding) => {
    // デフォルト値の設定
    el.value = binding.value;
    
    // フォーカスが外れた際に値をデフォルト値を再入力
    el.addEventListener('blur', () => {
      if (el.value === '') el.value = binding.value;
    });

    // 引数[:auto-focus]があった場合に要素に自動でフォーカスを当てる
    if (binding.arg === 'auto-focus') el.focus();
  }
});
```

```html
<div id="app">
  <input id="test" type="text" v-default-input:auto-focus="'消すと復活するぞ！'">
</div>
```

ディレクティブに与えた値と引数を使って、多機能なディレクティブを実装できました！
ちなみに、今回のフック関数に`inserted`を用いているのは、`bind`のタイミングでは対象の要素にフォーカスをあてることができないためです。

また、要素にディレクティブを付与している箇所で、値を`'消すと復活するぞ！'`とシングルクォーテーションで囲っているのは、**ディレクティブが基本的にJavaScriptの式を期待する**ためです。
(※ そのため、値には`{ test: 'foo', sample: 'bar' }`といったオブジェクトリテラルを渡すことも可能です。)

コード全体と実際の動作は以下から確認してください。

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="shironeko-shobo" data-slug-hash="YmXPvB" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="YmXPvB">
  <span>See the Pen <a href="https://codepen.io/shironeko-shobo/pen/YmXPvB/">
  YmXPvB</a> by shironeko-shobo (<a href="https://codepen.io/shironeko-shobo">@shironeko-shobo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


## 動的にディレクティブ引数を扱う
ディレクティブの引数は、以下のように記述をすることで動的に設定できます。

```vue
<div v-test:[bar]></div>
```

この`bar`の箇所には、ディレクティブが紐づいた要素を含むコンポーネントの`data`や`computed`の値などを設定でき、その値に応じて柔軟に要素の挙動を制御することが可能になります。

例として、紐づいた要素を直径50pxの玉にするディレクティブ`v-ball`を作ってみます。
このディレクティブには、玉の色を決定する引数`color`を持たせ、また、色の変化をスムーズにする修飾子`smooth`を設定可能にします。

```js
Vue.directive('ball', {
  bind: (el, binding) => {
    el.style.display = 'inline-block';
    el.style['background-color'] = binding.arg || 'black';
    el.style.width = '50px';
    el.style.height = '50px';
    el.style['border-radius'] = '50%';
    if (binding.modifiers.smooth) el.style.transition = '0.5s background-color ease';
  },
  update: (el, binding) => {
    el.style['background-color'] = binding.arg || 'black';
  }
});

const vm = new Vue({
  el: '#app',
  data: {
    //　動的な引数として与えるデータを保持
    color: 'black'
  },
  methods: {
    updateColor(el) {
      this.color = el.target.value;
    }
  }
});
```

```html
<div id="app">
  <div v-ball:[color].smooth></div>
  <div>
    <input type="text" placeholder="white or #FFFFFF or rgb(255,255,255)"  @input="updateColor">
  </div>
</div>
```

サンプルのフォームに実際に色名を入力して、動作を確認してみてください。

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="shironeko-shobo" data-slug-hash="KONQOP" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="KONQOP">
  <span>See the Pen <a href="https://codepen.io/shironeko-shobo/pen/KONQOP/">
  KONQOP</a> by shironeko-shobo (<a href="https://codepen.io/shironeko-shobo">@shironeko-shobo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

できましたね！今回の例のように、1つのディレクティブ定義オブジェクトに２つのフック関数を持たせることも可能です。
動的な引数として設定するデータはコンポーネント単位で用意できるため、使う場所に応じて軽い変化を与えたい場合に役立てられそうですね。

## コンポーネント別に登録する
`Vue.directive`APIはカスタムディレクティブをグローバルに登録しますが、これをローカル(個々のコンポーネント)に登録したい場合は、コンポーネントの`directives`オプションを使用することで実現できます。

```js
// ディレクティブあり
Vue.component('hasDirective', {
  template: `
  <div id="app">
   <p>ここ<span v-important>テストに出る</span>ぞー。</p>
  </div>
  `,
  directives: {
    important: {
      bind(el) {
        el.style.color = 'red';
        el.style['font-weight'] = 'bold';
      }
    }
  }
});

// ディレクティブなし
Vue.component('noDirective', {
  template: `
  <div id="app">
   <p>ここ<span v-important>テストに出る</span>ぞー。</p>
  </div>
  `
});

const vm = new Vue({
  el: '#app'
});
```
```html
<div id="app">
  <has-directive></has-directive>
  <no-directive></no-directive>
</div>
```

ここでは、最初の例で用いた`v-important`ディレクティブを流用しています。
`directives`オプションを指定した`hasDirective`コンポーネントの文字だけ、強調表示されていることが分かりますね。

以下のサンプルを見てみると、ディレクティブオプションで
<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="shironeko-shobo" data-slug-hash="qeqYmw" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="qeqYmw">
  <span>See the Pen <a href="https://codepen.io/shironeko-shobo/pen/qeqYmw/">
  qeqYmw</a> by shironeko-shobo (<a href="https://codepen.io/shironeko-shobo">@shironeko-shobo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

ただ、カスタムディレクティブは特定のコンポーネントに依存しない汎用的な機能を持つことが多いため、アプリケーション全体を通して利用出来るよう、グローバルに登録することが一般的かと思われます。


## 終わりに
カスタムディレクティブは、アプリケーションで多用するDOMの操作を共通化できるとても便利な機能です。

DOM要素に動きをもたせたいけど、個別にコンポーネント化するまでもないかな...といった場面できっと役にたつはず！
使いどころを見極めて、今後の開発にも積極的に活かしていきたいと思います。