|
| 1 | +# Custom Directives |
| 2 | + |
| 3 | +## Intro |
| 4 | + |
| 5 | +In addition to the default set of directives shipped in core (like `v-model` or `v-show`), Vue also allows you to register your own custom directives. Note that in Vue, the primary form of code reuse and abstraction is components - however, there may be cases where you need some low-level DOM access on plain elements, and this is where custom directives would still be useful. An example would be focusing on an input element, like this one: |
| 6 | + |
| 7 | +<p class="codepen" data-height="300" data-theme-id="39028" data-default-tab="result" data-user="Vue" data-slug-hash="JjdxaJW" data-editable="true" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Custom directives: basic example"> |
| 8 | + <span>See the Pen <a href="https://codepen.io/team/Vue/pen/JjdxaJW"> |
| 9 | + Custom directives: basic example</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) |
| 10 | + on <a href="https://codepen.io">CodePen</a>.</span> |
| 11 | +</p> |
| 12 | +<script async src="https://static.codepen.io/assets/embed/ei.js"></script> |
| 13 | + |
| 14 | +When the page loads, that element gains focus (note: `autofocus` doesn't work on mobile Safari). In fact, if you haven't clicked on anything else since visiting this page, the input above should be focused now. Also, you can click on the `Rerun` button and input will be focused. |
| 15 | + |
| 16 | +Now let's build the directive that accomplishes this: |
| 17 | + |
| 18 | +```js |
| 19 | +const app = Vue.createApp({}) |
| 20 | +// Register a global custom directive called `v-focus` |
| 21 | +app.directive('focus', { |
| 22 | + // When the bound element is mounted into the DOM... |
| 23 | + mounted(el) { |
| 24 | + // Focus the element |
| 25 | + el.focus() |
| 26 | + } |
| 27 | +}) |
| 28 | +``` |
| 29 | + |
| 30 | +If you want to register a directive locally instead, components also accept a `directives` option: |
| 31 | + |
| 32 | +```js |
| 33 | +directives: { |
| 34 | + focus: { |
| 35 | + // directive definition |
| 36 | + mounted(el) { |
| 37 | + el.focus() |
| 38 | + } |
| 39 | + } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +Then in a template, you can use the new `v-focus` attribute on any element, like this: |
| 44 | + |
| 45 | +```html |
| 46 | +<input v-focus /> |
| 47 | +``` |
| 48 | + |
| 49 | +## Hook Functions |
| 50 | + |
| 51 | +A directive definition object can provide several hook functions (all optional): |
| 52 | + |
| 53 | +- `beforeMount`: called when the directive is first bound to the element and before parent component is mounted. This is where you can do one-time setup work. |
| 54 | + |
| 55 | +- `mounted`: called when the bound element's parent component is mounted. |
| 56 | + |
| 57 | +- `beforeUpdate`: called before the containing component's VNode is updated |
| 58 | + |
| 59 | +:::tip Note |
| 60 | +We'll cover VNodes in more detail [later](TODO:/render-function.html#The-Virtual-DOM), when we discuss [render functions](TODO:./render-function.html). |
| 61 | +::: |
| 62 | + |
| 63 | +- `updated`: called after the containing component's VNode **and the VNodes of its children** have updated. |
| 64 | + |
| 65 | +- `beforeUnmount`: called before the bound element's parent component is unmounted |
| 66 | + |
| 67 | +- `unmounted`: called only once, when the directive is unbound from the element and the parent component is unmounted. |
| 68 | + |
| 69 | +You can check the arguments passed into these hooks (i.e. `el`, `binding`, `vnode`, and `prevVnode`) in [Custom Directive API](TODO) |
| 70 | + |
| 71 | +### Dynamic Directive Arguments |
| 72 | + |
| 73 | +Directive arguments can be dynamic. For example, in `v-mydirective:[argument]="value"`, the `argument` can be updated based on data properties in our component instance! This makes our custom directives flexible for use throughout our application. |
| 74 | + |
| 75 | +Let's say you want to make a custom directive that allows you to pin elements to your page using fixed positioning. We could create a custom directive where the value updates the vertical positioning in pixels, like this: |
| 76 | + |
| 77 | +```vue-html |
| 78 | +<div id="dynamic-arguments-example" class="demo"> |
| 79 | + <p>Scroll down the page</p> |
| 80 | + <p v-pin="200">Stick me 200px from the top of the page</p> |
| 81 | +</div> |
| 82 | +``` |
| 83 | + |
| 84 | +```js |
| 85 | +const app = Vue.createApp({}) |
| 86 | + |
| 87 | +app.directive('pin', { |
| 88 | + mounted(el, binding) { |
| 89 | + el.style.position = 'fixed' |
| 90 | + // binding.value is the value we pass to directive - in this case, it's 200 |
| 91 | + el.style.top = binding.value + 'px' |
| 92 | + } |
| 93 | +}) |
| 94 | + |
| 95 | +app.mount('#dynamic-arguments-example') |
| 96 | +``` |
| 97 | + |
| 98 | +This would pin the element 200px from the top of the page. But what happens if we run into a scenario when we need to pin the element from the left, instead of the top? Here's where a dynamic argument that can be updated per component instance comes in very handy: |
| 99 | + |
| 100 | +```vue-html |
| 101 | +<div id="dynamicexample"> |
| 102 | + <h3>Scroll down inside this section ↓</h3> |
| 103 | + <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p> |
| 104 | +</div> |
| 105 | +``` |
| 106 | + |
| 107 | +```js |
| 108 | +const app = Vue.createApp({ |
| 109 | + data() { |
| 110 | + return { |
| 111 | + direction: 'right' |
| 112 | + } |
| 113 | + } |
| 114 | +}) |
| 115 | + |
| 116 | +app.directive('pin', { |
| 117 | + mounted(el, binding) { |
| 118 | + el.style.position = 'fixed' |
| 119 | + // binding.arg is an argument we pass to directive |
| 120 | + const s = binding.arg || 'top' |
| 121 | + el.style[s] = binding.value + 'px' |
| 122 | + } |
| 123 | +}) |
| 124 | + |
| 125 | +app.mount('#dynamic-arguments-example') |
| 126 | +``` |
| 127 | + |
| 128 | +Result: |
| 129 | + |
| 130 | +<p class="codepen" data-height="300" data-theme-id="39028" data-default-tab="result" data-user="Vue" data-slug-hash="YzXgGmv" data-editable="true" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Custom directives: dynamic arguments"> |
| 131 | + <span>See the Pen <a href="https://codepen.io/team/Vue/pen/YzXgGmv"> |
| 132 | + Custom directives: dynamic arguments</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) |
| 133 | + on <a href="https://codepen.io">CodePen</a>.</span> |
| 134 | +</p> |
| 135 | +<script async src="https://static.codepen.io/assets/embed/ei.js"></script> |
| 136 | + |
| 137 | +Our custom directive is now flexible enough to support a few different use cases. To make it even more dynamic, we can also allow to modify a bound value. Let's create an additional property `pinPadding` and bind it to the `<input type="range">` |
| 138 | + |
| 139 | +```vue-html{4} |
| 140 | +<div id="dynamicexample"> |
| 141 | + <h2>Scroll down the page</h2> |
| 142 | + <input type="range" min="0" max="500" v-model="pinPadding"> |
| 143 | + <p v-pin:[direction]="pinPadding">Stick me 200px from the {{ direction }} of the page</p> |
| 144 | +</div> |
| 145 | +``` |
| 146 | + |
| 147 | +```js{5} |
| 148 | +const app = Vue.createApp({ |
| 149 | + data() { |
| 150 | + return { |
| 151 | + direction: 'right', |
| 152 | + pinPadding: 200 |
| 153 | + } |
| 154 | + } |
| 155 | +}) |
| 156 | +``` |
| 157 | + |
| 158 | +Now let's extend our directive logic to recalculate the distance to pin on component update: |
| 159 | + |
| 160 | +```js{7-10} |
| 161 | +app.directive('pin', { |
| 162 | + mounted(el, binding) { |
| 163 | + el.style.position = 'fixed' |
| 164 | + const s = binding.arg || 'top' |
| 165 | + el.style[s] = binding.value + 'px' |
| 166 | + }, |
| 167 | + updated(el, binding) { |
| 168 | + const s = binding.arg || 'top' |
| 169 | + el.style[s] = binding.value + 'px' |
| 170 | + } |
| 171 | +}) |
| 172 | +``` |
| 173 | + |
| 174 | +Result: |
| 175 | + |
| 176 | +<p class="codepen" data-height="300" data-theme-id="39028" data-default-tab="result" data-user="Vue" data-slug-hash="rNOaZpj" data-editable="true" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Custom directives: dynamic arguments + dynamic binding"> |
| 177 | + <span>See the Pen <a href="https://codepen.io/team/Vue/pen/rNOaZpj"> |
| 178 | + Custom directives: dynamic arguments + dynamic binding</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) |
| 179 | + on <a href="https://codepen.io">CodePen</a>.</span> |
| 180 | +</p> |
| 181 | +<script async src="https://static.codepen.io/assets/embed/ei.js"></script> |
| 182 | + |
| 183 | +## Function Shorthand |
| 184 | + |
| 185 | +In previous example, you may want the same behavior on `mounted` and `updated`, but don't care about the other hooks. You can do it by passing the callback to directive: |
| 186 | + |
| 187 | +```js |
| 188 | +app.directive('pin', (el, binding) => { |
| 189 | + el.style.position = 'fixed' |
| 190 | + const s = binding.arg || 'top' |
| 191 | + el.style[s] = binding.value + 'px' |
| 192 | +}) |
| 193 | +``` |
| 194 | + |
| 195 | +## Object Literals |
| 196 | + |
| 197 | +If your directive needs multiple values, you can also pass in a JavaScript object literal. Remember, directives can take any valid JavaScript expression. |
| 198 | + |
| 199 | +```vue-html |
| 200 | +<div v-demo="{ color: 'white', text: 'hello!' }"></div> |
| 201 | +``` |
| 202 | + |
| 203 | +```js |
| 204 | +app.directive('demo', (el, binding) => { |
| 205 | + console.log(binding.value.color) // => "white" |
| 206 | + console.log(binding.value.text) // => "hello!" |
| 207 | +}) |
| 208 | +``` |
| 209 | + |
| 210 | +## Usage on Components |
| 211 | + |
| 212 | +In 3.0, with fragments support, components can potentially have more than one root nodes. This creates an issue when a custom directive is used on a component with multiple root nodes. |
| 213 | + |
| 214 | +To explain the details of how custom directives will work on components in 3.0, we need to first understand how custom directives are compiled in 3.0. For a directive like this: |
| 215 | + |
| 216 | +```vue-html |
| 217 | +<div v-demo="test"></div> |
| 218 | +``` |
| 219 | + |
| 220 | +Will roughly compile into this: |
| 221 | + |
| 222 | +```js |
| 223 | +const vFoo = resolveDirective('demo') |
| 224 | + |
| 225 | +return withDirectives(h('div'), [[vDemo, test]]) |
| 226 | +``` |
| 227 | + |
| 228 | +Where `vDemo` will be the directive object written by the user, which contains hooks like `mounted` and `updated`. |
| 229 | + |
| 230 | +`withDirectives` returns a cloned VNode with the user hooks wrapped and injected as VNode lifecycle hooks (see [Render Function](TODO:Render-functions) for more details): |
| 231 | + |
| 232 | +```js |
| 233 | +{ |
| 234 | + onVnodeMounted(vnode) { |
| 235 | + // call vDemo.mounted(...) |
| 236 | + } |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +**As a result, custom directives are fully included as part of a VNode's data. When a custom directive is used on a component, these `onVnodeXXX` hooks are passed down to the component as extraneous props and end up in `this.$attrs`.** |
| 241 | + |
| 242 | +This also means it's possible to directly hook into an element's lifecycle like this in the template, which can be handy when a custom directive is too involved: |
| 243 | + |
| 244 | +```vue-html |
| 245 | +<div v-on:vnodeMounted="myHook" /> |
| 246 | +``` |
| 247 | + |
| 248 | +This is consistent with the [attribute fallthrough behavior](component-props.html#non-prop-attributes). So, the rule for custom directives on a component will be the same as other extraneous attributes: it is up to the child component to decide where and whether to apply it. When the child component uses `v-bind="$attrs"` on an inner element, it will apply any custom directives used on it as well. |
0 commit comments