-
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
Add "emits" component option #16
Conversation
This comment has been minimized.
This comment has been minimized.
Thanks for the proposal! Another concern is that this API isn't useful for production code: we probably don't want to check emitted events as we don't for props. So checking API might add runtime overhead. |
@niko278 |
I like this. I've referred to this in #26 - specifically, this option can serve as an indication that a certain event listener should be consumed by this specific component and should not be passed down in Some other feedback:
|
I'm all for |
@yyx990803 |
@niko278 great - could you update the PR to reflect the suggested changes? |
For emits, do you really need validation? it is the component that defines the values emitted, and not the consumer of the events (unlike props where the consumer is passing a value to the component) Maybe the definition of an emitter should be like: emits {
'foo-event': {
args: [
// Argument 1
[Number],
// Argument 2
[Number, String],
// Argument 3
[Event]
]
}
} Where args is an array of argument positions and expected types for that position. If an event emits no arguments, then one would specify an empty array (or perhaps |
This can potentially help typing the emit function too right? 😍 |
When combining with #137, and one has a custom event Perhaps all custom events should be prefixed (camelCase, PascalCase, kebab-case) so there cannot be any conflicts with native events? This would be considered a breaking change, but might be the only way around this now that the |
It is interesting whether it is possible to somehow combine this with the TypeScript syntax. To specify a more specific type of data that is transmitted with the event. And so that the component component listener can tell what is coming. |
@tmorehouse Based what I'm reading/understanding, if you omit |
@sirlancelot how would the following work with the removal of <custom-input @input.native="nativeHandler" @input="customHandler" :value="value"></custom-input> |
This is my opinion, but I wouldn't expect that scenario to be supported. To me it looks rather confusing. I have to imagine there's a better way to implement that component as it would seem like it's doing more than one thing. |
Imagine a custom input that debounces the v-model as the user type, and emits a custom input event after the debouncing timeout. But someone wants to alter the text as the user is typing (ie. transform to upper case), of which they need access to the native event object (the custom The majority of users expect custom input components to emit the Here is the same example from above written another way for clarity: <custom-input @input.native="nativeHandler" v-model="value"></custom-input> Where |
@tmorehouse v-model events changed in Vue 3. If a component supports |
@jods4 |
Sure, the point kind of was that v-model forces the event names. I'd rather user different event names to start with: if it's not the same thing, don't give it the same name. It avoids a lot of confusion and problems in the long run. |
In some cases that is not easy... when the name Unless Vue decides to dictate that all custom events need to be kebab-case/prefixed.... but then that just breaks when using |
Architecting an app is never easy, but I have to say that in the 5+ years I've been using Vue.js to write professionally and personally, I have never encountered the problem you're describing. That's not to imply it's not a valid scenario, but to point out that it's possible to avoid while still maintaining a manageable codebase. But I digress, we've gone a little off-topic... |
@yyx990803 didn't realize it was already in! Cool, I'm gonna try it out. Do you think |
@jods4 it would be best to keep the API consistent with v2 to avoid unnecessary differences. |
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. |
The proposed syntax allows strong typing of parameters but not of return values, right? In practice many events are void, but Vue returns an array of all values returned by registered handlers. // Asks parent if he vetoes the update of some data
let result = emit("canSave", entity); // should be boolean[] but any[] in current proposal
if (result.some(x => x === true)) return; Is custom parameter validation actually useful? Props are meant to be set by consumers of the component. It makes sense to be able to apply validation to their inputs, i.e. that the api is used correctly. Emits are sent by developpers of the component. It's an output and we validate that api behaves correctly with unit tests. I'd rather have no validation and strong typing, maybe like this: {
emits: {
// never called but defines Typescript types for the emit call and returned values
canSave(entity: object) { return true }, // emit("canSave", object) => boolean[]
getUuid() { return "" }, // emit("getUuid") => string[]
untyped: null, // emit("untyped", any) => any[]
// alternatively, works the same
canSave: null as (entity: object) => boolean,
getUuid: null as () => string,
}
} EDIT: as a bonus, the first syntax could actually define a default listener/return value when there's no listener. This can be handy if you build component with a lot of default overridable behavior (e.g. how Hibernate works on the back-end). Your form component could have EDIT2: I focused on the emitting side, but having a return type would also enable tooling like Vetur validate that the listeners are of correct type, including the value they return. Validating consumers usage is IMHO more important than validating internal implementation. |
@jods note that we shouldn't design an API that is solely geared for typing. Your proposed usage, while potentially provides a bit more type coverage, introduces an alternative that serves nothing but types, while blocking another runtime feature. Users not using TypeScript will most likely be confused why the function syntax is used for typing (which to them equals nothing) but not for runtime validation. You would certainly prefer the typing because you are a heavy TS user, but considering the overall user demographics of Vue I don't think that trade-off makes sense. |
@yyx990803 That was only a proposal. Maybe someone can come up with a better alternative. The key point here is that the proposal doesn't provide strong typing for event results for neither the emitter nor the listener (assuming one day Vetur may typecheck event). One message is that Vue 3 is designed for TS and as a heavy TS user (you're right), seeing BTW It's not the key point here but I'm gonna reiterate that I'm not sold on the "other runtime feature", to me the idea just feels weird. Input validation is a given, output not so much. Typing aside, I personally would have more use for a "default" event listener feature rather than output validation. So far since alpha.11 is out, all my events look like |
Here's another idea then: reuse the same shape as emits: {
// parameter is a number, implicitely no return value (void)
click: Number,
// couple is [payload type, return type]
canSave: [Object, Boolean],
// more complex scenarios can use an object
complex: {
type: Date, // payload type
return: Number, // return type
validator: (date) => (+date) > +(new Date), // optional validation of payload
required: true, // indicates that consumers are supposed to always provide a listener
default: (date) => 0, // optional listener that will be called if consumer didn't provide at least one
}
} |
Events can emit multiple values though: this.$emit('foo-event', 'string', 123, { foo: 'bar', bar: 'baz' }) |
@tmorehouse interesting, I wasn't aware. I assume you can only handle them by passing a function that takes multiple args? As inline expression only has access to a single That doesn't change much, though does it? I would be fine with using the object form when declaring a return value (one-way is the most common case here). So: emits: {
// Single parameter ($event)
click: Number,
// Multiple parameters
canSave: [Object, Boolean, Array],
// more complex scenarios can use an object
complex: {
// same as shorthand: could be a single type or an array
parameters: Date,
// optional return type
return: Number,
// optional validation of parameters
validator: (date, offset) => (+date) > offset+(new Date),
// indicates that consumers are supposed to always provide a listener
required: true,
// optional listener that will be called if consumer didn't provide at least one
default: (date, offset) => 0,
}
} |
Yeah, you need a method handler when accepting multiple args. we use it quite a bit. The first argument is available inline as I've never used the return value from |
Vue always returns an array with all results from every listener. What you do with them is up to you. Some other frameworks differentiate the two concepts. Take Aurelia for example, it has one syntax for "events" (as you use them) and a different syntax for "delegates", which are just a single function that the component can call back. Vue doesn't make a difference but since it provides the values returned by events, it can be used in both situations. |
delegates in vue are a prop which takes a function to be called. i use this pattern basically in all my components that want to delegate behavior (like forms).
the event pattern is inherently fire and forget. feedback from listeners to the event source should always be optional and can occur at any point in time after the event was fired. if you need a feedback channel, you can add methods to the event, which the listener can use (which may throw or do nothing depending on the state of the event source). i would consider using the return values of listeners an anti pattern, which may work well in very specific use cases, but a method on an event works just as well and communicates the control flow much better. |
There's a slight difference in that you can only bind a method name and not use a function expression like you can using But ok, that works well enough.
Then let's revisit this design choice: let's drop event return values from Vue 3. Or at least mark it obsolete, tell everyone it's only there to help backcompat of Vue 2 users and shouldn't be used and that's why it's not as well supported as other features. |
@jods4 in case you didn't know... when you declare a props named |
does it make sense to return anything from |
@yyx990803 Right, that's a slick trick. I like that new approach in v3! I knew all pieces that make it work but didn't glue them together! Well... maybe except I didn't know that properties Still, what do you think of those return values? Are they obsolete, are they supported? If they stay I still feel they should support Typescript (even though I care much less now). |
@jods4 @jacekkarczmarczyk yeah, I think it makes more sense to remove The feature actually did not exist in v2. I implemented it in v3 due to some existing requests, but now it seems indeed redundant. |
BREAKING CHANGE: this.$emit() and setupContext.emit() no longer return values. For logic that relies on return value of listeners, the listener should be declared as an `onXXX` prop and be called directly. This still allows the parent component to pass in a handler using `v-on`, since `v-on:foo` internally compiles to `onFoo`. ref: vuejs/rfcs#16
i think this is a documentation problem. when i started to use vue and read the official guide and a lot of blog posts about how to get stuff done in vue. on the topic of "function props" it was recommended to avoid them and use events/slots instead without going into detail about valid use-cases for props that receive functions.
how does this interact with listener merging/multiple listeners? (if it interacts)
you can do |
picking up on my last part of my last comment:
what is missing here, is my conclusion to also use either function values or inline functions in "but what about consistency with HTML inline listeners?", the main difference between HTML inline listeners and vue inline listeners is that in HTML you can ONLY pass statements acting as the listener code, while in vue you can also pass values (functions), but in JS a value or variable symbol is also a statement. |
Maybe I miss something but I wonder why we need both In vue 2 each component is an full functional event emitter, with the classic emit/on/off interface which can pass events not only from child to parent but also across the whole compoent tree. I think that is why in vue 2 events cannot turn into function props. But in vue 3 there is no more emit/on/off interface, and events are now limited from child to parent. So maybe it is better to unify the concepts of props and emits, making By doing this, typings of emits, including types of the parameters and the return value, can be easily defined just like normal props. |
@beeplin it is a viable style, but we also need to preserve the backwards compatibility for the event-based usage. Event system is also more fire-and-forget which is somewhat a different mental model. It would be too drastic a change to remove |
Rendered