Custom Directives - 低レベルDOM操作の再利用

ロードマップ: Vue.js学習ロードマップ

What(何についてか)

Custom Directive は、Vue の組み込み directive(v-model, v-show など)に加えて、アプリケーション独自の directive を定義し、plain DOM element に対する低レベル DOM 操作を再利用する仕組み である。

Vue における再利用手段の役割分担は次のように整理できる。

  • Component: UI 構造と見た目の再利用
  • Composable: stateful logic の再利用
  • Custom Directive: direct DOM manipulation を伴う要素単位ロジックの再利用

代表例は、要素が DOM に挿入された時点で focus() を呼ぶ v-focus や、要素に class を付与する v-highlight である。

Why(なぜ必要か)

Vue は宣言的 UI を基本とするが、すべての振る舞いを props, template, composable だけで自然に表現できるわけではない。 特に以下のようなケースでは、DOM 要素そのものに対する命令的な操作が必要になる。

  • focus() の呼び出し
  • scrollIntoView() の呼び出し
  • IntersectionObserverResizeObserver の接続対象管理
  • 3rd party DOM ライブラリの要素適用
  • 特定要素の直接的な style / class / 属性制御

このようなケースで custom directive を使うと、DOM 直接操作をテンプレート上の再利用可能な API に変換できる。

一方で、通常の状態管理や class/style 切り替え、データ取得、複数要素をまたぐアプリケーションロジックは custom directive の担当ではない。これらは built-in directive, component, composable, state management で扱う方が自然である。

How(どう動くか)

基本定義

<script setup> では、v で始まる camelCase 変数をそのまま custom directive としてテンプレートで使える。

<script setup lang="ts">
const vHighlight = {
  mounted: (el: HTMLElement) => {
    el.classList.add('is-highlight')
  }
}
</script>
 
<template>
  <p v-highlight>This sentence is important!</p>
</template>

グローバルに使いたい場合は app.directive() で登録する。

app.directive('highlight', {
  mounted: (el) => {
    el.classList.add('is-highlight')
  }
})

いつ使うべきか

Custom directive は、目的の機能が direct DOM manipulation でしか自然に実現しにくい時に限定して使う

例えば v-focus は典型的な適用例である。

<script setup lang="ts">
const vFocus = {
  mounted: (el: HTMLInputElement) => el.focus()
}
</script>
 
<template>
  <input v-focus />
</template>

autofocus 属性と異なり、この directive はページ初回表示時だけでなく、Vue によって後から動的に挿入された要素にも適用できる。

Directive Hooks

Directive は object として定義し、要素に対する処理タイミングごとに hook を定義できる。

const vExample = {
  created(el: HTMLElement) {},
  beforeMount(el: HTMLElement) {},
  mounted(el: HTMLElement) {},
  beforeUpdate(el: HTMLElement) {},
  updated(el: HTMLElement) {},
  beforeUnmount(el: HTMLElement) {},
  unmounted(el: HTMLElement) {}
}

主な役割は次の通りである。

  • mounted: 要素が DOM に挿入された後の初期適用
  • updated: binding 値変化後の再適用
  • unmounted: event listener や observer の cleanup

component lifecycle に似ているが、主語は component ではなく bound element である点が重要である。

Hook Arguments

Directive hook では主に elbinding を使う。

  • el: directive が紐付いた実際の DOM 要素
  • binding: テンプレートから渡された情報の集合
  • vnode: Vue 内部の Virtual DOM ノード
  • prevVnode: update 系 hook で使える前回の VNode

