本文へジャンプ

カスタムディレクティブ

はじめに

コアに設定されているデフォルトのディレクティブのセット(v-modelv-show など)に加え、Vue では独自のカスタムディレクティブを登録することができます。

Vue におけるコードの再利用として 2 つの方法を紹介してきました:コンポーネントコンポーザブルです。コンポーネントはメインブロックの構築、コンポーザブルはステートフルなロジックの再利用に重点を置いています。一方、カスタムディレクティブは主にプレーンな要素に対する低レベルの DOM アクセスを伴うロジックを再利用することを目的としています。

カスタムディレクティブは、コンポーネントと同様のライフサイクルフックを含むオブジェクトとして定義されます。フックは、ディレクティブがバインドされている要素を受け取ります。以下は Vue によって DOM に挿入されたときに要素にクラスを追加するディレクティブの例です:

vue
<script setup>
// テンプレート内で v-highlight が有効になります
const vHighlight = {
  mounted: (el) => {
    el.classList.add('is-highlight')
  }
}
</script>

<template>
  <p v-highlight>This sentence is important!</p>
</template>
js
const highlight = {
  mounted: (el) => el.classList.add('is-highlight')
}

export default {
  directives: {
    // テンプレート内で v-highlight が有効になります
    highlight
  }
}
template
<p v-highlight>This sentence is important!</p>

This sentence is important!

<script setup> では、接頭辞が v で始まるキャメルケースの変数をカスタムディレクティブとして使用することができます。上の例では、vHighlight はテンプレート内で v-highlight として使用できます。

<script setup> を使用しない場合、カスタムディレクティブは directives オプションを使用して登録することができます:

js
export default {
  setup() {
    /*...*/
  },
  directives: {
    // テンプレート内で v-highlight が有効になります
    highlight: {
      /* ... */
    }
  }
}

コンポーネントと同様に、カスタムディレクティブもテンプレートで使用できるように登録する必要があります。上の例では、 directives オプションを使用してローカル登録しています。

また、カスタムディレクティブをアプリケーションレベルでグローバル登録することもよくあります:

js
const app = createApp({})

// 全てのコンポーネントで v-highlight が使用可能
app.directive('highlight', {
  /* ... */
})

カスタムディレクティブを使用するタイミング

カスタムディレクティブは DOM を直接操作することでしか必要な機能を実現できない場合にのみ使用してください。

一般的な例として、要素にフォーカスを当てる v-focus カスタムディレクティブがあります。

vue
<script setup>
// テンプレート内で v-focus が有効になります
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>
js
const focus = {
  mounted: (el) => el.focus()
}

export default {
  directives: {
    // テンプレート内で v-focus が有効になります
    focus
  }
}
template
<input v-focus />

このディレクティブは autofocus 属性よりも便利です。ページ読み込み時だけでなく、Vue によって要素が動的に挿入されたときにも機能するからです!

可能な限り v-bind のような組み込みディレクティブを使用した宣言的なテンプレートを推奨します。これらはより効率的で、サーバーレンダリングにも適しているためです。

ディレクティブフック

ディレクティブ定義オブジェクトは、いくつかのフック関数(すべて任意です)を提供することができます:

js
const myDirective = {
  // バインドされた要素の属性や
  // イベントリスナーが適用される前に呼ばれます
  created(el, binding, vnode) {
    // 引数の詳細については下を参照してください
  },
  // 要素が DOM に挿入される直前に呼ばれます
  beforeMount(el, binding, vnode) {},
  // バインドされた要素の親コンポーネントと
  // そのすべての子要素がマウントされたときに呼び出されます
  mounted(el, binding, vnode) {},
  // 親コンポーネントが更新される前に呼ばれます
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 親コンポーネントとすべての子要素が
  // 更新された後に呼ばれます
  updated(el, binding, vnode, prevVnode) {},
  // 親コンポーネントがアンマウントされる前に呼ばれます
  beforeUnmount(el, binding, vnode) {},
  // 親コンポーネントのアンマウント時に呼ばれます
  unmounted(el, binding, vnode) {}
}

フックの引数

ディレクティブフックには以下の引数が渡されます:

  • el: ディレクティブがバインドされている要素。DOM を直接操作するために使用されます。

  • binding: 以下のプロパティを含むオブジェクト。

    • value: ディレクティブに渡される値。例えば v-my-directive="1 + 1" の場合、値は 2 となります。
    • oldValue: 更新前の値。 beforeUpdateupdated でのみ利用可能です。値が変更されているかどうかに関係なく利用できます。
    • arg: ディレクティブに渡される引数がある場合に存在する引数。例えば v-my-directive:foo の場合、引数は "foo" となります。
    • modifiers: 修飾子がある時に、それを含むオブジェクト。例えば v-my-directive.foo.bar の場合、修飾子オブジェクトは { foo: true, bar: true } となります。
    • instance: ディレクティブが使用されるコンポーネントのインスタンス。
    • dir: ディレクティブ定義オブジェクト。
  • vnode: バインドされた要素を表す基礎となる VNode。

  • prevVnode: 前のレンダリングからバインドされた要素を表す VNode。beforeUpdateupdated フックでのみ利用可能です。

例として、次のようなディレクティブの使い方を考えてみましょう:

template
<div v-example:foo.bar="baz">

引数 binding は以下のような構造のオブジェクトになります:

js
{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` の値 */,
  oldValue: /* 前回の更新による `baz` の値 */
}

組み込みのディレクティブと同様に、カスタムディレクティブの引数も動的にできます。例えば:

template
<div v-example:[arg]="value"></div>

ここでは、ディレクティブの引数は、コンポーネントの状態にある arg プロパティに基づいてリアクティブに更新されます。

注意

el 以外のディレクティブの引数は、読み取り専用として扱い、決して変更しないようにしましょう。フック間で情報を共有する必要がある場合は、要素の dataset を通して行うことを推奨します。

関数のショートハンド

よくあることとして、カスタムディレクティブが mountedupdated に対して同じ動作をさせ、他のフックを必要としないことがあります。このような場合、ディレクティブを関数として定義することができます:

template
<div v-color="color"></div>
js
app.directive('color', (el, binding) => {
  // `mounted` と `updated` の両方で呼ばれます
  el.style.color = binding.value
})

オブジェクトリテラル

ディレクティブが複数の値を必要とする場合、JavaScript のオブジェクトリテラルを渡すこともできます。ディレクティブは有効な JavaScript の式はなんでも引き受けられることを覚えておきましょう。

template
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
js
app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
})

コンポーネントでの使い方

推奨しません

コンポーネントにカスタムディレクティブを使用することは推奨しません。コンポーネントに複数のルートノードがある場合、予期しない動作が発生する可能性があります。

コンポーネントで使用すると、フォールスルー属性と同様にカスタムディレクティブは常にコンポーネントのルートノードに適用されます。

template
<MyComponent v-demo="test" />
template
<!-- template of MyComponent -->

<div> <!-- v-demo directive will be applied here -->
  <span>My component content</span>
</div>

コンポーネントは潜在的に複数のルートノードを持つ可能性があることに注意してください。複数のルートを持つコンポーネントに適用された場合、ディレクティブは無視され、警告が投げられます。属性とは異なり、ディレクティブは v-bind="$attrs" で別の要素に渡すことができません。

カスタムディレクティブが読み込まれました