-
Notifications
You must be signed in to change notification settings - Fork 546
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
Attribute Fallthrough + Functional Component Updates #154
Conversation
active-rfcs/0000-attr-fallthrough.md
Outdated
- In v3, the `@click` listener will fallthrough and register a native click listener on the root of `MyButton`. This means component authors no longer need to proxy native events to custom events in order to support `v-on` usage without the `.native` modifier. In fact, the `.native` modifier will be removed altogether. | ||
|
||
Note this may result in unnecessary registration of native event listeners when the user is only listening to component custom events, which we discuss below in [Unresolved Questions](#unresolved-questions). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given MyButton
is authored that way would it produce 2 click events with implicit event listeners fallthrough?
<button @click="$emit('click', $event)">Click me</button>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, the whole point is you don't proxy events anymore.
active-rfcs/0000-attr-fallthrough.md
Outdated
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. 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To support just style
and class
attributes (without event listeners) on the component root it won't be safe to simply reply on implicit fallthrough because:
- All components have a fallthrough for
style
,class
andv-on
attributes - Given many of the components emit their custom events we'll assign unnecessary event handlers on their roots from implicit fallthrough
- To opt-out of this behaviour we declare
inheritAttrs: false
for each component that has a custom event - Now to support
style
andclass
attributes on the root of those components we writev-bind="filteredAttrs"
which contains needed attributes only
Compare that with a v2 behaviour where nothing is required to get this behaviour out of the box and for arbitrary attributes only v-bind="$attrs"
is required. Also no extra work is required to make custom events work safely (that may have clashing names with native events).
Even if #16 does become implemented there're too many workarounds required for just custom events to be able to safely work.
I can hardly imagine $attrs
containing event listeners to be as useful as $attrs
in v2.
Consider this example:
<input @input="$emit('input', $event)" v-bind="$attrs">
If you do this in v3 then you should get 2 input
events at once. With inheritAttrs: true
this should produce 3 events since we also have implicit fallthrough.
In each of those scenarios you should always filter $attrs
and I couldn't find a good use-case where you need both attributes and listeners in one place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly I don't understand your concerns, so I'll just answer with what I think might address it:
-
In v2 the fallthrough applies to all attributes. It's not just
class
andstyle
. You never neededv-bind="$attrs"
unless you useinheritAttrs: false
. However, becausev-on
does not automatically fallthrough, users have to manually proxy the native events to custom events. -
In v3 with the
v-on
fallthrough you should never need to explicitly proxy events again. So in most cases implicit fallthrough will just work and you don't need to do anything. -
The only downside is when users listen for a custom event, a native listener is also added. This is why
emits
is suggested here: by declaring the custom events the component intends to emit, Vue can avoid binding them as native listeners.
So the only extra thing you need is the emits
option, which can serve documentation and runtime validation purpose as well, just like props
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, because v-on does not automatically fallthrough, users have to manually proxy the native events to custom events.
Having to add a .native
modifier is way easier than dealing with filtering $attrs
when authoring components. Being able to control whether an event listener should be assigned to the root or not on a component client level looks like a more reasonable approach for me, at least it does solve the issue of event listeners implicit fallthrough.
The naming could probably be reconsidered though. I suggest leaving the modifier behaviour but calling it .root
rather than .native
(that would better communicate that the listener would be assigned to a component's root and a fragment vnode does not fulfill this requirement).
The problem with this approach is that it simplifies things if you need your listeners on the root element and complicates things if you need them somewhere else. It also complicates things if you have root event listeners emitting anything but the event
object, in that case you'll have to disable fallthrough behaviour completely and manually filter $attrs
object.
So some components will benefit from that and others will require more work than usual. Vue 2 does not have that problem.
Maybe a balance could be achieved here?
Consider a component that has to have implicit attributes fallthrough, but at the same time emit a custom event:
<input @input="$emit('input', $event.target.value)" v-bind="filteredAttrs">
<script>
export default {
inheritAttrs: false,
computed: {
filteredAttrs() {...}
}
}
</script>
The component above should benefit from attributes fallthrough in theory but it has to disable that behaviour in order to have a custom event. filteredAttrs
should now filter all attributes except style
, class
and onInput
listener.
Since #16 is not part of this RFC at the current state this is how I imagine it's supposed to work for this case when this RFC is accepted. If the new behaviour requires emits
to deal with the issues above I think it should be a part of this RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting rid of .native
is a very good thing imo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the primary concern is for components that emit custom events with the same names as native events? How often does that really happen?
For most components that are wrappers of native equivalents (e.g. <MyButton>
, <MyInput>
), users would expect native event names to behave like native listeners. And for input components, you really should support v-model
as the primary usage anyway, so I believe the cases where you need to emit a custom event named input
AND the root element happens to be an <input>
element is quite rare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my codebase this happens a lot. This change would stop me from migrating to v3. Also there is not a single case of .native
in my project, but for some reason I'll be forced to go through a migration step in order to remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CyberAP assuming emits
option is added, you can explicit control the events that you want to "take over". E.g. with emits: ['input']
, @input
will no longer fallthrough as a native listener and you can decide what to do with it, with any other event listeners still automatically fallthrough. If you use inheritAttrs: true
, listeners listed in emits
are also removed from $attrs
. Does that resolve your problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does resolve my problem provided that this migration step is not manual.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically a codemod can scan all instances of $emit
calls in a component and generate the emits
option for you.
In Vue 2, trying to declare a prop called
If this limitation was removed, would it be possible to declare a prop named If it was possible, it would allow resolving both name clashes between native and custom events, and the unresolved question about avoiding certain native listeners. Perhaps declaring a fallthrough attribute as a prop could be a development-time warning instead of error. |
@leopiccionia in fact that already works in v3, although I think a dedicated |
BREAKING CHANGE: attribute fallthrough behavior has been adjusted according to vuejs/rfcs#154
active-rfcs/0000-attr-fallthrough.md
Outdated
### Multiple Root / Fragment Components | ||
|
||
In Vue 3, components can have multiple root elements (i.e. fragment root). In such cases, an automatic merge cannot be performed. The user will be responsible for spreading the attrs to the desired element: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if this is an obvious question, with this proposal is there a way to manually indicate what native HTML element is represented by the component?
For example, when building a component in React I can specify (in TypeScript) that my component extends JSX.IntrinsicElements<'button'>
, which then gives all consumers of my component proper autocomplete for HTML button
attributes + my custom props.
I see here that Vue 3 will try to automatically do pass through, but won't if there's a fragmented root. There are also cases where you could have a wrapper div around a native button
, but still want to spread and have prop validation for the underlying button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be two separate questions.
For TSX inference, it is possible but not as straightforward:
const MyComponent = defineComponent({
props: {
foo: String
}
})
const MyComponentWithButtonProps = MyComponent as {
new(): InstanceType<typeof MyComponent> & { $props: JSX.IntrinsicElements['a'] }
}
// voila
<MyComponentWithButtonProps href="foo" />
A helper type can be used to make this cleaner:
type ExtendProps<Comp extends { new(): any }, elements extends string> = {
new(): InstanceType<Comp> & { $props: JSX.IntrinsicElements[elements] }
}
const MyCompWithButtonProps = MyComponent as ExtendProps<typeof MyComponent, 'a'> // you can even use a union type to extend multiple elements
For fragment root / wrapper elements, you can always manually control where the $attrs
should be applied to. It's discussed right before this section you are commenting on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I got that you could manually place the attrs anywhere, I just wanted to make sure you could indicate in the types which element you are applying them to. Thanks!
This RFC is now in final comments stage. An RFC in final comments stage means that: The core team has reviewed the feedback and reached consensus about the general direction of the RFC and believe that this RFC is a worthwhile addition to the framework. |
@yyx990803 if event listeners are excluded from |
@CyberAP Edit:
|
i would expect having 3 different dictionaries:
|
@yyx990803 Now that optional props are gone, could we think about changing |
@Akryum I don't think so. That also should be in its own RFC if it were to be discussed. |
@backbone87 I don't know what you'd need |
@yyx990803 the usecase is to check if a listener was attached. consider the following template:
edit: edit2: |
@backbone87 you can also declare the |
|
That's what I thought at first as well, but the times when you actually need to do so is negligible. |
Just want to point out one caveat with relying on the approach of adding onMoreClick to props: if someone didn't read this bit of the documentation and tried to listen via following up ... from the docs:
now that we have It feels like putting events in emits and also having onEvent props now might make camelCase events more common, where it was previously discouraged. Is there any reason why camelCase events shouldn't trigger kebab-case equivalents? There is logic that already does this for |
... |
So what about custom directives? |
Revoke [RFC-0010] Optional props decalration
Update [RFC-0007] Functional and async components API change
New attribute fallthrough behavior RFC that supersedes Attr fallthrough behavior #137
The above changes are submitted together for review because they are closely related to one another. Below I will try to explain how we arrived at these changes.
The Problem
In an earlier phase of Vue 3 development we introduced [RFC0010] Optional Props Declaration - but this led to an issue with attr fallthrough. Given a component with no props declaration, we are no longer able to distinguish between the following user intentions:
A component utilizing optional props, i.e. "You can pass me any props, and I will decide what to do with them".
A component expecting no props. i.e. "I don't expect any props - anything you pass to me should just fallthrough.
In Vue 2.x, a component without props declaration means (2). With optional props declaration, it will mean (1).
For a type (1) component, it would not make sense to apply the attribute fallthrough behavior, because for a given prop, we cannot tell whether the component intends to treat it as a prop or as a fallthrough attribute:
Initially, we wanted to disable attribute fallthrough for components without props declaration in v3 (as proposed in an early draft of the attribute fallthrough RFC).
However, we noticed that users find it surprising that
class
fallthrough doesn't work when noprops
are declared. This is quite different from the expectation coming from Vue 2. In general, it is very common for users to rely on the fallthrough ofclass
andstyle
for layout purposes.So we are at a dilemma here: we cannot make the fallthrough work the same way for components with optional props, but making it not work at all can lead to both confusion and inconvenience.
Review the Goals
To get a better idea of how to evaluate the trade-off, let's look at the original reason for the fallthrough behavior to exist:
Cross-component layout styling: It is very common for users to rely on the fallthrough of
class
andstyle
for adjusting layout of nested components. In React, the lack of such a mechanism has led to various different workarounds and debates (e.g. should a component manage its own margins, etc.).Convenience for component library authors: The automatic fallthrough is valuable when shipping UI components that wraps native elements, so that the author doesn't need to manually proxy all the possible attributes on the native element via props (This is also the reason why we are making
v-on
fallthrough).Accessibility control for component consumers: Fallthrough is also useful for adding
aria-*
attributes to 3rd party components that the user has no control over.Other considerations:
Optional Props: the current issue is caused by the introduction of optional props. If we remove this feature, then the fallthrough behavior can be kept the same as in 2.x (except for warnings on fragment components).
Consistency: given that we keep optional props, we can choose to:
Make the fallthrough rules consistent for all components regardless of whether it has
props
declaration or not, ORMake it behave differently based on the presence of
props
declaration.Possible Solutions
A. Removing Optional Props
Removing the optional props feature altogether would allow us to keep the same behavior as in v2. Here we essentially need to compare the benefits of optional props vs. the trade-offs we have to make to keep it. Optional props have the following benefits:
Makes v3 functional components more succinct to use:
Makes it possible to use only type-annotations for props in TS, as some users prefer using TS type syntax for more complex prop types:
With the props declaration it is a bit awkward:
However, it could also be argued that this can potentially lead TS components in the ecosystem to exhibit different attribute fallthrough behavior from JS components.
B. Whitelist for All Cases
Assuming we keep optional props, one option is to allow only a whitelist of attributes to fallthrough for all components (regardless of the presence of props declaration). This is what I proposed in #137. However, the issue with this approach is that a minimal whitelist compromises goal 2 (convenience for component authors) and goal 3 (a11y). In order to better support these goals, we have to define a whitelist that becomes complicated and difficult to remember. In addition, the whitelist has possibility to clash with names that the user may intend to use as a prop (e.g.
id
orrole
), creating more potential confusions.C. Whitelist only for Components with Optional Props
Another option is differentiating between a component with no props declaration vs. a component with empty props declaration:
The upside is we get to keep optional props for stateful components; the downside is that this difference is quite subtle and can be confusing when it bites.
D. Optional Props only for Functional Components
This is the option this PR is going with.
Yet another option is to only support optional props for functional components:
All stateful components must still explicitly declare props.
Functional components without
props
declaration will have everything passed in as props. It will only have implicit fallthrough forclass
,style
andv-on
listeners.Plain functions as Functional Component is new in v3, so it is not going to affect existing code as much.
These functional components are typically created in very situational / one-off use cases where full fallthrough typically isn't needed.
class
,style
andv-on
should cover most of the use cases.