binding は以下の情報を持つ。

  • value: 現在値
  • oldValue: 前回値(beforeUpdate, updated
  • arg: v-example:foofoo
  • modifiers: v-example.foo.bar{ foo: true, bar: true }
  • instance: directive を使用している component instance
  • dir: directive 定義オブジェクト

例えば以下の template:

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

は概念的に次の入力を与える。

{
  arg: 'foo',
  modifiers: { bar: true },
  value: baz,
  oldValue: /* previous baz */
}

modifiers は名前付きフラグ集合であり、通常の使い方では順序を持たない。 したがって v-example.foo.barv-example.bar.foo は実質的に同義である。

el.dataset による補助情報の保持

bindingvnode は読み取り専用として扱うべきである。 Hook 間で小さな情報を共有したい場合は、el.dataset を使う。

el.dataset は要素の data-* 属性を JS から操作する API である。

const vExample = {
  mounted(el: HTMLElement) {
    el.dataset.initialized = 'true'
  },
  updated(el: HTMLElement) {
    if (el.dataset.initialized === 'true') {
      // 初期化済みとして扱う
    }
  },
  unmounted(el: HTMLElement) {
    delete el.dataset.initialized
  }
}

これは、その DOM 要素専用の小さなメタデータ置き場として機能する。

Function Shorthand

mountedupdated に同じ処理を書くだけなら、object ではなく関数で短く書ける。 この shorthand は mountedupdated の両方に適用される。

<script setup lang="ts">
const vColor = (el: HTMLElement, binding: { value: string }) => {
  el.style.color = binding.value
}
</script>
 
<template>
  <div v-color="'red'">hello</div>
</template>

これは、要素に対して現在値を毎回同じ方法で反映するだけの directive に向いている。 一方で cleanup や hook ごとの差異が必要な場合は object 形式で定義する。

Object Literal による複数値の受け渡し

directive の値には単一値だけでなく object literal も渡せる。 これにより、複数の設定項目を 1 つの binding.value にまとめられる。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
const vDemo = (
  el: HTMLElement,
  binding: { value: { color: string; text: string } }
) => {
  el.style.color = binding.value.color
  el.textContent = binding.value.text
}

入力の使い分けは以下のように考えると整理しやすい。

  • arg: モードや対象種別
  • modifiers: on/off フラグ
  • value: 本体データ
  • object literal: 本体データが複数項目ある場合の構造化入力

Component への適用制約

Custom directive を component に対して使うのは非推奨である。

<MyComponent v-demo="test" />

directive は component そのものではなく、その root node に適用される。 このため、component の内部実装に依存して壊れやすい。

  • root element が変わると適用先が変わる
  • 複数 root を持つ component では directive が無視され warning が出る
  • directive を適用したい要素が root とは限らない

そのため、custom directive は plain DOM element 向けの道具 として使うべきである。

Composition API における位置づけ

Custom directive は、Vue の宣言的な仕組みで表現しきれない DOM 直接操作を局所化する手段である。

flowchart TD
    A["やりたい再利用は何か"] --> B["UI 構造や見た目"]
    A --> C["状態や副作用ロジック"]
    A --> D["要素への直接DOM操作"]
    B --> E["Component"]
    C --> F["Composable"]
    D --> G["Custom Directive"]

Options API → Composition API 差分(補足)

項目従来の見方Vue 3 / Composition API 時代の見方
DOM 再利用component 内部実装や mixin に寄せがちplain element に対して directive で局所化
ロジック共有mixin が混ざりやすいstateful logic は composable、DOM 直接操作は directive
適用先component にも何となく貼りたくなるdirective は基本 plain DOM element 向け
記法object 中心<script setup>vXxx / function shorthand が自然

Key Concepts

用語説明
Custom Directiveplain DOM element への低レベル DOM 操作を再利用する仕組み
Direct DOM Manipulationfocus(), scrollIntoView(), class/style 直接変更など命令的要素操作
Directive Hooksmounted, updated, unmounted など要素処理タイミングごとの hook
binding.valuedirective に渡された本体値
binding.argv-example:foofoo
binding.modifiersv-example.foo.bar のようなフラグ集合
Function Shorthandmounted / updated が同一処理の時に使う短縮記法
el.datasetdata-* 属性経由で要素に小さな補助情報を保持する API
vnodeVue 内部で描画対象を表す Virtual DOM ノード

実務での判断軸

  • 見た目や構造を再利用したいなら component を使う
  • 状態や副作用ロジックを再利用したいなら composable を使う
  • DOM 要素へ直接命令したいなら custom directive を使う
  • まず declarative に書けるか検討し、どうしても要素直接操作が必要な場合だけ directive を選ぶ

Custom directive は強力だが、Vue の基本設計から一段低いレイヤーへ降りる道具である。したがって、常用するよりも「必要な時に限定して正確に使う」設計が望ましい。