# カスタムディレクティブ
# 基本
Vue.js 本体で提供されているデフォルトのディレクティブ (v-model
や v-show
) に加えて、独自のカスタムディレクティブ (custom directives) を登録することも可能です。Vue ではコードの再利用や抽象化の基本形はコンポーネントです。しかしながら、単純な要素への低レベルな DOM のアクセスが必要なケースがあるかもしれません。こういったケースにカスタムディレクティブが役に立つことでしょう。以下のような input 要素へのフォーカスが1つの例として挙げられます:
ページを読み込むと、この要素にフォーカスが当たります (注意:autofucus
はモバイルの Safari で動きません)。実際、このページに訪れてから他に何もクリックしなければ、上記の input 要素にフォーカスが当たります。また、Rerun
ボタンをクリックしても、input 要素はフォーカスされます。
ここからこれを実現するディレクティブを実装しましょう:
const app = Vue.createApp({})
// `v-focus` と呼ばれるグローバルカスタムディレクティブを登録します
app.directive('focus', {
// 束縛されている要素が DOM にマウントされると...
mounted(el) {
// その要素にフォーカスを当てる
el.focus()
}
})
2
3
4
5
6
7
8
9
ディレクティブを代わりにローカルに登録したい場合、コンポーネントの directives
オプションで登録できます:
directives: {
focus: {
// ディレクティブの定義
mounted(el) {
el.focus()
}
}
}
2
3
4
5
6
7
8
そしてテンプレートでは、新規登録した v-focus
属性をどの要素に対しても以下のように利用できます:
<input v-focus />
# フック関数
ディレクティブの定義オブジェクトは、いくつかのフック関数を提供しています (全てオプション):
beforeMount
: ディレクティブが初めて要素に束縛された時、そして親コンポーネントがマウントされる前に呼ばれます。ここは1度だけ実行するセットアップ処理を行える場所です。mounted
: 束縛された要素の親コンポーネントがマウントされた時に呼ばれます。beforeUpdate
: 束縛された要素を含むコンポーネントの VNode が更新される前に呼ばれます。
Note
VNodes は後で詳細に扱います。描画関数を説明する時です。
updated
: 束縛された要素を含むコンポーネントの VNode とその子コンポーネントの VNode が更新された後に呼ばれます。beforeUnmount
: 束縛された要素の親コンポーネントがアンマウントされる前に呼ばれます。unmounted
: ディレクティブが要素から束縛を解かれた時、また親コンポーネントがアンマウントされた時に1度だけ呼ばれます。
これらのフック関数に渡される引数 (すなわち、el
, binding
, vnode
, prevVnode
) は、カスタムディレクティブ API にて確認できます。
# 動的なディレクティブ引数
ディレクティブの引数は動的にできます。例えば、v-mydirective:[argument]="value"
において、argument
はコンポーネントインスタンスの data プロパティに基づいて更新されます! これにより、私たちのアプリケーション全体を通した利用に対して、カスタムディレクティブは柔軟になります。
ページの固定位置に要素をピン留めするカスタムディレクティブを考えてみましょう。引数の値が縦方向のピクセル位置を更新するカスタムディレクティブを以下のように作成することができます:
<div id="dynamic-arguments-example" class="demo">
<p>Scroll down the page</p>
<p v-pin="200">Stick me 200px from the top of the page</p>
</div>
2
3
4
const app = Vue.createApp({})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
// binding.value はディレクティブに渡した値です - このケースの場合、200 です
el.style.top = binding.value + 'px'
}
})
app.mount('#dynamic-arguments-example')
2
3
4
5
6
7
8
9
10
11
これにより、ページの上端から 200px の位置に要素を固定できます。しかし、上端からではなく左端から要素をピン留めする必要があるシナリオが出てきたらどうでしょうか?ここでコンポーネントインスタンスごとに更新できる動的引数がとても便利です:
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
2
3
4
const app = Vue.createApp({
data() {
return {
direction: 'right'
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
// binding.arg がディレクティブに渡した引数です
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
app.mount('#dynamic-arguments-example')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
結果:
このカスタムディレクティブは、いくつかの違うユースケースをサポートできるほど柔軟になりました。さらに動的にするには、束縛した値を修正できるようにすれば良いでしょう。pinPadding
という追加のプロパティを作成して、<input type="range">
に束縛してみましょう。
<div id="dynamicexample">
<h2>Scroll down the page</h2>
<input type="range" min="0" max="500" v-model="pinPadding">
<p v-pin:[direction]="pinPadding">Stick me 200px from the {{ direction }} of the page</p>
</div>
2
3
4
5
const app = Vue.createApp({
data() {
return {
direction: 'right',
pinPadding: 200
}
}
})
2
3
4
5
6
7
8
さらにコンポーネントの更新に従ってピンとの距離を再計算できるようにディレクティブのロジックを拡張しましょう:
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
},
updated(el, binding) {
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
2
3
4
5
6
7
8
9
10
11
結果:
# 関数による省略記法
前回の例では、mounted
と updated
に同じ振る舞いを欲しかったでしょう。しかし、その他のフック関数を気にしてはいけません。ディレクティブにコールバックを渡すことで実現できます:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
2
3
4
5
# オブジェクトリテラル
ディレクティブに複数の値が必要な場合、JavaScript のオブジェクトリテラルを渡すこともできます。覚えておいてください、ディレクティブはあらゆる妥当な JavaScript 式を取ることができます。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
2
3
4
# コンポーネントにおける使用法
3.0 ではフラグメントがサポートされているため、コンポーネントは潜在的に1つ以上のルートノードを持つことができます。これは複数のルートノードを持つ1つのコンポーネントにカスタムディレクティブが使用された時に、問題を引き起こします。
3.0 のコンポーネント上でどのようにカスタムディレクティブが動作するかを詳細に説明するために、3.0 においてカスタムディレクティブがどのようにコンパイルされるのかをまずは理解する必要があります。以下のようなディレクティブは:
<div v-demo="test"></div>
おおよそ以下のようにコンパイルされます:
const vDemo = resolveDirective('demo')
return withDirectives(h('div'), [[vDemo, test]])
2
3
ここで vDemo
はユーザによって記述されたディレクティブオブジェクトで、それは mounted
や updated
のフック関数を含みます。
withDirectives
は複製した VNode を返します。複製された VNode は VNode のライフサイクルフック (詳細は描画関数を参照) としてラップ、注入されたユーザのフック関数を持ちます:
{
onVnodeMounted(vnode) {
// vDemo.mounted(...) を呼びます
}
}
2
3
4
5
結果として、VNode のデータの一部としてカスタムディレクティブは全て含まれます。カスタムディレクティブがコンポーネントで利用される場合、これらの onVnodeXXX
フック関数は無関係な props としてコンポーネントに渡され、最終的に this.$attrs
になります。
これは以下のようなテンプレートのように、要素のライフサイクルに直接フックできることを意味しています。これはカスタムディレクティブが複雑すぎる場合に便利です:
<div @vnodeMounted="myHook" />
これは 属性のフォールスロー と一貫性があります。つまり、コンポーネントにおけるカスタムディレクティブのルールは、その他の異質な属性と同じです: それをどこにまた適用するかどうかを決めるのは、子コンポーネント次第です。子コンポーネントが内部の要素に v-bind="$attrs"
を利用している場合、あらゆるカスタムディレクティブもその要素に適用されます。