# [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 :::