Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Dynamic event names in emits option #204

Open
CyberAP opened this issue Aug 20, 2020 · 18 comments
Open

Dynamic event names in emits option #204

CyberAP opened this issue Aug 20, 2020 · 18 comments

Comments

@CyberAP
Copy link
Contributor

CyberAP commented Aug 20, 2020

What problem does this feature solve?

Components might emit events that can not be statically analyzed. For example v-model already does this with the update:[name || 'modelValue']. Where name is a v-model argument. A custom event could look like this for example: change:[name] where name is derived from a component's prop.

What does the proposed API look like?

I think emits should support these dynamic event names via guard callbacks:

<script>
export default {
  emits: [(eventName) => /^change:/.test(eventName)] // will match any event listener that starts with `onChange:`
}
</script>

This could also work for event validations:

<script>
export default {
  emits: [
    {
      name: (eventName) => /^change:/.test(eventName),
      validate: (payload) => payload !== null,
    }
  ]
}
</script>
@yyx990803 yyx990803 transferred this issue from vuejs/core Aug 25, 2020
@yyx990803
Copy link
Member

yyx990803 commented Aug 25, 2020

Transferred to RFCs for discussion.

@jods4
Copy link

jods4 commented Sep 6, 2020

Can you motivate this RFC with more use cases?
It's about components but the only example given is v-model, which is a directive.

I never had a component with dynamic events in my projects, it would help me think about the patterns if a real use case was given.

A point to consider: isn't a potential benefit of emits to eventually be integrated with tooling? E.g. so that when editing a template, you could hopefully get completion for @ on a component. This proposal wouldn't mesh well with this, would it?

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 6, 2020

v-model was given just as an example, this change has actually nothing to do with v-model since it doesn't require any special treatment by default. It's also not about components, but more about props and attribute inheritance. As you know if we don't use inheritAttrs: false or emits then event listeners will be assigned to the root element of a single root component.
A common use-case would be a component that receives some config, reacts to events and emits new events that specifically target some part of the config.

<DynamicFilters :filters="{ foo: [], bar: [], baz: [] }" @update:foo="onUpdateFoo" />

This can not be solved with a generic update event because it will also fire on things we don't want to handle.

There's a workaround with an inheritAttrs: false which is sort of usable, but limits wrapping options.
Consider DynamicFilters was renderless and had an inheritAttrs: false. It won't have attribute inheritance anymore when used as a root component. (though I haven't thoroughly tested that it had proper attribute inheritance with inheritAttrs enabled)

<DynamicFilters :filters="filters" @update:foo="onUpdateFoo">
  <div>
    <!-- actual component... -->
    <!-- will not receive class or attrs -->
  </div>
</DynamicFilters>

I have a library that emits these kind of events if you're interested: https://github.com/CyberAP/vue-component-media-queries

@jods4
Copy link

jods4 commented Sep 6, 2020

Thanks I understand better now, it's an interesting usage.

I'm not totally convinced by those 2 examples that the use-cases justify the extra complexity added to Vue core.
I feel like you could design those components in a satisfactory manner today:

This can not be solved with a generic update event because it will also fire on things we don't want to handle.

I think you could totally fire an event @update, which either has 2 parameters, or has one parameter that is an object with a filter: 'foo' property.

Instead of filtering in the event name, which is a disguised parameter; you'll have to filter in the handler code, or in the template: @update="$event.filter == 'foo' && onUpdateFoo($event)"

One may even argue that for a generic component it might be better because if I'm interested in a change in any filter (e.g. to refresh a list), I can do that by listening to just one event.

Another design could be to not provide this event at all and rely on the consumer passing a reactive object to :filters.
This might lead to more concise code, as listening to change events is not very Composition API-like.
You could run an effect that refreshes the data based on the filters objects (or does anything else) and it would be called any time filters change. You can use the 2 parameters watch if you absolutely need to react to a single property without running an eager effect.

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 6, 2020

I'm not totally convinced by those 2 examples that the use-cases justify the extra complexity added to Vue core.
I think you could totally fire an event @update, which either has 2 parameters, or has one parameter that is an object with a filter: 'foo' property.

With the increasing complexity on the developer's side, yes. But in this case we're solving the consequences of attribute inheritance, while we could completely avoid going into that in the first place.

Take for example an abstract StateWatcher component. It can watch some external state of any complexity, but the component will incorporate that state complexity inside an event:

<StateWatcher @update="$event.prop !== 'foo.bar.baz' && handleEvent($event)" />

This will have very poor performance overall.

Compare that to a dynamic listener example:

<StateWatcher @update:foo:bar:baz="handleEvent" />

Another design could be to not provide this event at all and rely on the consumer passing a reactive object to :filters.

This is unrelated since props and events are partially related. The actual data may be stored somewhere else.

@jods4
Copy link

jods4 commented Sep 7, 2020

Very slightly increased complexity on developer's side, but there's complexity on the Vue side as well that needs to be factored in.
Not saying it should not be done but I think it's good to have compelling use cases to motivate such additions, especially if it could work in user land.

Your solution to the StateWatcher perf issues is to tell it what you want to watch. You could pass that info in a more straightforward way without custom event names, in it the same way MutationObserver and co. are designed:

<StateWatcher watches="foo.bar.baz" @update="handleEvent" />

I'm gonna say it's even more flexible as you could easily add and remove watchers dynamically if that component supports an array:

<script> let fields = reactive(['foo', 'bar']); </script>
<StateWatcher :watches="fields" @update="handleEvent" />

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 7, 2020

I would argue that it is not more flexible, but quite the opposite.

If we want to watch multiple sources:

<StateWatcher :watches="fields" @update="handleEvent" />
const fields = reactive(['foo', 'bar'])
const handleEvent = (event) => {
  if (event.field === 'foo') { handleFooEvent(event) }
  else if (event.field === 'bar') { handleBarEvent(event) }
}

We have created a new reactive property and an event handler just to deal with two events. I think it's a cumbersome way to deal with dynamic events.

With dynamic event handlers we get this:

<StateWatcher
  @update:foo="handleFooEvent"
  @update:bar="handleBarEvent"
/>

No extra properties or event handlers required. It is a much more concise API no matter how you look at it.

The whole idea of this change is to give developers more ways to express their components API. And this actually works for Vue 2, so I don't see why it shouldn't work for Vue 3 since it is a perfectly valid case.

@jods4
Copy link

jods4 commented Sep 7, 2020

Doesn't have to be reactive, I think this is perfectly fine:

<StateWatcher :fields="['foo', 'bar']" @update="handleChange" />
<script>
function handleChange(e) {
  switch (e.field) {
    case 'foo':
       // do A
       break;
    case 'bar':
      // do B
      break;
  }
}
</script>

And of course you can go fancy if you have many handlers:

const handlers = {
  foo(e) { },
  bar(e) { },
};

function handleChange(e) { 
  handlers[e.field]?.(e)
}

No extra properties or event handlers required. It is a much more concise API no matter how you look at it.

You missed my point. Say I want to dynamically add or remove a listener for properties. How are you gonna do that? This is what I meant when I said the other solution is more flexible.

The whole idea of this change is to give developers more ways to express their components API.

Sure, I'm just playing the devil's advocate here.

Adding this requires more code in Vue, more documentation, creates more knowledge for users, doesn't mesh well with a potential autocomplete for templates in IDE and will need to remain supported for the foreseeable future...

I'm not opposed to the idea, just trying to see if the use cases are many or compelling enough to justify the addition.
Maybe they are 😄

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 7, 2020

Say I want to dynamically add or remove a listener for properties. How are you gonna do that? This is what I meant when I said the other solution is more flexible.

That would be easy to achieve with a v-on directive.

<StateWatcher v-on="{ 'update:foo': onFooUpdate, 'update:bar': onBarUpdate }" />

Adding this requires more code in Vue, more documentation, creates more knowledge for users, doesn't mesh well with a potential autocomplete for templates in IDE and will need to remain supported for the foreseeable future...

I wouldn't say this feature is required to have IDE support. It is required to filter out dynamic event listeners from attribute fallthrough and that's basically it. We don't have dynamic slot props IDE support and the feature itself is there nonetheless.

I would like to know how much maintenance burden it would actually introduce for Vue maintainers. From my perspective it doesn't really seem that hard to maintain if we ignore IDE support, but I might be wrong of course.

@jods4
Copy link

jods4 commented Sep 8, 2020

That would be easy to achieve with a v-on directive.

Right! So used to using the shortcut @ I even forgot the long form supports objects. 😁

I wouldn't say this feature is required to have IDE support.

Hopefully one day Vetur (or one of the new alternative plugins) will support event completion in IDE.
I would really like to get suggestions for a component events when I type <MyButton @| in a template.
This could be possible now that Vue 3 requires emits to declare component events.

If this happens, dynamic events as proposed here won't be supported (I don't see how). That's unfortunate and will feel like a limitation. 😞

