Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Directive API Change #33

Closed
yyx990803 opened this issue Apr 10, 2019 · 6 comments
Closed

Custom Directive API Change #33

yyx990803 opened this issue Apr 10, 2019 · 6 comments

Comments

@yyx990803
Copy link
Member

yyx990803 commented Apr 10, 2019

  • Start Date: 2019-04-09
  • Target Major Version: 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

  • 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".

Basic example

Before

const MyDirective = {
  bind(el, binding, vnode, prevVnode) {},
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {}
}

After

const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {}
}

Motivation

Make custom directive hook names more consistent with the component lifecycle.

Detailed design

Hooks Renaming

Existing hooks are renamed to map better to the component lifecycle, with some timing adjustments. Arguments passed to the hooks remain unchanged.

  • bind -> beforeMount
  • inserted -> mounted
  • beforeUpdate new, called before the element itself is updated
  • update removed, use updated instead
  • componentUpdated -> updated
  • beforeUnmount new
  • unbind -> unmounted

Usage on Components

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.

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.

Drawbacks

N/A

Alternatives

N/A

Adoption strategy

  • The renaming should be easy to support in the compat build
  • Codemod should also be straightforward
  • For directives used on components, the warning on unused $attrs as discussed in Attribute Fallthrough Behavior should apply as well.

Unresolved questions

N/A

@posva
Copy link
Member

posva commented Apr 10, 2019

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

@yyx990803
Copy link
Member Author

yyx990803 commented Apr 10, 2019

@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.

@posva
Copy link
Member

posva commented Apr 10, 2019

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

@LinusBorg
Copy link
Member

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
  })
}

@yyx990803
Copy link
Member Author

yyx990803 commented Apr 11, 2019

@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?

@yyx990803
Copy link
Member Author

Revised and published: vuejs/rfcs#32

@github-actions github-actions bot locked and limited conversation to collaborators Nov 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants