Skip to content

Commit c210a01

Browse files
NataliaTepluhinantepluhinaphananznck
authored
Migrate mixins & custom directives (#61)
* feat: migrated mixins * feat: started custom directives * feat: finished directives * Update src/guide/mixins.md Co-Authored-By: Phan An <me@phanan.net> * fix: structured directive arguments * fix: fixed html to vue-html * Update src/guide/custom-directive.md Co-Authored-By: Rahul Kadyan <hi@znck.me> * Update src/guide/custom-directive.md Co-Authored-By: Rahul Kadyan <hi@znck.me> * Update src/guide/custom-directive.md Co-Authored-By: Rahul Kadyan <hi@znck.me> * Update src/guide/custom-directive.md Co-Authored-By: Rahul Kadyan <hi@znck.me> * fix: made custom directives more clear Co-authored-by: ntepluhina <ntepluhina@gitlab.com> Co-authored-by: Phan An <me@phanan.net> Co-authored-by: Rahul Kadyan <hi@znck.me>
1 parent 55ca32e commit c210a01

File tree

3 files changed

+475
-0
lines changed

3 files changed

+475
-0
lines changed

src/.vuepress/config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const sidebar = {
2828
'/guide/component-provide-inject'
2929
]
3030
},
31+
{
32+
title: 'Reusability & Composition',
33+
collapsable: false,
34+
children: ['/guide/mixins', '/guide/custom-directive']
35+
},
3136
{
3237
title: 'Migration to Vue 3',
3338
collapsable: true,

src/guide/custom-directive.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)