Another issue today is Typescript support: when using TS emit("myevent", args) is strongly typed.
I think this is highly desirable and must not be broken.
Dynamic events as proposed here can't be understood by the type system.
So emit("update:field") will either be an error (not good), or any value will be accepted and typos are missed emit("nyewent") and that's not great either.

Now, all your examples are not completely random event names, but rather hiding a parameter inside the event name, such as update:field.
If we want to support that, maybe we can find an alternative design?
For example Vue could formally define support for using : in event names, as it does internally with v-model and update:XXX?

We can come up by many ways to do that, but one simple idea is to consider events in a special way if their name ends with :.
For proper validation and TS support they could define that their first parameter is a string and comes from the event name?

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 8, 2020

If we use an update: prefix we get the desired behaviour out of the box, but these events are reserved for v-model and I would like to avoid confusion here (my events can not be used on a v-model).

Having a separate rule for change: prefixed events would solve the problem.

@jods4
Copy link

jods4 commented Sep 8, 2020

I meant having some kind of API to define any event prefix, such as if you declare change: then Vue understands your component emits events like change:bar + others, but you could also declare delete: on the same component, etc.

@CyberAP
Copy link
Contributor Author

CyberAP commented Sep 8, 2020

I think adding a special case of a change: event prefix is a good enough trade-off.

If the goal is to have both versatility and at the same time leave emits API intact as much as possible the only viable solution I see is to have a prefix character for event names:

const emits = ['foo', '^bar:']
// matches `foo`, `bar:anything`

Vue 2 has a similar story with event modifiers for render functions (that didn't last in Vue 3 though).

@szulcus
Copy link

szulcus commented Oct 14, 2022

I think adding a special case of a change: event prefix is a good enough trade-off.

If the goal is to have both versatility and at the same time leave emits API intact as much as possible the only viable solution I see is to have a prefix character for event names:

const emits = ['foo', '^bar:']
// matches `foo`, `bar:anything`

Vue 2 has a similar story with event modifiers for render functions (that didn't last in Vue 3 though).

Any updates?

@laygir
Copy link

laygir commented Feb 20, 2023

I use the following approach quite often in my components such as navigation items, modal actions, data-table actions, even data-table row items to dynamically provide list of possible actions for that component..

Is this an anti-pattern? What would you suggest I do? (to prevent warnings and be able to declare emits that I do not know in advance)

// ActionItems.vue
<div>
  <my-button
    v-for="(action, i) in actions"
    :key="i"
    :variant="action.variant"
    :icon="action.icon"
    @click="$emit(action.event)"
  >
    {{ action.label }}
  </my-button>
</div>
actions() {
  return [
    {
      label: 'Create',
      event: 'createDocument',
      variant: 'button',
      icon: 'create',
    },
    {
      label: 'Send',
      event: 'sendDocument',
      variant: 'button-ghost',
      icon: 'send',
    },
  ];
}
<action-items 
    :actions="actions" 
    @create-document="createDoc" 
    @send-document="sendDoc"  
    />

@Mestpal
Copy link

Mestpal commented Sep 15, 2023

I use the following approach quite often in my components such as navigation items, modal actions, data-table actions, even data-table row items to dynamically provide list of possible actions for that component..

Is this an anti-pattern? What would you suggest I do? (to prevent warnings and be able to declare emits that I do not know in advance)

Maybe for this case you can use defineEmits()
https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits

And after import defineEmits() in your file, you can use something like this to define de emits from the events defined in your actions() :

   mounted () {
     const emits = this.actions.map(action => action.event);
     defineEmits(emits)
   }

@laygir
Copy link

laygir commented Sep 15, 2023

Maybe for this case you can use defineEmits() https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits

And after import defineEmits() in your file, you can use something like this to define de emits from the events defined in your actions() :

   mounted () {
     const emits = this.actions.map(action => action.event);
     defineEmits(emits)
   }

Awesome! I had no idea this was possible, this should do it. Thank you!

@emperorsegxy
Copy link

I use the following approach quite often in my components such as navigation items, modal actions, data-table actions, even data-table row items to dynamically provide list of possible actions for that component..
Is this an anti-pattern? What would you suggest I do? (to prevent warnings and be able to declare emits that I do not know in advance)

Maybe for this case you can use defineEmits() https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits

And after import defineEmits() in your file, you can use something like this to define de emits from the events defined in your actions() :

   mounted () {
     const emits = this.actions.map(action => action.event);
     defineEmits(emits)
   }

How is this used?

Usually when we define emits, we do this into a variable that can be called when component emits an event, I can't seem to find that here. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants