# [Vue] Custom Directives 自定義指令
###### tags: `Vue.js`
* 複用操作 DOM 元素的邏輯
## Directive Hooks
| Vue 2 | Vue 3 | 說明 |
| ---------------- | ------------------ | --- |
| 無 | created | 在綁定 element 的 attribute 前或 event listeners 應用前 |
| bind | beforeMount | 在 element 插入到 DOM 之前。只調用一次 |
| inserted | mounted | 在綁定 element 的父組件及自己所有子組件都掛載完成後 |
| 無 | beforeUpdate | 父組件更新前 |
| update | 移除,改用 updated | |
| componentUpdated | updated | 父組件及自己所有子組件都更新後 |
| 無 | beforeUnmount | 父組件卸載前 |
| unbind | unmounted | 父組件卸載後。只調用一次 |
### Hook 參數
* `el`:element,可用於直接操作 DOM。`el` 以外的參數都是 read-only,不要修改
* `binding`
* `value`:傳給 directive 的值
* `oldValue`:之前的值
* 只能在 `beforeUpdate` 和 `updated` 中使用
* 無論值是否有變更皆可使用
* `arg`:傳給 directive 的參數
* `modifiers`:修飾符
* `instance`:組件實例
* `dir`:directive 定義的 object 的內容
* `vnode`:綁定 element 的底層 VNode
* `prevVnode`:先前渲染中綁定 element 的 VNode
* 只能在 `beforeUpdate` 和 `updated` 中使用
## 寫法
* 指令的行為,以寫在 `<script setup>` 為例
```javascript
import { ref } from 'vue';
const msg = ref('Hello');
const vMyDirective = {
mounted(el, binding) {
console.log(binding.value); // Hello
console.log(binding.arg); // foo
console.log(binding.modifiers); // {bar: true, baz: true}
console.log(Object.keys(binding.dir)); // ['mounted', 'updated', 'unmounted']
},
updated(el, binding, vnode, preVnode) {
console.log('el', el);
console.log('binding', binding);
console.log('oldValue', binding.oldValue);
console.log('vnode', vnode);
console.log('preVnode', preVnode);
},
unmounted() {
// ...
}
};
```
* 在 `<template>` 內使用
```htmlembedded
<p v-my-directive:foo.bar.baz="msg">Custom directives</p>
```
### 簡寫
若 `mounted` 和 `updated` 是相同的行為,且不需要其他 Hooks 時
```javascript
const vMyDirective = (el, binding) => {
console.log(binding.value); // Hello
}
```
## 註冊
### component 內
* 指令寫在 `<script setup>`,變數名稱以 `v` 開頭
```javascript
import { ref } from 'vue';
const msg = ref('Hello');
const vFocus = (el) => el.focus();
```
* 在 `<template>` 內使用
```htmlembedded
<input type="text" v-focus v-model="msg">
<p>{{ msg }}</p>
```
* 如果是用 `<script>`,將指令寫在 `directives` 內
:::spoiler 範例
```htmlembedded
import { ref } from 'vue';
export default {
setup() {
const msg = ref('Hello');
return {
msg,
};
},
directives: {
focus(el) {
el.focus();
},
},
};
```
:::
### 全域註冊
* Vue 3 寫法
```javascript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { focus, timeFormat, img } from '@/directives';
createApp(App)
.directive('focus', focus)
.directive('timeFormat', timeFormat)
.directive('img', img)
.mount('#app');
```
* Vue 2 寫法
:::spoiler 範例
```javascript
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value;
},
});
```
:::
## 範例
### 自動 focus
* 指令
```javascript
const focus = (el) => el.focus();
```
* 使用方式
```htmlembedded
<input type="text" v-model="msg" v-focus>
```
### 格式化時間
* 指令
* 使用 [Day.js](https://day.js.org/) 進行格式化
```javascript
import dayjs from 'dayjs';
const timeFormat = (el, binding) => {
const result = dayjs(binding.value).format('YYYY-MM-DD');
el.textContent = result;
}
```
* 使用方式
* 假設 `timestamp` 值為 `0`,最終顯示結果為 `1970-01-01`
```htmlembedded
<p v-time-format="timestamp"></p>
```
### 載入圖片失敗時顯示預設圖
* 指令
```javascript
import altDefault from '@/assets/images/altDefault.webp';
import altAvatar from '@/assets/images/altAvatar.webp';
import altProduct from '@/assets/images/altProduct.webp';
const img = (el, binding) => {
const elImg = el;
const imgUrl = binding.value;
const altImgType = {
avatar: altAvatar,
product: altProduct,
}
const altImg = altImgType[binding.arg] ?? altDefault;
if (imgUrl) {
const img = new Image();
img.src = imgUrl;
img.onload = () => {
elImg.src = imgUrl;
};
img.onerror = () => {
elImg.src = altImg;
};
} else {
elImg.src = altImg;
}
};
```
* 使用方式
* 假設值為
```javascript
const imgList = [
{
type: 'avatar',
url: 'https://picsum.photos/id/237/200/300',
},
{
type: 'product',
url: 'https://picsum.photos/id/238/200/300',
},
{
type: 'news',
url: 'https://picsum.photos/id/239/200/300',
},
];
```
```htmlembedded
<img
v-for="item in imgList"
:key="item.url"
v-img:[item.type]="item.url"
:alt="item.type"
>
```
## 參考資料
* [Vue 3 官方文件:Custom Directives](https://vuejs.org/guide/reusability/custom-directives.html "Custom Directives | Vue.js")
* [Vue 2 官方文件:Custom Directives](https://v2.vuejs.org/v2/guide/custom-directive "Custom Directives | Vue.js")
* [Vue 3 Migration Guide:Custom Directives](https://v3-migration.vuejs.org/breaking-changes/custom-directives.html "Custom Directives | Vue 3 Migration Guide")
* [[重構倒數第13天] - Vue3定義自己的模板語法](https://ithelp.ithome.com.tw/articles/10266580 "[重構倒數第13天] - Vue3定義自己的模板語法 by Mike")
---
:::info
建立日期:2023-08-15
更新日期:2024-03-02
:::