From cf827843cfa07429a78b4f9ce947bd6871073395 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 8 Apr 2019 16:39:11 +0800 Subject: [PATCH 01/11] attr fallthrough behavior --- active-rfcs/0000-attr-fallthrough.md | 196 +++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 active-rfcs/0000-attr-fallthrough.md diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md new file mode 100644 index 00000000..34f87cf6 --- /dev/null +++ b/active-rfcs/0000-attr-fallthrough.md @@ -0,0 +1,196 @@ +- Start Date: 2019-04-08 +- Target Major Version: 3.x +- Reference Issues: N/A +- Implementation PR: N/A + +# Summary + +- Disable implicit attribute fall-through to child component root element + +- Remove `inheritAttrs` option + +# Basic example + +To replicate 2.x behavior in templates: + +``` html +
hi
+``` + +In render function: + +``` js +import { h } from 'vue' + +export default { + render() { + return h('div', this.$attrs, 'hi') + } +} +``` + +# Motivation + +In 2.x, the current attribute fallthrough behavior is quite implicit: + +- `class` and `style` used on a child component are implicitly applied to the component's root element. It is also automatically merged with `class` and `style` bindings on that element in the child component template. + + - However, this behavior is not consistent in functional components because functional components may return multiple root nodes. + + - With 3.0 supporting fragments and therefore multiple root nodes for all components, this becomes even more problematic. The implicit behavior can suddenly fail when the child component changes from single-root to multi-root. + +- attributes passed to a component that are not declared by the component as props are also implicitly applied to the component root element. + + - Again, in functional components this needs explicit application, and would be inconsistent for 3.0 components with multiple root nodes. + + - `this.$attrs` only contains attributes, but excludes `class` and `style`; `v-on` listeners are contained in a separate `this.$listeners` object. There is also the `.native` modifier. The combination of `inheritAttrs`, `.native`, `$attrs` and `$listeners` makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs means "pass everything that I don't care about down to this element/component". + + - `class` and `style` are always automatically merged, and are not affected by `inheritAttrs`. + +The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments (the ability for a component to have multiple root nodes) in 3.0, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't. + +In 3.0, we are planning to make attribute fallthrough an explicit decision of component authors. Whether a component accepts additional attributes becomes part of the component's API contract. We believe overall this should result in a simpler, more explicit and more consistent API. + +# Detailed design + +- `inheritAttrs` option will be removed. + +- `.native` modifier will be removed. + +- Non-prop attributes no longer automatically fallthrough to the root element of the child component (including `class` and `style`). This is the same for both stateful and functional components. + + This means that with the following usage: + + ``` js + const Child = { + props: ['foo'], + template: `
{{ foo }}
` + } + + const Parent = { + components: { Child }, + template: `` + } + ``` + + Both `bar="2"` AND `class="bar"` on `` will be ignored. + +- `this.$attrs` now contains **everything** passed to the component except those that are declared as props or custom events. **This includes `class`, `style`, `v-on` listeners (as `onXXX` properties)**. The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in [Render Function API Change](https://github.com/vuejs/vue-next/issues/29)). + + To explicitly inherit additional attributes passed by the parent, the child component should apply it with `v-bind`: + + ``` js + const Child = { + props: ['foo'], + template: `
{{ foo }}
` + } + ``` + + This also applies when the child component needs to apply `$attrs` to a non-root element, or has multiple root nodes: + + ``` js + const ChildWithNestedRoot = { + props: ['foo'], + template: ` + + ` + } + + const ChildWithMultipleRoot = { + props: ['foo'], + template: ` + + + ` + } + ``` + + In render functions, if simple overwrite is acceptable, `$attrs` can be merged using object spread. But in most cases, special handling is required (e.g. for `class`, `style` and `onXXX` listeners). Therefore a `cloneVNode` helper will be provided. It handles the proper merging of VNode data: + + ``` js + import { h, cloneVNode } from 'vue' + + const Child = { + render() { + const inner = h(InnerComponent, { + foo: 'bar' + }) + return cloneVNode(inner, this.$attrs) + } + } + ``` + + The 2nd argument to `cloneVNode` is optional. It means "clone the vnode and add these additional props". The `cloneVNode` helper serves two purposes: + + - Avoids mutating the original VNode + - Handles special merging logic for `class`, `style` and event listeners + + Inside render functions, the user also has the full flexibility to pluck / omit any props from `$attrs` using 3rd party helpers, e.g. lodash. + +## Removing Unwanted Listeners + +With flat VNode data and the removal of `.native` modifier, all listeners are passed down to the child component as `onXXX` functions: + +``` html + +``` + +compiles to: + +``` js +h(foo, { + onClick: foo, + onCustom: bar +}) +``` + +When spreading `$attrs` with `v-bind`, all parent listeners are applied to the target element as native DOM listeners. The problem is that these same listeners can also be triggered by custom events - in the above example, both a native click event and a custom one emitted by `this.$emit('click')` in the child will trigger the parent's `foo` handler. This may lead to unwanted behavior. + +Props do not suffer from this problem because declared props are removed from `$attrs`. Therefore we should have a similar way to "declare" emitted events from a component. There is currently [an open RFC for it](https://github.com/vuejs/rfcs/pull/16) by @niko278. + +Event listeners for explicitly declared events will be removed from `$attrs` and can only be triggered by custom events emitted by the component via `this.$emit`. + +# Drawbacks + +- Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, especially when using `class` and `style` for styling purposes, but it is the more "correct" behavior when it comes to component responsibilities and boundaries. Styling use cases can be easily worked around with by wrapping the component in a wrapper element. In fact, this should be the best practice in 3.0 because the child component may or may not have multiple root nodes. + +- For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread `$attrs` so that any `aria-x` attributes can fallthrough. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels. + +# Alternatives + +N/A + +# Adoption strategy + +## Documentation + +This RFC discusses the problem by starting with the 2.x implementation details with a lot of history baggage so it can seem a bit complex. However if we were to document the behavior for a new user, the concept is much simpler in comparison: + +- For a component without explicit props and events declarations, everything passed to it from the parent ends up in `$attrs`. + +- If a component declares explicit props, they are removed from `$attrs`. + +- If a component declares explicit events, corresponding `onXXX` listeners are removed from `$attrs`. + +- `$attrs` essentially means **extraneous attributes,**, or "any attributes passed to the component that hasn't been explicitly handled by the component". + +## Migration + +This will be one of the changes that will have a bigger impact on existing code and would likely require manual migration. + +- We will provide a warning when a component has unused extraneous attributes (i.e. non-empty `$attrs` that is never used during render). + +- For application code that adds `class` / `style` to child components for styling purposes: the child component should be wrapped with a wrapper element. + +- For higher-order components or reusable components that allow the consumer to apply arbitrary attributes / listeners to an inner element (e.g. custom form components that wrap ``): + + - Declare props and events that are consumed by the HOC itself (thus removing them from `$attrs`) + + - Refactor the component and explicitly add `v-bind="$attrs"` to the target inner component or element. For render functions, apply `$attrs` with the `cloneVNode` helper. + + - If a component is already using `inheritAttrs: false`, the migration should be relatively straightforward. + +We will need more dogfooding (migrating actual apps to 3.0) to provide more detailed migration guidance for this one, since the migration cost heavily depends on usage. From 1e21f8eafd36fc544cb71b587d4253042cebe9cd Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 8 Apr 2019 17:00:28 +0800 Subject: [PATCH 02/11] update links --- active-rfcs/0000-attr-fallthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 34f87cf6..86ac900b 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -75,7 +75,7 @@ In 3.0, we are planning to make attribute fallthrough an explicit decision of co Both `bar="2"` AND `class="bar"` on `` will be ignored. -- `this.$attrs` now contains **everything** passed to the component except those that are declared as props or custom events. **This includes `class`, `style`, `v-on` listeners (as `onXXX` properties)**. The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in [Render Function API Change](https://github.com/vuejs/vue-next/issues/29)). +- `this.$attrs` now contains **everything** passed to the component except those that are declared as props or custom events. **This includes `class`, `style`, `v-on` listeners (as `onXXX` properties)**. The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in [Render Function API Change](https://github.com/vuejs/rfcs/pull/28)). To explicitly inherit additional attributes passed by the parent, the child component should apply it with `v-bind`: From 852121583f35db489cb267ced03d8e93802d5d94 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 9 Apr 2019 09:24:09 +0800 Subject: [PATCH 03/11] cloneVNode -> mergeData --- active-rfcs/0000-attr-fallthrough.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 86ac900b..d04e893c 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -108,26 +108,18 @@ In 3.0, we are planning to make attribute fallthrough an explicit decision of co } ``` - In render functions, if simple overwrite is acceptable, `$attrs` can be merged using object spread. But in most cases, special handling is required (e.g. for `class`, `style` and `onXXX` listeners). Therefore a `cloneVNode` helper will be provided. It handles the proper merging of VNode data: + In render functions, if simple overwrite is acceptable, `$attrs` can be merged using object spread. But in most cases, special handling is required (e.g. for `class`, `style` and `onXXX` listeners). Therefore a `mergeData` helper will be provided. It handles the proper merging of VNode data: ``` js - import { h, cloneVNode } from 'vue' + import { h, mergeData } from 'vue' const Child = { render() { - const inner = h(InnerComponent, { - foo: 'bar' - }) - return cloneVNode(inner, this.$attrs) + return h(InnerComponent, mergeData({ foo: 'bar' }, this.$attrs)) } } ``` - The 2nd argument to `cloneVNode` is optional. It means "clone the vnode and add these additional props". The `cloneVNode` helper serves two purposes: - - - Avoids mutating the original VNode - - Handles special merging logic for `class`, `style` and event listeners - Inside render functions, the user also has the full flexibility to pluck / omit any props from `$attrs` using 3rd party helpers, e.g. lodash. ## Removing Unwanted Listeners From 5affdee06a4d5fa45b969c6443c76a4d616254ac Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 9 Apr 2019 09:24:43 +0800 Subject: [PATCH 04/11] Update 0000-attr-fallthrough.md --- active-rfcs/0000-attr-fallthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index d04e893c..d075b8df 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -181,7 +181,7 @@ This will be one of the changes that will have a bigger impact on existing code - Declare props and events that are consumed by the HOC itself (thus removing them from `$attrs`) - - Refactor the component and explicitly add `v-bind="$attrs"` to the target inner component or element. For render functions, apply `$attrs` with the `cloneVNode` helper. + - Refactor the component and explicitly add `v-bind="$attrs"` to the target inner component or element. For render functions, apply `$attrs` with the `mergeData` helper. - If a component is already using `inheritAttrs: false`, the migration should be relatively straightforward. From 59e91fafec180c4b2eefa88b8477f65ddd01fda8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 9 Apr 2019 10:34:40 +0800 Subject: [PATCH 05/11] Update 0000-attr-fallthrough.md --- active-rfcs/0000-attr-fallthrough.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index d075b8df..db7ff696 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -55,7 +55,7 @@ In 3.0, we are planning to make attribute fallthrough an explicit decision of co - `inheritAttrs` option will be removed. -- `.native` modifier will be removed. +- `.native` modifier for `v-on` will be removed. - Non-prop attributes no longer automatically fallthrough to the root element of the child component (including `class` and `style`). This is the same for both stateful and functional components. @@ -74,6 +74,8 @@ In 3.0, we are planning to make attribute fallthrough an explicit decision of co ``` Both `bar="2"` AND `class="bar"` on `` will be ignored. + +- `this.$listeners` will be removed. - `this.$attrs` now contains **everything** passed to the component except those that are declared as props or custom events. **This includes `class`, `style`, `v-on` listeners (as `onXXX` properties)**. The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in [Render Function API Change](https://github.com/vuejs/rfcs/pull/28)). From e443f4e7bf533292e9651f1cf75afad69b3d49d4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Nov 2019 23:56:37 -0500 Subject: [PATCH 06/11] rework --- active-rfcs/0000-attr-fallthrough.md | 202 ++++++++++----------------- 1 file changed, 76 insertions(+), 126 deletions(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index db7ff696..75ee8b94 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -5,124 +5,116 @@ # Summary -- Disable implicit attribute fall-through to child component root element +- Make the attrs fallthrough behavior more consistent; +- Make it easier to pass all extraneous attrs to child elements / components. -- Remove `inheritAttrs` option +# Motivation -# Basic example +In Vue 2.x, components have an implicit attrs fallthrough behavior. Any prop passed to a component, but is not declared as a prop by the component, is considered an "extraneous attribute". In 2.x, these extraneous attributes are exposed in `this.$attrs` and implicitly applied to the component's root node. This behavior can be disabled with `inheritAttrs: false`, where the user expects to explicit control where the attrs should be applied. -To replicate 2.x behavior in templates: +There are a number of inconsistencies and issues in the 2.x behavior: -``` html -
hi
-``` +- `inheritAttrs: false` does not affect `class` and `style`. -In render function: +- `class`, `style`, `v-on` listeners and custom directives are not included in `$attrs`, making it cumbersome for a higher-order component (HOC) to properly pass everything down to a nested child component. -``` js -import { h } from 'vue' +- Functional components have no implicit attrs fallthrough at all. -export default { - render() { - return h('div', this.$attrs, 'hi') - } -} -``` +In 3.x, the need for "spreading extraneous attrs" also becomes more prominent due to the ability for components to render multiple root nodes (fragments). This RFC seeks to address these problems. -# Motivation - -In 2.x, the current attribute fallthrough behavior is quite implicit: +# Detailed design -- `class` and `style` used on a child component are implicitly applied to the component's root element. It is also automatically merged with `class` and `style` bindings on that element in the child component template. +`this.$attrs` now contains **everything** passed to the component except those that are declared as props. **This includes `class`, `style`, `v-on` listeners (as `onXXX` props), and custom directives (as `onVnodeXXX` props)**. (This is based on flat props structure as proposed in [Render Function API Change](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md#flat-vnode-props-format)). As a result of this: - - However, this behavior is not consistent in functional components because functional components may return multiple root nodes. +- `.native` modifier for `v-on` will be removed. - - With 3.0 supporting fragments and therefore multiple root nodes for all components, this becomes even more problematic. The implicit behavior can suddenly fail when the child component changes from single-root to multi-root. +- `this.$listeners` will be removed. -- attributes passed to a component that are not declared by the component as props are also implicitly applied to the component root element. +When the component returns a single root node, `this.$attrs` will be implicitly merged into the root node's props. This is the same as 2.x, except it will now include all the props that were not previously in `this.$attrs`, as discussed above. - - Again, in functional components this needs explicit application, and would be inconsistent for 3.0 components with multiple root nodes. +If the component receives extraneous attrs, but returns multiple root nodes (a fragment), an automatic merge cannot be performed. If the user did not perform an explicit spread (checked by access to `this.$attrs` during render), a runtime warning will be emitted. The component should either pick an element to apply the attrs to (via `v-bind="$attrs"`), or explicitly suppress the warning with `inheritAttrs: false`. - - `this.$attrs` only contains attributes, but excludes `class` and `style`; `v-on` listeners are contained in a separate `this.$listeners` object. There is also the `.native` modifier. The combination of `inheritAttrs`, `.native`, `$attrs` and `$listeners` makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs means "pass everything that I don't care about down to this element/component". +## `inheritAttrs: false` - - `class` and `style` are always automatically merged, and are not affected by `inheritAttrs`. +With `inheritAttrs: false`, the component can either choose to intentionally ignore all extraneous attrs, or explicitly control where the attrs should be applied via `v-bind="$attrs"`: -The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments (the ability for a component to have multiple root nodes) in 3.0, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't. +``` html +
+ + +
+``` -In 3.0, we are planning to make attribute fallthrough an explicit decision of component authors. Whether a component accepts additional attributes becomes part of the component's API contract. We believe overall this should result in a simpler, more explicit and more consistent API. +In 2.x, this option does not affect `class` and `style` - they will be implicitly merged on root in all cases for stateful components - but in 3.0 this special case is removed: `class` and `style` will be part of `$attrs` just like everything else. -# Detailed design +## Merging Attrs in Render Functions -- `inheritAttrs` option will be removed. +In manual render functions, it may seem convenient to just use a spread: -- `.native` modifier for `v-on` will be removed. +``` js +export default { + props: { /* ... */ }, + inheritAttrs: false, + render() { + return h('div', { class: 'foo', ...this.$attrs }) + } +} +``` -- Non-prop attributes no longer automatically fallthrough to the root element of the child component (including `class` and `style`). This is the same for both stateful and functional components. +However, this will cause attrs to overwrite whatever existing props of the same name. For example, there the local `class` may be overwritten when we probably want to merge the classes instead. Vue provides a `mergeProps` helper that handles the merging of `class`, `style` and `onXXX` listeners: - This means that with the following usage: +``` js +import { mergeProps } from 'vue' - ``` js - const Child = { - props: ['foo'], - template: `
{{ foo }}
` +export default { + props: { /* ... */ }, + inheritAttrs: false, + render() { + return h('div', mergeProps({ class: 'foo' }, this.$attrs)) } +} +``` - const Parent = { - components: { Child }, - template: `` - } - ``` +This is also what `v-bind` uses internally. - Both `bar="2"` AND `class="bar"` on `` will be ignored. - -- `this.$listeners` will be removed. +## Consistency between Functional and Stateful Components -- `this.$attrs` now contains **everything** passed to the component except those that are declared as props or custom events. **This includes `class`, `style`, `v-on` listeners (as `onXXX` properties)**. The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in [Render Function API Change](https://github.com/vuejs/rfcs/pull/28)). +Functional components will now share the exact same behavior with Stateful components. The extraneous attrs is passed via the second context argument (as specified in [Render Function API Change](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md#functional-component-signature)): - To explicitly inherit additional attributes passed by the parent, the child component should apply it with `v-bind`: +``` js +const Func = (props, { attrs }) => { + return h('div', mergeProps({ id: 'x' }, attrs), props.msg) +} - ``` js - const Child = { - props: ['foo'], - template: `
{{ foo }}
` - } - ``` - - This also applies when the child component needs to apply `$attrs` to a non-root element, or has multiple root nodes: - - ``` js - const ChildWithNestedRoot = { - props: ['foo'], - template: ` - - ` - } +Func.props = { /* ... */ } +``` - const ChildWithMultipleRoot = { - props: ['foo'], - template: ` - - - ` - } - ``` +## Components with no Props Declaration - In render functions, if simple overwrite is acceptable, `$attrs` can be merged using object spread. But in most cases, special handling is required (e.g. for `class`, `style` and `onXXX` listeners). Therefore a `mergeData` helper will be provided. It handles the proper merging of VNode data: +Note that for components without props declaration (see [Optional Props Declaration](https://github.com/vuejs/rfcs/pull/25)), there will be no implicit attrs handling of any kind, because everything passed in is considered a prop and there will be no "extraneous" attrs. A component without props declaration (mostly functional components) is responsible for explicitly passing down necessary props. This can be easily done with object rest spread: - ``` js - import { h, mergeData } from 'vue' +``` js +const Func = ({ msg, ...rest }) => { + return h('div', mergeProps({ id: 'x' }, rest), [ + h('span', msg) + ]) +} +``` - const Child = { - render() { - return h(InnerComponent, mergeData({ foo: 'bar' }, this.$attrs)) - } - } - ``` +# Drawbacks + +For existing components using `inheritAttrs: false` this will be a breaking change. However, the upgrade should lead to simpler code. - Inside render functions, the user also has the full flexibility to pluck / omit any props from `$attrs` using 3rd party helpers, e.g. lodash. +# Alternatives + +N/A + +# Adoption strategy + +- Migration guide for existing components using `inheritAttrs: false`. +- Rework documentation regarding `$attrs`. + +# Unresolved questions ## Removing Unwanted Listeners @@ -146,45 +138,3 @@ When spreading `$attrs` with `v-bind`, all parent listeners are applied to the t Props do not suffer from this problem because declared props are removed from `$attrs`. Therefore we should have a similar way to "declare" emitted events from a component. There is currently [an open RFC for it](https://github.com/vuejs/rfcs/pull/16) by @niko278. Event listeners for explicitly declared events will be removed from `$attrs` and can only be triggered by custom events emitted by the component via `this.$emit`. - -# Drawbacks - -- Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, especially when using `class` and `style` for styling purposes, but it is the more "correct" behavior when it comes to component responsibilities and boundaries. Styling use cases can be easily worked around with by wrapping the component in a wrapper element. In fact, this should be the best practice in 3.0 because the child component may or may not have multiple root nodes. - -- For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread `$attrs` so that any `aria-x` attributes can fallthrough. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels. - -# Alternatives - -N/A - -# Adoption strategy - -## Documentation - -This RFC discusses the problem by starting with the 2.x implementation details with a lot of history baggage so it can seem a bit complex. However if we were to document the behavior for a new user, the concept is much simpler in comparison: - -- For a component without explicit props and events declarations, everything passed to it from the parent ends up in `$attrs`. - -- If a component declares explicit props, they are removed from `$attrs`. - -- If a component declares explicit events, corresponding `onXXX` listeners are removed from `$attrs`. - -- `$attrs` essentially means **extraneous attributes,**, or "any attributes passed to the component that hasn't been explicitly handled by the component". - -## Migration - -This will be one of the changes that will have a bigger impact on existing code and would likely require manual migration. - -- We will provide a warning when a component has unused extraneous attributes (i.e. non-empty `$attrs` that is never used during render). - -- For application code that adds `class` / `style` to child components for styling purposes: the child component should be wrapped with a wrapper element. - -- For higher-order components or reusable components that allow the consumer to apply arbitrary attributes / listeners to an inner element (e.g. custom form components that wrap ``): - - - Declare props and events that are consumed by the HOC itself (thus removing them from `$attrs`) - - - Refactor the component and explicitly add `v-bind="$attrs"` to the target inner component or element. For render functions, apply `$attrs` with the `mergeData` helper. - - - If a component is already using `inheritAttrs: false`, the migration should be relatively straightforward. - -We will need more dogfooding (migrating actual apps to 3.0) to provide more detailed migration guidance for this one, since the migration cost heavily depends on usage. From ab71b8c7d6410c7931f84e66cbe1685558d20978 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Nov 2019 23:58:17 -0500 Subject: [PATCH 07/11] update date --- active-rfcs/0000-attr-fallthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 75ee8b94..722ccf9a 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -1,4 +1,4 @@ -- Start Date: 2019-04-08 +- Start Date: 2019-11-05 - Target Major Version: 3.x - Reference Issues: N/A - Implementation PR: N/A From b1ee6885e2dbad00b2279564a419b81c0423cb23 Mon Sep 17 00:00:00 2001 From: Dobromir Hristov Date: Fri, 15 Nov 2019 16:47:04 +0200 Subject: [PATCH 08/11] fix links to Proposals (#100) --- active-rfcs/0000-attr-fallthrough.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 722ccf9a..8d5a4707 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -24,7 +24,7 @@ In 3.x, the need for "spreading extraneous attrs" also becomes more prominent du # Detailed design -`this.$attrs` now contains **everything** passed to the component except those that are declared as props. **This includes `class`, `style`, `v-on` listeners (as `onXXX` props), and custom directives (as `onVnodeXXX` props)**. (This is based on flat props structure as proposed in [Render Function API Change](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md#flat-vnode-props-format)). As a result of this: +`this.$attrs` now contains **everything** passed to the component except those that are declared as props. **This includes `class`, `style`, `v-on` listeners (as `onXXX` props), and custom directives (as `onVnodeXXX` props)**. (This is based on flat props structure as proposed in [Render Function API Change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md#flat-vnode-props-format)). As a result of this: - `.native` modifier for `v-on` will be removed. @@ -79,7 +79,7 @@ This is also what `v-bind` uses internally. ## Consistency between Functional and Stateful Components -Functional components will now share the exact same behavior with Stateful components. The extraneous attrs is passed via the second context argument (as specified in [Render Function API Change](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md#functional-component-signature)): +Functional components will now share the exact same behavior with Stateful components. The extraneous attrs is passed via the second context argument (as specified in [Render Function API Change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md#functional-component-signature)): ``` js const Func = (props, { attrs }) => { @@ -91,7 +91,7 @@ Func.props = { /* ... */ } ## Components with no Props Declaration -Note that for components without props declaration (see [Optional Props Declaration](https://github.com/vuejs/rfcs/pull/25)), there will be no implicit attrs handling of any kind, because everything passed in is considered a prop and there will be no "extraneous" attrs. A component without props declaration (mostly functional components) is responsible for explicitly passing down necessary props. This can be easily done with object rest spread: +Note that for components without props declaration (see [Optional Props Declaration](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0010-optional-props-declaration.md)), there will be no implicit attrs handling of any kind, because everything passed in is considered a prop and there will be no "extraneous" attrs. A component without props declaration (mostly functional components) is responsible for explicitly passing down necessary props. This can be easily done with object rest spread: ``` js const Func = ({ msg, ...rest }) => { From 5bd745ea5f936a20dfa52905e38e7312566cbbfd Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 28 Feb 2020 14:05:35 -0500 Subject: [PATCH 09/11] revise attr fallthrough rfc --- active-rfcs/0000-attr-fallthrough.md | 116 +++++++++++++++++++-------- 1 file changed, 84 insertions(+), 32 deletions(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 8d5a4707..0ae7ff00 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -5,38 +5,91 @@ # Summary -- Make the attrs fallthrough behavior more consistent; -- Make it easier to pass all extraneous attrs to child elements / components. +- Implicit fallthrough now by default only applies for a whitelist of attributes (`class`, `style`, event listeners, and a11y attributes). + +- Implicit fallthrough now works consistently for both stateful and functional components (as long as the component has a single root node). + +- `this.$attrs` now contains everything passed to the component minus those explicitly declared as props, including `class`, `style`, and listeners. `this.$listeners` is removed. # Motivation -In Vue 2.x, components have an implicit attrs fallthrough behavior. Any prop passed to a component, but is not declared as a prop by the component, is considered an "extraneous attribute". In 2.x, these extraneous attributes are exposed in `this.$attrs` and implicitly applied to the component's root node. This behavior can be disabled with `inheritAttrs: false`, where the user expects to explicit control where the attrs should be applied. +In Vue 2.x, components have an implicit attributes fallthrough behavior. Any attribute passed to a component that is not declared as a prop by the component, is considered an **extraneous attribute**. Fore example: + +``` html + +``` + +If `MyComp` didn't declare a prop named `id`, then the `id` is considered an extraneous attribute and will implicitly be applied to the root node of `MyComp`. + +This behavior is very convenient when tweaking layout styling between parent and child (by passing on `class` and `style`), or applying a11y attributes to child components. + +This behavior can be disabled with `inheritAttrs: false`, where the user expects to explicitly control where the attributes should be applied. These extraneous attributes are exposed in an instance property: `this.$attrs`. There are a number of inconsistencies and issues in the 2.x behavior: - `inheritAttrs: false` does not affect `class` and `style`. -- `class`, `style`, `v-on` listeners and custom directives are not included in `$attrs`, making it cumbersome for a higher-order component (HOC) to properly pass everything down to a nested child component. +- Implicit fallthrough does not apply for event listeners, leading to the need for `.native` modifier if the user wish to add a native event listener to the child component root. -- Functional components have no implicit attrs fallthrough at all. +- `class`, `style` and `v-on` listeners are not included in `$attrs`, making it cumbersome for a higher-order component (HOC) to properly pass everything down to a nested child component. -In 3.x, the need for "spreading extraneous attrs" also becomes more prominent due to the ability for components to render multiple root nodes (fragments). This RFC seeks to address these problems. +- Functional components have no implicit attrs fallthrough behavior. + +In 3.x, we are also introducing additional features such as [Optional Props Declaration](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0010-optional-props-declaration.md) and Fragments (multiple root nodes in a component template), both of which require additional considerations on the behavior. # Detailed design -`this.$attrs` now contains **everything** passed to the component except those that are declared as props. **This includes `class`, `style`, `v-on` listeners (as `onXXX` props), and custom directives (as `onVnodeXXX` props)**. (This is based on flat props structure as proposed in [Render Function API Change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md#flat-vnode-props-format)). As a result of this: +## Implicit Fallthrough Whitelist + +Implicit fallthrough now by default only affects a whitelist of attributes: + +- `class` +- `style` +- `v-on` event listeners (as `onXXX` props) +- Accessibility attributes: `aria-xxx` and `role`. + +To qualify for the whitelist, an attribute must: + +- have a common use case that relies on fallthrough +- have a low chance of being used as a component prop (e.g. `id` is not a good candidate) +- apply to all element types (e.g. `alt` is not included because it only applies for `img`). + +## Why Is There a Whitelist? + +We introduced [Optional Props Declaration](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0010-optional-props-declaration.md) in a previous RFC. However, optional props won't really be practical if the implicit fallthrough applies to all attributes. Consider the following example (using 3.x functional components for brevity): + +``` js +const Foo = props => h('div', props.msg) +``` + +Here the component intends to use `msg` as a component prop, however with implicit fallthrough, the root `div` will also render `msg` as a fallthrough attribute. To avoid the undesired fallthrough, the component would still need to explicitly declare props. -- `.native` modifier for `v-on` will be removed. +Another option is disabling the implicit fallthrough if the component has no props declared. However, not only is this inconsistent and confusing, it's also a major breaking change. In 2.x, a component without props simply means it does not expect any props and the user would expect the implicit fallthrough to be applied. -- `this.$listeners` will be removed. +The whitelist is a middle ground where: + +- Users can still rely on the convenience of implicit fallthrough for the most common cases. +- Users can enjoy more succinct component authoring without having to always declare props (especially for functional components). + +## Single Root vs. Fragment Root When the component returns a single root node, `this.$attrs` will be implicitly merged into the root node's props. This is the same as 2.x, except it will now include all the props that were not previously in `this.$attrs`, as discussed above. -If the component receives extraneous attrs, but returns multiple root nodes (a fragment), an automatic merge cannot be performed. If the user did not perform an explicit spread (checked by access to `this.$attrs` during render), a runtime warning will be emitted. The component should either pick an element to apply the attrs to (via `v-bind="$attrs"`), or explicitly suppress the warning with `inheritAttrs: false`. +If the component receives extraneous attrs, but returns multiple root nodes (a fragment), an automatic merge cannot be performed. If the user did not perform an explicit spread (checked by access to `this.$attrs` during render), a runtime warning will be emitted. The component should either pick an element to apply the attrs to (via `v-bind="$attrs"`, see section below), or explicitly suppress the warning with `inheritAttrs: false`. + +## Explicitly Controlling the Fallthrough -## `inheritAttrs: false` +`this.$attrs` (and `context.attrs` for `setup()` and functional components) now contains all attributes passed to the component (as long as it is not declared as props). This includes `class`, `style`, normal attributes and `v-on` listeners (as `onXXX` props). This is based on the flat props structure proposed in [Render Function API Change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md#flat-vnode-props-format). -With `inheritAttrs: false`, the component can either choose to intentionally ignore all extraneous attrs, or explicitly control where the attrs should be applied via `v-bind="$attrs"`: +In case the user wants to let an attribute that is not in the whitelist fallthrough, e.g. `id`, it can be added individually as: + +``` html +
+``` + +### `inheritAttrs: false` + +With `inheritAttrs: false`, the implicit fallthrough is disabled. The component can either choose to intentionally ignore all extraneous attrs, or explicitly control where the attrs should be applied via `v-bind="$attrs"`: ``` html
@@ -45,9 +98,11 @@ With `inheritAttrs: false`, the component can either choose to intentionally ign
``` -In 2.x, this option does not affect `class` and `style` - they will be implicitly merged on root in all cases for stateful components - but in 3.0 this special case is removed: `class` and `style` will be part of `$attrs` just like everything else. +Note `$attrs` will also include attributes not in the implicit whitelist. So this pattern can also be used to get around the whitelist and force all attributes to fallthrough. + +> In 2.x, `inheritAttrs` does not affect `class` and `style` - they will still be merged onto the root element. With this RFC, this special case is removed: `class` and `style` will be part of `$attrs` just like everything else. -## Merging Attrs in Render Functions +### In Render Functions In manual render functions, it may seem convenient to just use a spread: @@ -89,30 +144,29 @@ const Func = (props, { attrs }) => { Func.props = { /* ... */ } ``` -## Components with no Props Declaration +## API Deprecations -Note that for components without props declaration (see [Optional Props Declaration](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0010-optional-props-declaration.md)), there will be no implicit attrs handling of any kind, because everything passed in is considered a prop and there will be no "extraneous" attrs. A component without props declaration (mostly functional components) is responsible for explicitly passing down necessary props. This can be easily done with object rest spread: +- `.native` modifier for v-on will be removed. `v-on` listeners attached to a component now implicitly fallthrough to the child component root, and is also included in `$attrs` in case of manual inheritance. -``` js -const Func = ({ msg, ...rest }) => { - return h('div', mergeProps({ id: 'x' }, rest), [ - h('span', msg) - ]) -} -``` +- `this.$listeners` will be removed for the same reason. # Drawbacks -For existing components using `inheritAttrs: false` this will be a breaking change. However, the upgrade should lead to simpler code. +- Attributes not in the whitelist will now need to be explicitly applied in the child component. However, such cases should be rare and we can detect and warn the presence of such attributes in the compatibility build. -# Alternatives +# Adoption strategy -N/A +There are two types of 2.x components affected by these changes: -# Adoption strategy +1. Component using `inheritAttrs: false`. We can detect and warn usage of `this.$listeners` in the compatibility build. -- Migration guide for existing components using `inheritAttrs: false`. -- Rework documentation regarding `$attrs`. + There could technically be cases where the user relies on the 2.x `class` and `style` behavior with `inheritAttrs: false`, but it should be very rare. We will have a dedicated item in the migration guide / helper to remind the developer to check for such cases. + +2. Components relying on fallthrough of attributes not in the whitelist. This should be a relatively rare case and can be detected and warned against in the compatibility build + +Additionally: + +- `.native` modifier will be a no-op and emit a migration warning. # Unresolved questions @@ -135,6 +189,4 @@ h(foo, { When spreading `$attrs` with `v-bind`, all parent listeners are applied to the target element as native DOM listeners. The problem is that these same listeners can also be triggered by custom events - in the above example, both a native click event and a custom one emitted by `this.$emit('click')` in the child will trigger the parent's `foo` handler. This may lead to unwanted behavior. -Props do not suffer from this problem because declared props are removed from `$attrs`. Therefore we should have a similar way to "declare" emitted events from a component. There is currently [an open RFC for it](https://github.com/vuejs/rfcs/pull/16) by @niko278. - -Event listeners for explicitly declared events will be removed from `$attrs` and can only be triggered by custom events emitted by the component via `this.$emit`. +Props do not suffer from this problem because declared props are removed from `$attrs`. Therefore we should have a similar way to "declare" emitted events from a component. Event listeners for explicitly declared events will be removed from `$attrs` and can only be triggered by custom events emitted by the component via `this.$emit`. There is currently [an open RFC for it](https://github.com/vuejs/rfcs/pull/16) by @niko278. It is complementary to this RFC but does not affect the design of this RFC, so we can leave it for consideration at a later stage, even after Vue 3 release. From 6ec0a7d9bfba9d71431fe86ee6c81bbddd50b499 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 28 Feb 2020 17:28:40 -0500 Subject: [PATCH 10/11] add data-xxx attributes to the whitelist --- active-rfcs/0000-attr-fallthrough.md | 1 + 1 file changed, 1 insertion(+) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index 0ae7ff00..a8d20123 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -47,6 +47,7 @@ Implicit fallthrough now by default only affects a whitelist of attributes: - `style` - `v-on` event listeners (as `onXXX` props) - Accessibility attributes: `aria-xxx` and `role`. +- Data attributes: `data-xxx`. To qualify for the whitelist, an attribute must: From 6d5192ee3e583f2adbd52a83031a22a46a062793 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 28 Feb 2020 17:45:40 -0500 Subject: [PATCH 11/11] edits --- active-rfcs/0000-attr-fallthrough.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/active-rfcs/0000-attr-fallthrough.md b/active-rfcs/0000-attr-fallthrough.md index a8d20123..57984916 100644 --- a/active-rfcs/0000-attr-fallthrough.md +++ b/active-rfcs/0000-attr-fallthrough.md @@ -5,11 +5,11 @@ # Summary -- Implicit fallthrough now by default only applies for a whitelist of attributes (`class`, `style`, event listeners, and a11y attributes). +- Implicit fallthrough now by default only applies for a whitelist of attributes (`class`, `style`, event listeners, a11y attributes, and data attributes). - Implicit fallthrough now works consistently for both stateful and functional components (as long as the component has a single root node). -- `this.$attrs` now contains everything passed to the component minus those explicitly declared as props, including `class`, `style`, and listeners. `this.$listeners` is removed. +- `this.$attrs` now contains everything passed to the component minus those explicitly declared as props, including `class`, `style`, and listeners. (`this.$listeners` is removed) # Motivation @@ -153,7 +153,9 @@ Func.props = { /* ... */ } # Drawbacks -- Attributes not in the whitelist will now need to be explicitly applied in the child component. However, such cases should be rare and we can detect and warn the presence of such attributes in the compatibility build. +Having a hard-coded whitelist is not ideal, but we are making a pragmatic trade-off here since the whitelist should cover the vast majority of use cases where implicit fallthrough provides convenience value and is expected. + +Attributes not in the whitelist will now need to be explicitly applied in the child component. However, such cases should be rare and we can detect and warn the presence of such attributes in the compatibility build. # Adoption strategy