Re-design custom directive API so that it better aligns with component lifecycle
Custom directives usage on components will follow the same rules as discussed in the Attribute Fallthrough Behavior RFC. It will be controlled by the child component via v-bind="$attrs".
const MyDirective = {
bind(el, binding, vnode, prevVnode) {},
inserted() {},
update() {},
componentUpdated() {},
unbind() {}
}
const MyDirective = {
beforeMount(el, binding, vnode, prevVnode) {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {}, // new
unmounted() {}
}
Make custom directive hook names more consistent with the component lifecycle.
Existing hooks are renamed to map better to the component lifecycle, with some timing adjustments. Arguments passed to the hooks remain unchanged.
bind -> beforeMountinserted -> mountedbeforeUpdate new, called before the element itself is updatedupdateupdated insteadcomponentUpdated -> updatedbeforeUnmount newunbind -> unmountedIn 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.
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:
<div v-foo="bar"></div>
Will roughly compile into this:
const vFoo = resolveDirective('foo')
return applyDirectives(h('div'), this, [
[vFoo, bar]
])
Where vFoo will be the directive object written by the user, which contains hooks like mounted and updated.
applyDirective returns a cloned VNode with the user hooks wrapped and injected as vnode lifecycle hooks (see Render Function API Changes for more details):
{
vnodeMounted(vnode, prevVNode) {
// call vFoo.mounted(...)
}
}
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 vnodeXXX hooks are passed down to the component as extraneous props and end up in this.$attrs.
This is consistent with the attribute fallthrough behavior discussed in vuejs/rfcs#26. 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.
N/A
N/A
$attrs as discussed in Attribute Fallthrough Behavior should apply as well.N/A
I think it will make sense to still support directives on components, even if we receive an array of elements. Some DOM manipulations like v-focus or v-clickaway are still valid for components. This will also make the adoption strategy a bit harder
@posva making the first argument Element | Array<Element> is still a breaking change, in fact it requires heavier refactoring to make sure existing directives work properly. It's also not clear what a directive should do in the case of receiving multiple elements: what should v-focus or v-clickaway do in such cases? Implicitly failing seems like a bad idea.
making the first argument Element | Array
is still a breaking change, in fact it requires heavier refactoring to make sure existing directives work properly
True, but it isn't much for existing directives to just extract the first element and throw if there are more (thinking of a possible codemod)
const [el] = elements
if (elements.length > 1) throw new Error()
For example, v-focus could lookup an input/select/button or explicitly tab-indexed element. For clickaway it could look up a data attribute.
But my point is, recreating those features with components is way more verbose:
<Focus>
<v-text-field/>
</Focus>
or isn't declarative:
<v-text-field ref="mainInput"/>
this.$refs.mainInput.focus() // a component method
Just thinking out loud: Would this be achievable? And if so, would it make any sense? Would make the implementation treeshakable, right?
import {
beforeMount,
mounted,
beforeUpdate,
update,
beforeUnmount,
unmounted,
} from 'vue/directives'
export default () => {
mounted((el, binding, vnode, prevVnode) => {
// do stuff
})
}
@posva I just realized in v3 directives are in fact implemented as special VNode data hooks - that is to say in render functions you can do this:
h('div', {
vnodeMounted() { ... },
vnodeUpdated() { ... }
})
And for template <div v-foo/>, it will take the mounted hook of v-foo and merge it into the VNode as vnodeMounted.
So they would be also included in the child component's this.$attrs. If the child component does not bind $attrs, the directive is ignored just like other extraneous attributes; if the child binds $attrs, it binds the custom directives as well. This makes it consistent with the new attr fallthrough behavior. WDYT?
Revised and published: https://github.com/vuejs/rfcs/pull/32
Most helpful comment
@posva I just realized in v3 directives are in fact implemented as special VNode data hooks - that is to say in render functions you can do this:
And for template
<div v-foo/>, it will take themountedhook ofv-fooand merge it into the VNode asvnodeMounted.So they would be also included in the child component's
this.$attrs. If the child component does not bind$attrs, the directive is ignored just like other extraneous attributes; if the child binds$attrs, it binds the custom directives as well. This makes it consistent with the new attr fallthrough behavior. WDYT?