Component no longer need to delcare props in order to receive props. Everything passed from parent vnode's data (with the exception of internal properties, i,e. key, ref, slots and nativeOn*) will be available in this.$props and also as the first argument of the render function. This eliminates the need for this.$attrs and this.$listeners.
When no props are declared on a component, props will not be proxied on the component instance and can only be accessed via this.$props or the props argument in render functions.
You still can delcare props in order to specify default values and perform runtime type checking, and it works just like before. Declared props will also be proxied on the component instance. However, the behavior of undeclared props falling through as attrs will be removed; it's as if inheritAttr now defaults to false. The component will be responsible for merging the props as attrs onto the desired element.
// before
{
attrs: { id: 'foo' },
domProps: { innerHTML: '' },
on: { click: foo },
key: 'foo',
ref: 'bar'
}
// after (consistent with JSX usage)
{
id: 'foo',
domPropsInnerHTML: '',
onClick: foo,
key: 'foo',
ref: ref => {
this.$refs.bar = ref
}
}
h can now be globally imported and is no longer bound to component instacnes. VNodes created are also no longer bound to compomnent instances (this means you can no longer access vnode.context to get the component instance that created it)
No longer resolves component by string names; Any h call with a string is considered an element. Components must be resolved before being passed to h.
import { resolveComponent } from 'vue'
render (h) {
// only necessary when you are trying to access a registered component instead
// of an imported one
const Comp = resolveComponent(this, 'foo')
return h(Comp)
}
In templates, components should be uppercase to differentiate from normal elements.
NOTE: how to tell in browser templates? In compiler, use the following intuitions:
resolveComponent returns name string if component is not found)Scoped slots and normal slots are now unified. There's no more difference between the two. Inside a component, all slots on this.$slots will be functions and all them can be passed arguments.
// before
h(Comp, [
h('div', { slot: 'foo' }, 'foo')
h('div', { slot: 'bar' }, 'bar')
])
// after
h(Comp, () => h('div', 'default slot'))
// or
import { childFlags } from 'vue/flags'
h(Comp, null, {
slots: {
foo: () => h('div', 'foo'),
bar: () => h('div', 'bar')
}
}, childFlags.COMPILED_SLOTS)
// also works
h(Comp, null, {
foo: () => h('div', 'foo'),
bar: () => h('div', 'bar')
})
Functional components can now really be just functions.
// before
const Func = {
functional: true,
render (h, ctx) {
return h('div')
}
}
// Now can also be:
const Func = (h, props, slots, ctx) => h('div')
Func.pure = true
Async components now must be explicitly created.
import { createAsyncComponent } from 'vue'
const AsyncFoo = createAsyncComponent(() => import('./Foo.vue'))
Now are internally on-vnode hooks with the exact same lifecycle as components.
Custom directives are now applied via a helper:
import { applyDirective, resolveDirective } from 'vue'
render (h) {
// equivalent for v-my-dir
const myDir = resolveDirective(this, 'my-dir')
return applyDirective(h('div', 'hello'), [[myDir, this.someValue]])
}
No longer performs auto-prefixing.
false. Instead, it's set as attr="false" instead. To remove the attribute, use null.Filters are gone for good (or can it?)
v-for. Instead, use something like :ref="'foo' + key" or function refs./cc @vuejs/collaborators @octref @Atinux @DanielRosenwasser @alexchopin @clarkdo @pi0 @chenjiahan @johnleider @Leopoldthecoder @KaelWD @icarusion @rstoenescu @rigor789 @Hanks10100
You have been added to this repo either because you are a core team member or as maintainer of notable projects that builds on top of Vue. I'm giving you early access to the WIP repo to provide early feedback on proposed breaking changes to Vue internals. What I am most interested in is how much would the above changes affect your project - for each change, how difficult would it be to adapt to it? Would any of these be a deal breaker? Any of these would really help? Any additional ideas? Any type of feedback is welcome - also keep in mind that this is very early stage and nothing is set in stone yet.
Hi Evan,
Nice work! Here are some of my early thoughts:
However, the behavior of undeclared props falling through as attrs will be removed; it's as if
inheritAttrnow defaults tofalse. The component will be responsible for merging the props as attrs onto the desired element.
Does this mean component users cannot output arbitrary attributes onto the root element unless the component authors explicitly allow this? I think that might cause some troubles because component authors usually cannot know in advance what attributes are necessary in certain use cases (mostly interoperability issues). eg. A11Y related stuff aria-*/role or Microdata's itemprop/itemtype/... or some necessary attributes when leveraging some existing frontend libraries that depend them.
This seem that we cannot access components inside directives anymore (which we do quite a lot currently in our projects). I personally prefer directives over components on certain use cases and I haven't think of a way to migrate without drastically breaking our current API ATM.
In templates, components should be uppercase to differentiate from normal elements.
Does this mean we no longer allow kebab-casing for Vue components in templates?
NOTE: how to tell in browser templates?
Just skip step 1 for compiler intuitions seems fine?
Great. Though this might hurt those components providing slots and scoped slots with the same name (but for different purposes). It already doesn't work as expected when using template, while those who are using render functions for this might gonna change slot names.
- Custom directives are now applied via a helper:
Does this only affects render functions? Is there any difference for templates?
No longer removes attribute if value is boolean false. Instead, it's set as attr="false" instead. To remove the attribute, use null.
Do we still have predefined boolean attributes (true boolean attrs like disabled/checked, not boolean-ish strings like draggable)?
might gonna change slot names
You could probably use slot.length to differentiate between scoped and not.
seem that we cannot access components inside directives anymore
I think we use this in a few places too.
However, the behavior of undeclared props falling through as
attrswill be removed; it's as ifinheritAttrnow defaults tofalse.
What I get here is: this.$props would have all the props and there is no this.$attrs which means if I have to forward unhandled attributes to an element I have to calculate this.$attrs equivalent. Right?
VNodes are now context-free
I guess this would affect devtools. /cc @Akryum
// after (consistent with JSX usage)
{
type: [Function MyComponent], // type of vnode, some sort of reference to component.
id: 'foo',
domPropsInnerHTML: '',
onClick: foo,
key: 'foo',
ref: ref => {
this.$refs.bar = ref
}
}
Maybe we can add type to vnode so that it's easy do the check vnode.type === MyComponent.
Flat Data Format
seems friendly to tsx, should we add event(emit) declaration into component?
similar to props declaration, you can access the declaration of the event in component instance, or $emit the dynamic event
// Now can also be:
const Func = (h, props, slots, ctx) => h('div')
Func.pure = true
why we need the pure props?
Filters are gone for good (or can it?)
a nice sugar for template
why we need the pure props?
I think it's to identify functional components from components created with Vue.extend/ async components. But I think we could add a flag to extended components, and omit pure from functional components.
Filters are gone for good (or can it?)
a nice sugar for template
Oh man, I can still recall the chaos it created when the same proposal was raised for v2. Too bad it's been a while and pipe operator is still not a thing yet. FWIW, all the arguments (for both sides) in the linked issue should still be valid.
Edit: Yes, they can be gone now that users are more used to a v2 world with limited support for filters :)
What would be the prop merging behavior for class and event props (onClick)?
I share the same concern with @Justineo . Other than ariia/itemprop like attributes, class is a common usage of prop falling. Migration tool might help but code modification seems to be unavoidable and tedious.
why we need the pure props?
@Kingwl I guess it might be used to differentiate between class component or Vue.extend from pure functional component.
pure seems to be optimization hint.
https://github.com/vuejs/vue-next/blob/8de1c484ff2c9bab81f1a93fcb58f53859ff0227/packages/core/src/createRenderer.ts#L558-L560
@phanan oh, i forget the pipeline operator syntax :rofl:
you are right, drop it
Although pipeline is still stage 1
I also share some of the concerns @Justineo has.
component authors usually cannot know in advance what attributes are necessary in certain use cases
Without $attrs, we may have to manually filter out all explicit component APIs from props and attach what's left of it to the desired element.
we cannot access components inside directives anymore
This seems to be a fairly common use case in our projects as well.
Filters are gone for good (or can it?)
I personally have never used filters in my projects since Vue 2.0, so for me removing it doesn't hurt.
Really be excited with Vue 3, and looking forward to it.
Weex is built on bottom of Vue.js, so the breaking changes of syntax will not affect Weex actually. But some compatibility work still can't be omitted, mostly for the new package structure and VNode, not syntax.
VNodes are now context-free
By the way, I think it is a good idea and should be insisted. I wish the Component and VNode could be separate, the interactive API between them could be explicit and minimal.
If be more radical, the vdom (or VNode) may not be needed for native-rendering and server-side-rendering scenarios, at least it should not be handled by javascript. For Weex, it's feasible to implement the vdom (VNode) natively by C++, and expose the corresponding APIs to the running context. Moreover, the create/diff process of vdom can also be compiled to WebAssembly, although it may not certainly improve the performance since WebAssembly can't assess DOM API yet, it can be used to generate HTML strings in the server side. However, if component and vnode have so many coupled properties or features, it would be very hard to make the rendering process to take advantage of native platform abilities.
So, I think separate template, component, and vdom is good for long-term evolving, even if it hurts.
This looks a amazing, I finally feel like this is something I can navigate and undestand :-P
Points I want to comment on:
Like others I'm not too sure about the whole droppinf of $attrs and $listeners, and especially automatic interance of attributes.
While $attrs and $listeners can probably be re-implemented in userland pretty easily, the last point could be a point of great pain for people - unless we find a way to easily allow for that to be done in userland as well without requiring to touch every single template in your project?
I'm torn. Filters are usually formatters that people use appliction-wide. When we drop them, people will be forced to implement them as methods, possibly via extensions to the Vue prototype.
currency is a create name for a filter or a data/prop. Sure, naming rules can help here, but the current implementation doesn't have this problems as filters live in there own namespace.I'm not too attached to them, but if we drop them, we need a good guide about how to replace them in a maintainable way.
Awesome, will be very good for performance to make them lazy I imagine. Even though that change requires changes to manually-written render function, the changes are small and easy (and maybe possible to be automated?)
The new API seems great, slim and easy to parse. and I see how attrs and listeners don't fint in there, but could be re-implemented on the instance as mentioned above.
Of course this means, similar to slots, that manually written render components have to be updated, but unlike slots, the change is a little more work. I could imagine that we provide a little helper method that people can wrap their manually written VNodeData objects in to be converted to the new format. That would allow for a quick fix, and can be cleaned manually later.
This is a point that wasn't mentioned at all in the OP, and I can't find anything about them in the source either.
I hope they're not killed like React did when they switched to a class-based syntax. So much of the ecosystem relies on them (most of Vuetify is implemented as mixins I think), so I can imagine it would be a big problem.
Would it be possible to make them a static property on the class that is used by the renderer to apply the mixin after creating the instance? what about beforeCreate mixins, then? How do we make them works with types? I have no idea. :/
And on a lighter note: Not sure how to feel about the fact that Vue 3 will have Portals, which kills the need for my only popular OS library :D :D
How do we make them works with types?
I wrote a little helper for that, could probably be included with vue depending on what the API ends up looking like.
export default mixins(A, B, C).extend({...})
// OR
export default class MyComponent extends mixins(A, B, C) {...}
The only thing affecting Vetur is removal of filter. This is great because I can treat interpolations as JS statements without any custom syntax handling. But also +1 to what @LinusBorg said:
I'm not too attached to them, but if we drop them, we need a good guide about how to replace them in a maintainable way.
I might be able to provide editor support that:
Answering a few concerns:
First, class and style merging behavior remains the same (and now applies to single-root functional components as well).
Second, I think the ability to render arbitrary attributes on the root of a child component is a useful one, but currently it's a very implicit behavior.
The problem I see with the implicit fall-through is that you read the code of a component being passed props, you won't know which ones will be treated as props and which ones will be treated as attributes without knowing what props the component declares:
<!-- is label a prop or an attribute? -->
<Foo label="123" />
In addition, because props declarations are now optional, we actually don't have a way to implicitly extract attributes for component that does not declare props.
Maybe we can differentiate the two (component props vs. native attributes) similar to how we differentiate component events vs. native events with the .attr modifier:
<!-- this is always a prop, although the component *may* explicitly render it as an attribute, there's no guarantee -->
<Foo label="123" />
<!-- this is always an attribute on the component root -->
<Foo label.attr="123" />
In the compiled code, props with .attr modifiers are extracted as:
h(Comp, {
// explicitly merged on to child component root, like `class` and `style`.
attrs: { /* ...* / }
})
Then, we need to consider the case where the component may return a Fragment, or may have a wrapper element and want to take full control of the full-through. In such case, inheritAttrs: false will disable merging for class, style and attrs, and the component author will be able to spread these onto desired element via this.$props.[class | style | attrs]. (this avoids the runtime cost of extracting and allocating memory for $attrs.)
Or, maybe I'm overthinking all this and implicit fall-through is fine (and actually useful). Although, note that the following are orthogonal to whether we keep implicit fall-through or not:
.attr modifier. This allows you to be explicit and not relying on anything implicit.inheritAttrs: false affecting style and class: probably a good idea to make it more consistent.$props when inheritAttrs: false: this allows components to achieve the equivalent of {...props} with v-bind="$props".The only difference is that with implicit fall-through, any props not declared (plus ones with .attr modifier) will be grouped under $props.attrs. Otherwise, only ones with .attr modifier will be grouped in there.
Thoughts?
This is still possible. The vnodes are context-free, but directives are applied in render functions with the component this passed in.
Also re @Justineo : the directive change only affects render functions. Template syntax remains the same.
.pureFunctional components will always update by default due to possible props mutations in a nested property, so using shallow compare by default will be unsafe. Explicitly marking a functional component with .pure = true essentially enables automatic shallow equal shouldUpdate checks.
Btw @octref - I'd like the 3.0 compiler to provide infrastructure for even better IDE support - e.g. template type checking / type-aware completions. Maybe even keep the language service in the repo.
@KaelWD that mixins helper is great, I was actually still thinking about how to deal with it 😂
Adding .attr make me feel Vue templates are not HTML anymore.
Btw @octref - I'd like the 3.0 compiler to provide infrastructure for even better IDE support - e.g. template type checking / type-aware completions. Maybe even keep the language service in the repo.
That's great. I'll take a look of the parser / compiler and let you know what change would it take. If we have Error Tolerance in the core parser I should be able to use it in Vetur.
<!-- is label a prop or an attribute? --> <Foo label="123" />
I think it's implemention detail and should be encapsulated by component authors. As I understand, the separation of content attributes (attrs) and IDL attributes (props) only makes sense for native elements, as we can only specify string values in HTML. While Vue templates can specify any data type with v-bind, we don't need to separate them at least for components API. Component users shouldn't care if a documented prop serves as an attribute or not.
In addition, because props declarations are now optional, we actually don't have a way to implicitly extract attributes for component that does not declare props.
Actually because props declarations are optional so we cannot tell the true semantics of a prop. It feels like you define a function without defining parameter types, but pass in types at each time you call a function and this is kinda weird. I'd rather explicitly define props by component authors instead of let users dig into details.
Will there be an easier way for devtools to access to functional components if we cannot access context of vnodes anymore?
Update: attribute fallthrough will be preserved when the component has declared props (the behavior will be the same as before). In addition, all parent class, style and nativeOn bindings will be in $attrs, so when the child component returns a Fragment, or has inheritAttrs: false, simply doing v-bind="$attrs" or {...this.$attrs} in JSX will place all non-props bindings from parent root node on that node instead.
Thanks for inviting me to this early (and exciting) preview of Vue 3.0!
VNodes
Does the change in VNodes affect the template compiler modules? In NativeScript-Vue we have some syntactic sugar that is handled by the template compiler through modules, I'm guessing these will need to be updated (not a deal breaker, just wondering)
Component in Render Functions
No longer resolves component by string names
This is not a deal breaker, but currently we use render: h => h('frame', [h(App)]) in the default nativescript-vue template (frame is a component), as some IDE's complain about the <Frame> element, and mess up autocompletion (phpstorm/webstorm I'm looking at you...)
In templates, components should be uppercase to differentiate from normal elements.
Does uppercase mean <FOOCOMPONENT> or can we still use PascalCase <FooComponent>?
Slots
Scoped slots and normal slots are now unified. There's no more difference between the two. Inside a component, all slots on
this.$slotswill be functions and all them can be passed arguments.
Will the template syntax for scoped slots change due to this, or is this just an implementation detail/render function specific change?
Filters
I'm not attached to them, but I agree with @LinusBorg about the potential risks of name conflicts.
I haven't had the time to dig through the codebase entirely but just glancing at some parts of it, I'm really liking the new structure, seems a lot easier to follow / contribute to!
Maybe we can differentiate the two (component props vs. native attributes) similar to how we differentiate component events vs. native events with the .attr modifier:
<!-- this is always a prop, although the component *may* explicitly render it as an attribute, there's no guarantee --> <Foo label="123" /> <!-- this is always an attribute on the component root --> <Foo label.attr="123" />
How about <Foo :attrs="{ label: '123'}"> which is more friendly if you want to put a set of aria-* attributes in. And it's not necessary to be converted into { attrs } for render function again.
Component no longer need to delcare props in order to receive props. Everything passed from parent vnode's
data(with the exception of internal properties, i,e.key,ref,slotsandnativeOn*) will be available inthis.$propsand also as the first argument of the render function. This eliminates the need forthis.$attrsandthis.$listeners.When no props are declared on a component, props will not be proxied on the component instance and can only be accessed via
this.$propsor thepropsargument in render functions.
I've read the source but I'm not sure if things are complete yet, but here's my understanding of that:
props always will appear on this.$props or props in a functional component.props for a new component's options, props will also be available on this.props that get transferred onto this are all reactive and can be mutated, but are otherwise considered immutable.Is that correct? If not, can you give an example of the two scenarios in action just so I can get an idea of what the workflow is?
@DanielRosenwasser it's definitely not complete yet.
In a functional component there's no this, so the only way to access its props is via the argument.
Yes.
No, props on this are readonly (both type-wise and implementation-wise).
Here's how a user would specify props types:
interface Data {
foo: string
}
interface Props {
bar: number
}
class Foo extends Component<Data, Props> {
static options = {
props: {
bar: { type: Number, default: 123 }
}
}
data () {
return {
foo: 'hello' // will be an error if type doesn't match interface
}
}
render (props) {
// accessing data
this.foo
this.$data.foo
// accessing props
this.bar
props.bar
this.$props.bar
}
}
A few obvious things to improve here:
If the user provided a Props interface but didn't specify the static props options, the props will still be merged onto this in types but not in implementation.
User has to specify the Props interface for compile-time type checking AND the props options for runtime type checking. It'd be nice if the compile-time types can be inferred from the props options like it does for 2.x, although I haven't figured out how to do that yet.
Note the reason we are designing it like this is because we want to make plain ES usage and TS usage as close as possible. As you can see, an ES2015 version of the above would simply be removing the interfaces and replacing the static options with an assignment.
If we can get a decorator implementation that matches the new stage-2/3 proposal, we can provide decorators that make it simpler:
class Foo extends Component {
@data foo: string = 'hello'
@prop bar: number = 123
}
@prop bar: number = 'baz'
TS2322: Type '"baz"' is not assignable to type 'number'.
@KaelWD fixed ;)
I've come up an alternative component definition. Make Component take two arguments which define data and prop. It would look like:
const propDef = {
bar: { type: Number, default: 123 }
}
const dataDef = (prop) => ({
myData: 'bar',
baz: prop.bar + 1
})
class Foo extends Component(propDef, dataDef) {
// more definition
}
The good part is that this method doesn't require users to duplicate prop/data definition for type and for runtime. But the bad part is also evident that it breaks component definition into several distinct blocks.
Ideally, if we can instruct TypeScript compiler to "inject" some properties to class instance via static field, this might be more idiomatic. By "injection", I mean something like this:
class A extends Component {
static prop = { bar: Number }
method() {
this.bar // property bar is injected to class instance via the static field `prop`
}
}
I like interface and implementation for props separated. This makes it possible to specify compound types as props, and it makes clear that anything you put to options are runtime behaviors. The typings could be simplified a lot as well.
makes it possible to specify compound types as props
Compound types can already be inferred in 2.x, it's non-primitive types that have to be annotated manually.
type: [String, Number] works correctly.
@KaelWD You are right, but afaik it doesn't support some of the more advanced union types, such as string literal unions. Also as you mentioned, it doesn't work for non-primitives, especially object literal / classes.
I guess I meant "complex types" 😛
@yyx990803 Will we be able to assign multiple simultaneous listeners without patterns like prop getters, which are necessary in React? Currently, child components don't have to worry about whether the parent might be listening to the same events, like in this example.
Also, what would you think about removing nativeOn? The issue is that it requires knowledge of the root element of a child component, which is really an implementation detail. If it's a BaseInput component for example, the root element could become a <label>, which then breaks silently breaks all components that were relying on nativeOn.
@yyx990803 With the flat structure, does this mean any prop/attr that starts with on will be interpreted as an event? So in a template, does this mean @click="foo" would do the same thing as on-click="foo"/onClick="foo"?
@yyx990803 With listeners being props, does this mean a listener would have to be declared as a prop to be used? (I'm OK with this.) And if so, would we still need $emit or would we just call the function passed to the prop?
@yyx990803 Regarding:
const Func = (h, props, slots, ctx) => h('div')
I'm wondering if we should keep everything in an object in the 2nd argument, as we do now, so that users don't have to worry about argument order and can just pull out what they need with destructuring.
createAsyncComponent with advanced async components@yyx990803 Does that mean we'd have this kind of API for advanced async components?
createAsyncComponent(() => import('./MyComponent.vue', {
loading: require('./Loading.vue').default
})
resolveComponent questions@yyx990803 So this means globally registered components have no effect on render functions? I'm curious what's the reason for this change, as it seems much less convenient.
If we do need resolveComponent though, should it actually be asynchronous in case the component being resolved is async?
class components being the defaultWould class components be the recommended default for build-step apps? Would there be any cases where users would have to use a class component, instead of the object syntax? If the answer to any of these is yes, I have strong reservations.
Since many features of class are still in flux in the spec, we're opening up the possibility for users to take advantage of these stage-x features, even if we don't rely on them ourselves. For example, React has faced issues on several occasions when something changes in the spec, and so changes in Babel, and everyone's apps are suddenly broken. In the next version of React, I've even heard from Dan and Andrew that they're moving completely away from class components, partly due to these chronic problems (and the awkwardness of JavaScript's classes in many cases).
I also have some other thoughts on the many advantages of the object syntax, particularly from a learning and organization perspective, but I'll hold those for now. 🙂
If we do need resolveComponent though, should it actually be asynchronous in case the component being resolved is async?
AFAIK async component triggers rerender when component is resolved.
@chrisvfritz
High level notes: anything not specifically mentioned is in principle unchanged.
h('button', {
onClick: [handlerA, handlerB]
})
Also when you cloneVNode(vnode, { onClick: foo }), the listeners is merged with existing ones instead of overriding. Same for nativeOn. A component that does not want nativeOn to be placed on its root node should specify inheritAttrs: false and then spread $attrs on to desired root node, which includes all nativeOn listeners.
With the flat structure, does this mean any prop/attr that starts with on will be interpreted as an event?
Inside render functions, yes
So in a template, does this mean
@click="foo"would do the same thing ason-click="foo"/onClick="foo"?
No. Template syntax is irrelevant and does not change.
With listeners being props, does this mean a listener would have to be declared as a prop to be used?
No.
Would we still need
$emitor would we just call the function passed to the prop?
You can still use $emit.
I'm wondering if we should keep everything in an object in the 2nd argument, as we do now, so that users don't have to worry about argument order and can just pull out what they need with destructuring.
Yeah, probably good for normal render fns too.
Does that mean we'd have this kind of API for advanced async components?
Yes (slightly different), see implementation
So this means globally registered components have no effect on render functions?
No. resolveComponent still checks for globally registered components. The change simply moves the part that is coupled to this out of the vdom implementation itself (into user render functions) so that VNodes can be context-free.
If we do need resolveComponent though, should it actually be asynchronous in case the component being resolved is async?
Async components are created inside a wrapper HOC so there's no need for async resolving.
Class will be the new recommended API for any setup. It's designed specifically to be usable in native ES2015 environments without a build step AND without any reliance on stage-x features. The reason is because it serves well consistently for all major setup types:
Plain ES2015 without build step / stage-x features: that's the default.
Babel: same API, but can optionally use class fields or (new) decorators. Stage-x features may change, so we don't encourage it, but you also don't want to prevent users from willingly opt-in.
TS: same API + optional class fields / decorators usage + type inference (important).
Right now, projects not using TS and projects using TS look completely different, and with TS becoming more prevalent, it's going to make it difficult for TS and non-TS users to cross-contribute.
That said, object syntax will still be supported and the user will never have to use classes. (And the compat code is quite simple. Right now in 2.x, what we are doing is actually converting an object component into a constructor internally. 3.0 simply exposes the ability to directly author this constructor using classes.
nativeOn (and maybe inheritAttrs altogether?)@yyx990803 I worry that nativeOn is actually an anti-pattern though. I personally teach people to never use it, since you can accidentally break any component by changing its root node. And unlike attributes passed to a component, it actually requires a refactor to remove the .native in the parent after using v-bind="$attrs"/{...this.$attrs} and inheritAttrs: false in the parent.
In general, maybe it would be best to force the explicitness, rather than having parent and child components coupled by default, with the parent making assumptions about the root node of the child? When we first shipped Vue 2.0, it wasn't easy to choose an element to pass all attrs/listeners to, which is why implicit attribute passing and .native were useful. Now we've solved that problem and moving to explicitness, even when you want to pass to the root node, is a really quick refactor, so I feel like those features are no longer necessary.
@yyx990803
So in a template, does this mean
@click="foo"would do the same thing ason-click="foo"/onClick="foo"?
No. Template syntax is irrelevant and does not change.
How would on-click="foo" be interpreted in a template then? Would it simply be ignored? Translated to a prop also called onClick?
$emit maybe?)What I like about the props/attrs behavior you suggested is that if you define props, then anything that's not defined there will be in attrs, creating a nice split between the API of _this_ component and API that should just be transparently passed through to a child element. If v-bind="$attrs" will also bind listeners, should we recommend declaring those listeners as props, so that the same clean separation exists for listeners as well?
And if that _is_ the best practice, I'm wondering if it might be best to get rid of $emit and just recommend calling those listeners directly, thus reducing our API surface area.
That API looks great. 🙂 I have slight concerns about renaming the component attribute to factory, but I understand why you did it. What would you think about something like asyncComponent or componentFunction instead though? That way, we still communicate that they can't just use a raw component definition, but it can still be more meaningful than factory to a lot of people, particularly beginners.
class componentsClass will be the new recommended API for any setup.
OK, then my concern still remains actually. I do like this a lot:
It's designed specifically to be usable in native ES2015 environments without a build step AND without any reliance on stage-x features.
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
And even if _we_ only use features in native class, when many users pull themselves back to the Babel implementation to get newer stage-x features, we end up with 3 different implementations of class (the Babel one, the native one, and the TypeScript one) being used in the wild, with many, subtle or not-so-subtle behavior differences.
And finally a (perhaps unjustified) fear. The ES class implementation is particlarly hacky within engines _and_ a relatively new feature, so I worry there might be many subtle behavior differences and optimization choices between browser engines that we'll keep discovering and have to deal with.
Right now, projects not using TS and projects using TS look completely different, and with TS becoming more prevalent, it's going to make it difficult for TS and non-TS users to cross-contribute.
I agree this is a problem, but I worry we might be fixing a fragmented ecosystem by creating a frequently broken one. I'm definitely not a TS expert, so I really don't know what other kinds of options we might have, but if there's literally any other way to improve support - even relying on an as-yet-unreleased feature of TypeScript - I feel like we should strongly consider it.
Finally, this also reminds me a little of what Angular went through. They wanted first-class TypeScript support, while also allowing anyone to use Babel just as easily. Keeping both happy turned out to be so difficult that at this point, they're not even pretending to support or recommend JavaScript anymore. I'm not suggesting it's impossible to offer a consistent, first-class experience for both. I just don't know where all the landmines are, so worry about falling into the same traps. 😕
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
Honestly, it's like throwing the baby out with the water if we don't use classes because of this. It's a valid concern, but only for the now. Note 3.0 is designed for the future where these proposals are likely more stable than they are now and breaking changes becoming less frequent (not like they are actually frequent now). What's more important is we make it clear in our docs that we are aware of these stage-x features that can be used, but there's risk and the user should be responsible for opting into such risk.
And even if we only use features in native class, when many users pull themselves back to the Babel implementation to get newer stage-x features, we end up with 3 different implementations of class (the Babel one, the native one, and the TypeScript one) being used in the wild, with many, subtle or not-so-subtle behavior differences.
Unless the user wants to support IE11, their Babel / TS setup will emit native classes so this is not really a problem. Also see implementation notes below.
And finally a (perhaps unjustified) fear. The ES class implementation is particlarly hacky within engines and a relatively new feature, so I worry there might be many subtle behavior differences and optimization choices between browser engines that we'll keep discovering and have to deal with.
I think this is unfounded. Classes isn't really that new - earliest browser support shipped in mid 2015 (Edge 12) and it became available in stable versions of all major evergreen browsers in March 2016. So that's two and half years since they've been supported in these major browsers and I can't really recall any "subtle behavior differences" that would affect us. In fact, our implementation doesn't even care if it's a native class or not. The code treats a component as a good old constructor function with a prototype and can be newed.
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
I think the worry is unwarranted. Lib authors can use any setup they want and compile down to ES2015. Users can use any setup to consume the distribution. Many packages in npm today are written in TS but compiled down to JS. They don't force the user into using TS but even gives them correct auto-completion and error checking in IDE.
most apps just break sometimes, when part of the spec changes.
Not really. Let's say you use a babel plugin for a stage 1/2 proposal. The version is locked and your code compiles. When the spec change, you either do not upgrade your babel plugin or you upgrade both your babel plugin and your code. It's an acknowledged price you pay for using stage 1 proposal. And this is a babel/TS-experimental-feature issue, not a Vue issue.
Also given the timeline, I feel the decorator spec will be stable enough when Vue 3.0 is out.
@chrisvfritz I work on TypeScript, but I hope you can trust this is in good faith. 🙂
I really don't want to push the Vue community into anything that would negatively affect it for the sake of TypeScript support, so I have some of the same concerns you have on the Vue community's behalf; however, the truth of it is that so many of the things Vue's API does today is to just construct a class without using any language syntax. It feels more comfortable from a pure ES5 world, but as time goes on the concept will look stranger to newer JS users who come from other languages or who already know about classes. Anecdotally, I have friends who've used some frameworks that started in the ES3-era whose reactivity model was based around calling .get() and .set(). They now find it strange in a world with getters and setters.
In fact, most of the basic concepts boil down to something simpler when you use classes. methods are now just instance methods. computeds are just get-ers and set-ers. The name field can just be the class name. As a small bonus, these things don't need to be separated by commas. 😉
And while I get the concern over ECMA 262 proposal churn, as an occasional TC39 attendee, I think you'll see less of this over time as these features stabilize - something @yyx990803 and @octref partially alluded to.
I've even heard from Dan and Andrew that they're moving completely away from class components, partly due to these chronic problems (and the awkwardness of JavaScript's classes in many cases
Admittedly I'm not the expert on component models here; I'd be curious to see what they have as an alternative, but I'm very surprised given that the community very quickly switched over from createClass even without things like auto-binding this on methods.
if there's literally any other way to improve support - even relying on an as-yet-unreleased feature of TypeScript - I feel like we should strongly consider it.
This is tough - I have spent a lot of hours trying to make things work better between Vue and TypeScript with the goal of making it so that the Vue experience is 1:1 between JavaScript and TypeScript users. I don't have any ideas that would significantly improve the experience around the current API. In short, my goal was always to make it so the Vue ecosystem wouldn't have to accommodate TypeScript, but rather that the TypeScript language would find a way to accommodate Vue. We have that in some capacity, and it's been helpful for tooling in Vetur. But:
We're working on driving UX improvements from our side. But on the whole, if there's an opportunity to use modern features that's largely backwards compatible, and it opens up the chance for things like
this in classes (e.g. Flow).then I feel like this is a reasonable direction for Vue to take.
The ES class implementation is particularly hacky within engines
I can't speak to that (I'm not an engine person) but I'm wondering what you mean. Maybe something we can chat about that elsewhere. 🙂
I was thinking a bit more about the render function signature and would like some feedback.
h as the first argument?Please vote with thumbs up for removing it, and thumbs down for keeping it.
Since in 3.0 VNodes are context free, it's no longer necessary to use the instance-bound h function in render functions. Instead, we can just use the global h imported from Vue.
Relying on instance specific h makes it necessary to always pass along the h when you want to split part of the render function into another function, which can be really annoying. Importing the global h allows you to just do it once and forget about it.
h injection has been annoying to deal with in JSX. We have some pretty hacky logic for automatic h injection in our current JSX implementation. Importing instead of passing argument allows us to get rid of that problem entirely.
Assuming we will be importing h in a reasonable amount of cases, we are essentially wasting one argument position in those cases. Removing the argument and enforcing importing h everywhere makes usage consistent.
Incompatibility with 2.x API. Although this can easily be made compatible via an runtime adaptor or automatic codemods.
Always need to import h when using render functions. For end users, this isn't that bad and is balanced by the benefit mentioned above.
This has another implication for library authors, because they will be importing h from Vue as a peer dependency, this requires them to use a correct externals configuration when distributing their libraries. However, externals configuration will likely become a necessity for 3.0 compatible libs because we will have a lot of the framework become tree-shakable, and for libs to be able to use these tree-shakable features, they will always need correct externals setup (e.g. import { h } from 'vue' should be preserved in ESM builds but converted to const { h } = Vue in global builds.) The configuration can be standardized via Vue CLI or a library boilerplate.
How will the devtools be able to go from a DOM element to the corresponding component instance if the vnodes don't have any access to it anymore? Also if I remember correctly there is a PR to add the context for functional components to be able to inspect their props.
On another topic, I think it would be nice to have an official way of "pre-rendering" the components tree to gather prefetching data when doing SSR. Ideally much lighter than a real render, with ways to mock global/local properties and methods to speed it up, maybe with an API libraries can use to mock themselves.
This would hugely improve the SSR story because currently we are limited to the routes components. I recently made some experimentation with this in the vue-apollo SSR API that allows the user to prefetch all the GraphQL queries in his app without manually adding them if they are in rotue sub-components (or even outside of router views). However, I think it would be better as an official API so it's less exposed to breaking due to Vue internal changes.
Additional considerations for h removal and render functions signatures:
First of all we are making render functions signatures for stateful vs. functional components exactly the same.
With h always taking up the first argument position, render functions would look clunky with flat arguments:
render (h, props, slots, attrs) {
// ...
}
Which is what lead to @chrisvfritz 's suggestion of using a context object:
render (h, { props }) {
// ...
}
Side note: in 3.x functional components don't really need the
ctxobject anymore. (I've always thought about the functional render context being a bit messy). With the new implementation,props,slotsandattrsare all you need to transparently pass everything down to a child component. And if a component needs access to its parent, use a stateful one instead (as the performance has improved greatly).
So, after h removal, we have the following options:
render (props, slots, attrs) {
// ...
}
vs.
render ({ props, slots, attrs }) {
}
attrs is going to be a rarely used value, as it's automatically merged onto the root node and needed only when returning fragments or with inheritAttrs: false. (It's also accessible via this.$attrs.) So in most cases you actually end up with just two arguments, or even just one if your component doesn't deal with slots:
render (props, slots) {
// ...
}
render (props) {
// ...
}
The benefit of this over a context object:
No need for destructuring syntax. Note a context object also makes ES5 usage more cumbersome due to the lack of destructuring.
No need to allocate an additional object and destructuring from it for each render. Tiny cost, but it adds up.
Easier TS typing. In TS, typing object destructuring is a bit annoying.
So I'm actually inclined to use a flat, no h render function signature:
render (props, slots, attrs) {
// ...
}
Thoughts?
@Akryum we will still expose element.__vue__ in dev mode. I don't recall devtools relying on vnode.context, or do we?
I need to check! :smile_cat:
@Akryum I just did a grep on dev branch and looks like we don't ;)
Great! Also will it be possible to store the functional components in dev mode with their props at each render? I think the current implementation in the devtools is quite fragile (and props inspect doesn't even work yet).
About SSR I forgot to write that the vue-apollo implementation also recognizes a special attribute like no-prefetch which skips a components sub-tree to optimize the tree walking if it is known that no queries will be found there.
I believe having h as the first argument is convenient, but there are more pros to removing it. Advanced users who are more likely to touch render functions in the first place are not going to mind an extra import for the global h. As for the other arguments, if it's just the 3 props, slots, attrs then I don't think grouping them into a ctx object is worthwhile.
@yyx990803 @octref @DanielRosenwasser Thanks for all of your feedback. 🙂 I do see some of the advantages of classes, but I guess it's the "likely more stable" part that has me nervous. Building the framework around classes has proven to be painful for React, so it makes me nervous to embrace them out of the hope that things will be different in the future, even though we have no control over changes to the class spec - or even unstable additions that, if the React community is any indication, users will embrace even despite loud warnings when it's essential for a useful pattern.
I trust that you all know more about this than me though, so if you all think it will really be OK, I'll drop this.
h as the first argument@yyx990803 That sounds fine to me. I'd probably still lean towards destructuring though.
No need for destructuring syntax. Note a context object also makes ES5 usage more cumbersome due to the lack of destructuring.
It's slightly more cumbersome, but I also very rarely see render functions in an app using ES5.
No need to allocate an additional object and destructuring from it for each render. Tiny cost, but it adds up.
I haven't benchmarked this to know whether it adds up to anything significant, so nothing to add here.
Easier TS typing. In TS, typing object destructuring is a bit annoying.
Still seems worth it to me. With destructuring, we remove the annoyance of having to remember argument order for _all_ users, in exchange for a roughly equal annoyance for only _some_ users.
@yyx990803 What did you think of what I proposed here?
h 😅Similar to how Babel handles polyfills, could we detect JSX or when h is called as a function in a Babel plugin, then automatically add import { h } from Vue to the module (unless h is already defined in the module scope)? Then in the vast majority of cases, users could just write JSX without having to worry about what it compiles down to.
The downside is that when using h directly instead of JSX, this will appear to be very magical behavior.
Thoughts?
@yyx990803 Currently in our functional components, we can return an array of vnodes. To me, that's much more ideal than introducing fragments as public API, which would be a new concept that also comes with new syntax. In Vue 3, what would you think about just allowing render functions to return arrays, then having Vue create a fragment under-the-hood as an implementation detail?
Looks like I'm the only one super excited about flat data format for VNodes. There's always a lot of people having trouble with JSX spreading and this is a great quality-of-life improvement for JSX users.
@yyx990803 OK, this should hopefully help with class and the general unpredictability of the ES spec. Talking about this kind of issue with folks at the Framework Maintainer's summit, Tom Dale managed to set up a direct, monthly line of communication with TC39 for the frontend frameworks. See the invite below:
Hi Chris,
I'm working with the ECMA TC39 team to bring together a representative group of people who author popular JS frameworks and libraries to have a discussion about TC39 proposals, and JavaScript pain points and feature requests. We'd love for you to be a part of it as a representative for Vue.
We want to create a communication channel to have monthly discussions since previous out-of-TC39 calls have proven themselves productive. We intend on holding a monthly call, and want to get the days people are available through this Doodle: https://doodle.com/poll/bxyv5ie2ytyux6xf.
Please let me know if this is something that you'd be interested in, or if you know of another good representative.
Thanks!
Chelsea
I'm definitely not the best person to represent Vue in this kind of thing - that would probably be you Evan, right?
@nickmessing Regarding JSX, should we have some custom merge syntax to replace use of the spread operator? Otherwise, even with a flat data structure, won't merging for class, style, and events not work correctly?
If I understand correctly, the flat data structure should make those behave just the same as normal props and attributes insofar as merging is concerned.
@chrisvfritz, we already have a runtime merge helper, but the object that needs to be passed to spread has to be structured like this:
{
attrs: { id: 'foo' },
domProps: { innerHTML: '' },
on: { click: foo },
key: 'foo',
ref: 'bar'
}
With flat data, users can spread object structured just like normal attributes they write.
@chrisvfritz
I somehow missed the comment about removing nativeOn. Sounds like a good idea, I'll look into it.
Re: "Using arrays instead of fragments in the public API": that's already the case. You can just return arrays in a render function and they get converted to fragments implicitly.
RE: TC39: I actually agreed to do this on another occasion when discussing proxy stuff with Daniel Ehrenberg - I think that might be the same thing. Feel free to let them contact me instead.
@chrisvfritz just to clarify, by deprecating nativeOn are you proposing that we:
inheritAttrs optionv-bind="$attrs" to desired root nodeNote that in addition to nativeOn listeners we also have class and style bindings which are specially treated (they are always merged down to the child component root, and I think we should preserve that).
@nickmessing Great to hear that the runtime merge helper already correctly handles class, style, and events instead of overwriting! 🙂 Thanks for clarifying.
@yyx990803 That is exactly what I'm proposing. And just like with attributes, the user could prevent a listener from being added to $attrs by declaring it as a prop. I'd personally prefer not to make exceptions for class and style though, because I think the behavior isn't intuitive for most people and has even caused significant confusion in some apps I've worked on - particularly in base element wrappers (e.g. a BaseInput component where the root element is actually a label, but the dev expects to be adding a class/style only to the input element). Does that make sense?
I'd like to get people's opinion on the idea of removing inheritAttrs as outlined here. I do occasionally find it convenient to be able to add a class or styling on a child component for layout or ad-hoc tweaks, but I also see how the 2.x behavior can become inconsistent especially with fragments being common in 3.0. Thoughts?
Personally I would much rather prefer inconsistence here, than inconvenience by having to explicitly pass class to the child component as a prop. Being them merged automagically is very convenient and feels kinda natural. I use it all the time to have my components independent of the place they’re being used in. This is a very good candidate for an RFC imo.
@michalsnik Note that with this proposal, you wouldn't have to explicitly define class as a prop - you could just add v-bind="$attrs" to the root element for any components where you wanted this behavior for class and all other attributes. When this is desired for a 3rd party component, it's also an extremely simple pull request and we could recommend in the style guide that most components have attrs passed through to some element.
One big problem with the implicit behavior is that when it isn't desired, really ugly hacks are necessary to work around it. For example, span.my-class to prevent targeting the class also being added to a root div, which further couples the components together. For style, you'd have to define a special childStyle or similar prop. And on top of all this, when implicit attribute inheritance only works for class and style, people will become very confused thinking the behavior didn't change, but wondering what they're doing wrong or if this is some kind of bug in Vue, where the class is being added, but the placeholder is not, etc.
It just seems like a pretty huge cost for a very tiny inconvenience.
And as Evan mentioned, people would never be able to rely on the behavior, since any component could return multiple root nodes. This means for 3rd party components, changing the template or render function in a way that does not affect the public interface or behavior would still result in breaking changes for users, because we'd have allowed users to reach in and bypass the public interface.
Now that I think about it, it is indeed going to be problematic if we keep the implicit class/style fallthrough behavior. Most of the time, we can class/style to style the child component assuming it has a single root node AND that the class/style is going to be merged onto the root node. However, it's possible that:
$attrs on one of the nodes in the returned fragment)inheritAttrs: false and spreads $attrs on a non-root node (e.g. <input> inside <label>)In either case above, the layout is not going to work as intended. The only way to guarantee desired styling behavior is to always wrap the child component in a <div> or <span> before styling it.
Incidentally, this also avoids the problem of the parent accidentally passing down a class that is used inside the child's scoped styles.
@yyx990803 Just to confirm and summarize, does that mean the following will be true for Vue 3?
inheritAttrs will be removednativeOn/.native will be removedclass and stylev-bind="$attrs"/{...this.$attrs}$attrs will include all attributes and events not defined as a propDid I get anything wrong?
Could we define events like we do for props? They would be omitted from $attrs. Also it might help with HOC and typing, for example this.$emit.myEvent('foo') and docs.
IMO component consumers will expect the fall-through behavior. The real difference is whether component author have to spread $attrs manually by default, or they only need to do so when they choose to implement a component as a fragment or other special occasions.
If we prevent the fall-through behavior by default, I will expect there will be less 3rd party libs which allow passing arbitrary attributes to DOM, which may have an impact on a11y and other aspects I mentioned in my initial comment.
If component authors will need to spread $attrs or maintain an “allow list” in most cases, then I’d expect the current behavior (fall through by default) to be preserved.
If we prevent the fall-through behavior by default, I will expect there will be less 3rd party libs which allow passing arbitrary attributes to DOM
@Justineo I agree with your concerns, but I feel like this is an education issue we can solve with documentation, encouraging people to always define a pass-through with v-bind="$attrs"/{...this.$attrs}. We can even create a strongly-recommended rule in eslint-plugin-vue. And even for libraries that ignore documentation and don't use the ESLint plugin, it's a 1-line, 5-second change per component, meaning a medium-sized lib of 50 components will still only take 5 minutes to submit a PR to. To me, that seems like a pretty reasonable trade-off in exchange for explicitness, making sure that attributes and listeners are always passed to the _correct_ element.
I think it's an education issue in either way. We can also educate users that if you don't explicitly take over $attrs they'll be spread into the root element by default...maybe it's only not so intuitive for users coming from React world...
And the current behavior only fails when component authors didn't do anything to redirect $attrs to the correct element, in which case the explicit method would also fail.
I agree that making small changes to components are trivial. But if we are using a 3rd party component lib we have no guarantee whether/when the PR is gonna be merged and published in a new version, while currently it doesn't require users to ask for permission to spread $attrs and it just works in most cases.
@Justineo
maybe it's only not so intuitive for users coming from React world...
It's not just React users, unfortunately. Probably most new Vue developers I work with are also surprised by this behavior.
And the current behavior only fails when component authors didn't do anything to redirect $attrs to the correct element, in which case the explicit method would also fail.
I'd rather have something work or not work _consistently_, rather than seeming to work _sometimes_ while very likely to break at any time, even in a patch release, because library authors will accidentally make a breaking change with a different root node or making the root node a fragment, both of which will be common (I know I've been affected by the former with Vue 2). Until the library moves to explicitness, you're essentially using an unstable API.
if we are using a 3rd party component lib we have no guarantee whether/when the PR is gonna be merged and published in a new version
If adding v-bind="$attrs" were a particularly difficult/complex change to review, I think that would be something to consider, but it's not. It's either on the correct element or not, so should be an easy merge. If a library is so unmaintained that not even simple and obvious improvements are being merged, that seems like a separate issue.
@yyx990803 As an aside, would it be possible to see a kitchen sink example of the proposed class-based component API?
@chrisvfritz created an issue for that: https://github.com/vuejs/vue-next/issues/4
I'd rather have something work or not work consistently.
I'd prefer a consistent behavior as I don't want to jump into component definition to check if a custom prop can be added or not. It's better to never put extra prop/attrs unless component API/docs explicitly specifies so.
It's better to never put extra prop/attrs unless component API/docs explicitly specifies so.
@znck For the reasons @Justineo mentioned, I _do_ think there should be an expectation that unless a component renders null or only renders children, it should _always_ pass arbitrary attrs/events to some element. Otherwise, accessibility (e.g. aria-label), e2e testing (e.g. data-testid), etc become much more complicated. Does that make sense?
I have some different thoughts. 😅
First it's great to support fragment and portal. But at the same time, we break up, at least, weaken the "future ready" feature that Vue components could be built into native web components. Because in 3.0, we are not sure a Vue component has a root DOM element. If we couldn't get a way to build fragment/portal component into native web components someway, I would feel like we leave future web standard a little more far away.
Second, In web platform, I suggest to make global html attributes like style, class, aria-* and native DOM events always meaningful and configurable: that's also what native web components do. And keep the syntax as same as possible. It follows users' intuitions. Maybe we can consider introducing some new symbol/prefix for custom component event.
And a small question: I'm not sure what the render functions will be if I write more than 2 custom directives like <comp v-foo="x" v-bar="y">. Does the order matter?
Thanks.
@Jinjiang
Because in 3.0, we are not sure a Vue component has a root DOM element.
Native web components always have its own custom element / shadow root that can serve as the root element, so I don't really think that's a problem.
I suggest to make global html attributes like style, class, aria-* and native DOM events always meaningful and configurable
With the introduction of fragments, we can no longer rely on implicit behavior. I think the solution is making it a best practice to always spread $attrs if you intend to ship your component as a library. (private components are fine because the user can modify them anytime).
And a small question: I'm not sure what the render functions will be if I write more than 2 custom directives like
. Does the order matter?
It will look something like this:
const comp = resolveComponent(this, 'comp')
const foo = resolveDirective(this, 'foo')
const bar = resolveDirective(this, 'bar')
return applyDirectives(
h(comp),
this,
[foo, this.x],
[bar, this.y]
)
I think the solution is making it a best practice to always spread
$attrsif you intend to ship your component as a library.
I guess there would be quite amount of mis-operations when:
style but forget $attrs, orclass to a fragment component or "closed" (without spreading $attrs) component but see nothing happened without any tips.Maybe it's better to make $attrs implicit and print warnings when its a fragment/portal/closed component IMO. It helps both authors and users to make less mistakes.
Btw, till now, I sometimes mis-wrote @click.native as @click and get lost for a while. If possible, I wish 3.0 a better experience. 😂
Thanks.
write a component which is designed to accept style but forget $attrs
The best/common practice would be always merging $attrs in its entirety. It would be a conscious decision to only allow style falling through so I don't think it's possible to "forget".
use a class to a fragment component or "closed (without spreading $attrs)" component but see nothing happened without any tips.
This we can actually detect and throw a warning for (if $attrs is not empty, but never accessed during render).
This we can actually detect and throw a warning for (if $attrs is not empty, but never accessed during render).
What if this is intentional and all allowed attributes are declared in props instead?
Maybe it's better to make
$attrsimplicit and print warnings when its a fragment/portal/closed component IMO.
@Jinjiang I really like the idea of a warning for users, but with any implicit pass-through, I really think we're only _delaying_ pain by allowing users to rely on an unstable API that the component author didn't explicitly define.
Btw, till now, I sometimes mis-wrote @click.native as @click and get lost for a while. If possible, I wish 3.0 a better experience. 😂
@Jinjiang Me too. 😅 But if we remove all implicit pass-through, that problem is solved. 😉
What if this is intentional and all allowed attributes are declared in props instead?
@Justineo Since there will be valid use cases for not binding $attrs (null renders and components that only render children), I think we could provide a neutral warning that doesn't imply the component author definitely did something wrong. Maybe something like:
Component \
What do you think?
@Justineo Also, I _think_ null and children renders are the only cases where intentionally not spreading $attrs would be valid, but maybe I've missed a use case. Can you think of a situation where a component has its own markup, but the author might not want to spread $attrs?
@yyx990803 And actually, is there potentially a way we could detect if a component has generated its own markup, rather than rendering null or just its children? That way, we could have more precise warnings. If a component _does_ generate its own markup, then:
Component \
And if the component _doesn't_ generate its own markup:
Component \
- propA (String)
- propB (Number)
- propC (Boolean)
What do you think?
I'm not sure if there will be such use cases that the author want to hand-pick all allowed attributes and declare them inside props so nothing in $attrs will be touched. Technically it is a valid usage but users may come across the warning.
I'm not sure if there will be such use cases that the author want to hand-pick all allowed attributes and declare them inside props so nothing in $attrs will be touched. Technically it is a valid usage but users may come across the warning.
@Justineo Beyond the unnecessary clutter this would add to the component definition (especially for the minimum 21 aria attributes every element must accept), wouldn't this also prevent unpredictable attributes from being used (e.g. data-testid for e2e tests)? For that reason, wouldn't hand-picking attributes always be an anti-pattern?
wouldn't hand-picking attributes always be an anti-pattern?
Yes. I'm just saying it's valid in the current mechanism, despite being an anti-pattern. On a second thought it may be good enough to suggest them spread $attrs instead.
Another issue might be whether we suggest authors to always spread the $attrs object in the warning message, or give them some space to explicitly allow/disallow the attributes end up with in the target element. I saw some React component libs only allow data-*/aria-*/role to be passed through.
@Justineo
suggest authors to always spread the $attrs object in the warning message
That would be my preference, as I'm still not convinced there's a valid use case for whitelisting. I feel like there can always be exceptions where an attribute beyond data-*/aria-*/role is necessary due to a strange requirement in a vendor library or specific accessibility technology.
give them some space to explicitly allow/disallow the attributes end up with in the target element. I saw some React component libs only allow data-/aria-/role to be passed through.
If users _really_ need to do attribute whitelisting (or blacklisting), they technically already can, e.g. with:
pickBy(
this.$attrs,
(value, key) =>
/^(data|aria)[A-Z]/.test(key) ||
['role'].includes(key)
)
And this would still trigger the getter for $attrs, thus disabling the warning, so I think that should keep those users from complaining.
As this thread has become super long and hard to navigate, let's open separate issues to discuss specific topics.
Open separate issue for attrs fallthrough behavior: #5
Closing in favor of public RFCs.
Most helpful comment
/cc @vuejs/collaborators @octref @Atinux @DanielRosenwasser @alexchopin @clarkdo @pi0 @chenjiahan @johnleider @Leopoldthecoder @KaelWD @icarusion @rstoenescu @rigor789 @Hanks10100
You have been added to this repo either because you are a core team member or as maintainer of notable projects that builds on top of Vue. I'm giving you early access to the WIP repo to provide early feedback on proposed breaking changes to Vue internals. What I am most interested in is how much would the above changes affect your project - for each change, how difficult would it be to adapt to it? Would any of these be a deal breaker? Any of these would really help? Any additional ideas? Any type of feedback is welcome - also keep in mind that this is very early stage and nothing is set in stone yet.