Skip to content

Commit 68175e9

Browse files
committed
custom directives
1 parent 8d4a6cb commit 68175e9

File tree

2 files changed

+143
-131
lines changed

2 files changed

+143
-131
lines changed

src/.vitepress/theme/styles/inline-demo.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
margin-top: 10px;
3131
}
3232

33+
.demo input:focus {
34+
outline: 1px solid blue;
35+
}
36+
3337
.demo select {
3438
/* this was set by normalize.css */
3539
-webkit-appearance: listbox;
Lines changed: 139 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,193 @@
11
# Custom Directives
22

3+
<script setup>
4+
const vFocus = {
5+
mounted: el => el.focus()
6+
}
7+
</script>
8+
39
## Introduction
410

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:
11+
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.
612

7-
<!-- <common-codepen-snippet title="Custom directives: basic example" slug="JjdxaJW" :preview="false" /> -->
13+
We have introduced two forms of code reuse in Vue: [components](/guide/essentials/component-basics.html) and [composables](./composables). Components are the main building blocks, while composables are focused on reusing stateful logic. Custom directives, on the other hand, are mainly intended for reusing logic that involves low-level DOM access on plain elements.
814

9-
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+
A custom directive is defined as an object containing lifecycle hooks similar to those of a component. The hooks receive the element the directive is bound to. Here is an example of a directive that focuses an input when the element is inserted into the DOM by Vue:
1016

11-
Now let's build the directive that accomplishes this:
17+
<div class="composition-api">
1218

13-
```js
14-
const app = Vue.createApp({})
15-
// Register a global custom directive called `v-focus`
16-
app.directive('focus', {
17-
// When the bound element is mounted into the DOM...
18-
mounted(el) {
19-
// Focus the element
20-
el.focus()
21-
}
22-
})
19+
```vue
20+
<script setup>
21+
// enables v-focus in templates
22+
const vFocus = {
23+
mounted: (el) => el.focus()
24+
}
25+
</script>
26+
27+
<template>
28+
<input v-focus />
29+
</template>
2330
```
2431

25-
If you want to register a directive locally instead, components also accept a `directives` option:
32+
</div>
33+
34+
<div class="options-api">
2635

2736
```js
28-
directives: {
29-
focus: {
30-
// directive definition
31-
mounted(el) {
32-
el.focus()
33-
}
37+
const focus = {
38+
mounted: (el) => el.focus()
39+
}
40+
41+
export default {
42+
directives: {
43+
// enables v-focus in template
44+
focus
3445
}
3546
}
3647
```
3748

38-
Then in a template, you can use the new `v-focus` attribute on any element, like this:
39-
4049
```vue-html
4150
<input v-focus />
4251
```
4352

44-
## Directive Hooks
45-
46-
A directive definition object can provide several hook functions (all optional):
47-
48-
- `created`: called before the bound element's attributes or event listeners are applied. This is useful in cases where the directive needs to attach event listeners that must be called before normal `v-on` event listeners.
49-
50-
- `beforeMount`: called when the directive is first bound to the element and before parent component is mounted.
51-
52-
- `mounted`: called when the bound element's parent component is mounted.
53+
</div>
5354

54-
- `beforeUpdate`: called before the containing component's VNode is updated
55+
<div class="demo">
56+
<input v-focus placeholder="This should be focused" />
57+
</div>
5558

56-
:::tip Note
57-
We'll cover VNodes in more detail [later](/guide/advanced/render-function.html#the-virtual-dom-tree), when we discuss render functions.
58-
:::
59+
Assuming you haven't clicked elsewhere on the page, the input above should be auto-focused. This directive is more useful than the `autofocus` attribute because it works not just on page load - it also works when the element is dynamically inserted by Vue.
5960

60-
- `updated`: called after the containing component's VNode **and the VNodes of its children** have updated.
61+
<div class="composition-api">
6162

62-
- `beforeUnmount`: called before the bound element's parent component is unmounted
63+
In `<script setup>`, any camelCase variable that starts with the `v` prefix can be used as a custom directive. In the example above, `vFocus` can be used in the template as `v-focus`.
6364

64-
- `unmounted`: called only once, when the directive is unbound from the element and the parent component is unmounted.
65+
If not using `<script setup>`, custom directives can be registered using the `directives` option:
6566

66-
You can check the arguments passed into these hooks (i.e. `el`, `binding`, `vnode`, and `prevVnode`) in [Custom Directive API](/api/application.html#app-directive)
67+
```js
68+
export default {
69+
setup() {
70+
/*...*/
71+
},
72+
directives: {
73+
// enables v-focus in template
74+
focus: {
75+
/* ... */
76+
}
77+
}
78+
}
79+
```
6780

68-
### Dynamic Directive Arguments
81+
</div>
6982

70-
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.
83+
<div class="options-api">
7184

72-
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:
85+
Similar to components, custom directives must be registered so that they can be used in templates. In the example above, we are using local registration via the `directives` option.
7386

74-
```vue-html
75-
<div id="dynamic-arguments-example" class="demo">
76-
<p>Scroll down the page</p>
77-
<p v-pin="200">Stick me 200px from the top of the page</p>
7887
</div>
79-
```
88+
89+
It is also common to globally register custom directives at the app level:
8090

8191
```js
82-
const app = Vue.createApp({})
92+
const app = createApp({})
8393

84-
app.directive('pin', {
85-
mounted(el, binding) {
86-
el.style.position = 'fixed'
87-
// binding.value is the value we pass to directive - in this case, it's 200
88-
el.style.top = binding.value + 'px'
89-
}
94+
// make v-focus usable in all components
95+
app.directive('focus', {
96+
/* ... */
9097
})
91-
92-
app.mount('#dynamic-arguments-example')
9398
```
9499

95-
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:
100+
:::tip
101+
Custom directives should only be used when the desired functionality can only be achieved via direct DOM manipulation. Prefer declarative templating using built-in directives such as `v-bind` when possible because they are more efficient and server-rendering-friendly.
102+
:::
103+
104+
## Directive Hooks
96105

97-
```vue-html
98-
<div id="dynamicexample">
99-
<h3>Scroll down inside this section ↓</h3>
100-
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
101-
</div>
102-
```
106+
A directive definition object can provide several hook functions (all optional):
103107

104108
```js
105-
const app = Vue.createApp({
106-
data() {
107-
return {
108-
direction: 'right'
109-
}
109+
const myDir = {
110+
// called before bound element's attributes
111+
// or event listeners are applied
112+
created(el binding, vnode, prevVnode) {
113+
// see below for details on arguments
114+
},
115+
// called right before the element is inserted into the DOM.
116+
beforeMount() {},
117+
// called when the bound element's parent component
118+
// and all its children are mounted.
119+
mounted() {},
120+
// called before the parent component is updated
121+
beforeUpdate() {},
122+
// called after the parent component and
123+
// all of its children have updated
124+
updated() {},
125+
// called before the parent component is unmounted
126+
beforeUnmount() {},
127+
// called when the parent component is unmounted
128+
unmounted() {}
110129
}
111-
})
130+
}
131+
```
112132

113-
app.directive('pin', {
114-
mounted(el, binding) {
115-
el.style.position = 'fixed'
116-
// binding.arg is an argument we pass to directive
117-
const s = binding.arg || 'top'
118-
el.style[s] = binding.value + 'px'
119-
}
120-
})
133+
### Hook Arguments
121134

122-
app.mount('#dynamic-arguments-example')
123-
```
135+
Directive hooks are passed these arguments:
124136

125-
Result:
137+
- `el`: the element the directive is bound to. This can be used to directly manipulate the DOM.
126138

127-
<!-- <common-codepen-snippet title="Custom directives: dynamic arguments" slug="YzXgGmv" :preview="false" /> -->
139+
- `binding`: an object containing the following properties.
140+
- `value`: The value passed to the directive. For example in `v-my-directive="1 + 1"`, the value would be `2`.
141+
- `oldValue`: The previous value, only available in `beforeUpdate` and `updated`. It is available whether or not the value has changed.
142+
- `arg`: The argument passed to the directive, if any. For example in `v-my-directive:foo`, the arg would be `"foo"`.
143+
- `modifiers`: An object containing modifiers, if any. For example in `v-my-directive.foo.bar`, the modifiers object would be `{ foo: true, bar: true }`.
144+
- `instance`: The instance of the component where directive is used.
145+
- `dir`: the directive definition object.
128146

129-
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">`
147+
- `vnode`: the underlying VNode representing the bound element.
148+
- `prevNode`: the VNode representing the bound element from the previous render. Only available in the `beforeUpdate` and `updated` hooks.
130149

131-
```vue-html{4}
132-
<div id="dynamicexample">
133-
<h2>Scroll down the page</h2>
134-
<input type="range" min="0" max="500" v-model="pinPadding">
135-
<p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction || 'top' }} of the page</p>
136-
</div>
150+
As an example, the following directive usage:
151+
152+
```vue-html
153+
<div v-example:foo.bar="baz">
137154
```
138155

139-
```js{5}
140-
const app = Vue.createApp({
141-
data() {
142-
return {
143-
direction: 'right',
144-
pinPadding: 200
145-
}
146-
}
147-
})
156+
The `binding` argument would be an object in the shape of:
157+
158+
```js
159+
{
160+
arg: 'foo',
161+
modifiers: { baz: true },
162+
value: /* value of `baz` */,
163+
oldValue: /* value of `baz` from previous update */
164+
}
148165
```
149166

150-
Now let's extend our directive logic to recalculate the distance to pin on component update:
167+
Similar to built-in directives, custom directive arguments can be dynamic. For example:
151168

152-
```js{7-10}
153-
app.directive('pin', {
154-
mounted(el, binding) {
155-
el.style.position = 'fixed'
156-
const s = binding.arg || 'top'
157-
el.style[s] = binding.value + 'px'
158-
},
159-
updated(el, binding) {
160-
const s = binding.arg || 'top'
161-
el.style[s] = binding.value + 'px'
162-
}
163-
})
169+
```vue-html
170+
<div v-example:[arg]="value"></div>
164171
```
165172

166-
Result:
173+
Here the directive argument will be reactively updated based on `arg` property in our component state.
167174

168-
<!-- <common-codepen-snippet title="Custom directives: dynamic arguments + dynamic binding" slug="rNOaZpj" :preview="false" /> -->
175+
:::tip Note
176+
Apart from `el`, you should treat these arguments as read-only and never modify them. If you need to share information across hooks, it is recommended to do so through element's [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset).
177+
:::
169178

170179
## Function Shorthand
171180

172-
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:
181+
It's common for a custom directive to need the same behavior for `mounted` and `updated`, and don't care about the other hooks. In such cases we can define the directive as a function:
182+
183+
```vue-html
184+
<div v-color="color"></div>
185+
```
173186

174187
```js
175-
app.directive('pin', (el, binding) => {
176-
el.style.position = 'fixed'
177-
const s = binding.arg || 'top'
178-
el.style[s] = binding.value + 'px'
188+
app.directive('color', (el, binding) => {
189+
// this will be called for both `mounted` and `updated`
190+
el.style.color = binding.value
179191
})
180192
```
181193

@@ -196,22 +208,18 @@ app.directive('demo', (el, binding) => {
196208

197209
## Usage on Components
198210

199-
When used on components, custom directive will always apply to component's root node, similarly to [non-prop attributes](/guide/components/attrs.html).
211+
When used on components, custom directive will always apply to component's root node, similarly to [Fallthrough Attributes](/guide/components/attrs.html).
200212

201213
```vue-html
202-
<my-component v-demo="test"></my-component>
214+
<MyComponent v-demo="test" />
203215
```
204216

205-
```js
206-
app.component('my-component', {
207-
template: `
208-
<div> // v-demo directive will be applied here
209-
<span>My component content</span>
210-
</div>
211-
`
212-
})
213-
```
217+
```vue-html
218+
<!-- template of MyComponent -->
214219
215-
Unlike attributes, directives can't be passed to a different element with `v-bind="$attrs"`.
220+
<div> <!-- v-demo directive will be applied here -->
221+
<span>My component content</span>
222+
</div>
223+
```
216224

217-
Note that components can potentially have more than one root nodes. When applied to a multi-root component, directive will be ignored and the warning will be thrown.
225+
Note that components can potentially have more than one root nodes. When applied to a multi-root component, directive will be ignored and the warning will be thrown. Unlike attributes, directives can't be passed to a different element with `v-bind="$attrs"`. In general, it is **not** recommended to use custom directives on components.

0 commit comments

Comments
 (0)