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

RFC: lwc:on Directive #92

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions text/0000-dynamic-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: `lwc:on` directive
status: DRAFTED
created_at: 2025-01-13
updated_at: 2025-01-13
pr: (leave this empty until the PR is created)
gaurav-rk9 marked this conversation as resolved.
Show resolved Hide resolved
---

# `lwc:on` Directive

## Summary

This proposal adds a mechanism to add a collection of event listeners to elements in an LWC template using a new directive `lwc:on`.

## Basic example

```js
// x/myComponent.js
export default class MyComponent extends LightningElement {

childEventHandlers = {
foo: function (event) {console.log('foo');} ,
bar: function (event) {console.log('bar');}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if one of these examples used this, to demonstrate that the listeners are bound to the component instance.

};

}
```

```html
<!-- x/myComponent.html -->
<template>
<x-child lwc:on={childEventHandlers}></x-child>
</template>
```

For each property in `childEventHandlers`, the new `lwc:on` directive adds an event listener to `x-child`. The event listener listens for the event specified by the property's key and uses the property's value, bound to the instance of `MyComponent`, as the event handler.

## Motivation

LWC's support for declarative event listeners is limited to scenarios where event name are known while authoring the owner component template. This is sufficient for most of the cases, however when using the `lwc:component` directive, it is common to require event listeners based on the current value of the constructor passed to `lwc:is`. In such cases, it is not possible to know the event name while authoring the owner component template.

As an alternative, it is possible to add event listeners imperatively. However it requires the owner component to have a reference of the element on which it needs to add event listener. The owner component can have a reference of a element only after the element has been connected and hence typically only after the connectedCallback of corresponding component has terminated , making it impossible for the owner component to handle events dispatched by the connectedCallback of the corresponding component.


## Detailed design

A new directive `lwc:on` will be introduced that can be used to add a collection of event listeners whose name may not be known while authoring the component.

### Structure

The `lwc:on` directive would accept an object. For each property of the object, it would add a event listener which would listen for the event specified by the property's key and handle it using the property's value bound to the owner component.

### Caching

#### For static components, i.e. components declared in template directly using its selector
Since it is uncommon for event listeners to change after the start of Owner component's intial render, We can cache them to improve performance. Note that this is same as how `onevent` on template works currently. For consumers, the implication of this would be that any changes made to the object passed to `lwc:on` after the first `connectedCallback` of owner component would cause no effect.
gaurav-rk9 marked this conversation as resolved.
Show resolved Hide resolved

#### For dynamic components, i.e. components created using `lwc:component` directive
Since it is uncommon for event listeners to change if the constructor passed to `lwc:is` doesn't change, We can skip patching of event listeners if the element doesn't change. For consumers, the implication of this would be that after the first `connectedCallback` of owner component, any changes made to the object passed to `lwc:on` would cause no effect until the constructor passed to `lwc:is` itself is changed.


### Overriding

```js
// x/myComponent.js
export default class MyComponent extends LightningElement {

childEventHandlers = {
foo: function (event) {console.log('lwc:on');} ,
};

fooHandler(){
console.log('onfoo');
}

}
```

```html
<!-- x/myComponent.html -->
<template>
<x-child onfoo={fooHandler} lwc:on={childEventHandlers}></x-child>
</template>
```

`lwc:on` will always be applied last. That means it will take precedence over whatever event listeners are declared in the template directly. In the above example, `x-child`, only one listener for event `foo` would be added and `childEventHandlers.foo` would be used for event handler .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me. If something is dynamically applied, it "feels" like it should take precedence. So onfoo is essentially the "default" listener.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense that only one or the other would be bound to the element, when you're generally able to bind the same event more than once?

<template>
    <x-counter lwc:ref="counter" onfoo={handleFoo}></x-counter>
</template>
import { LightningElement } from 'lwc';
export default class extends LightningElement {
  handleFoo() {
    console.log('foo static')
  }
  renderedCallback() {
    this.refs.counter.addEventListener('foo', () => {
      console.log('foo dynamic');
    });
    this.refs.counter.dispatchEvent(new CustomEvent('foo'));
  }
}
> foo static
> foo dynamic

Copy link
Author

@gaurav-rk9 gaurav-rk9 Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts on this were

  • Since we don't provide consumers with a way to explicitly remove these listeners, To execute multiple functions when an event is recieved, One can create a wrapper handler over these functions with added guarantee of order.
  • Fallback event listener seems to be of more value than an additional unconditional event listener
  • This would be consistent with how lwc:spread works.
  • This directive should not be a way for having multiple event listeners. If at some point we want to support multiple declarative event listeners, we should have a clean way to do that and it should not be a side effect of something else.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be consistent with how lwc:spread works.

This makes sense to me. Under the hood, it will be implemented similar to how lwc:spread is implemented, i.e. lwc:spread is:

props: {
  foo: $cmp.foo,
  ...$cmp.someObject
}

and lwc:on would be:

on: {
  foo: $cmp.foo,
  ...$cmp.someObject
}

(I'm skipping the binding and caching, but you get the idea.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thought was that onfoo={handleFoo} and lwc:on={ /* foo: handleFoo */ } should allow both, because having multiple event handlers is conceptually fine. But it's also reasonable to say "there's just one way of doing things, don't mix and match", to avoid user confusion and combinatorial complexity.

Additionally, there can be only one `lwc:on` directive on a element
gaurav-rk9 marked this conversation as resolved.
Show resolved Hide resolved

### Event names

The keys of object passed to `lwc:on` should conform to the requirement set by DOM Event Specification. There would be no other constraint on the object's keys.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently LWC throws a compile-time error if you try to use onfooBar or onfoo-bar or other disallowed naming patterns. Are you proposing that we throw an error at runtime? Or warn? Or something else?

FWIW this was actually a point of controversy in the Custom Elements Everywhere benchmark, which LWC does not fully pass because we don't support these event names.

I think there's a case to be made that we should not be so strict here, and should just accept whatever event string name the user passes in. (Except maybe nonsensical values like the empty string '', non-strings, etc.) There are cases of web components "in the wild" that use odd naming conventions for their event names, and it would be nice to support that in lwc:on.

Copy link
Author

@gaurav-rk9 gaurav-rk9 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of why this is disallowed is based on Summary of rfc: declarative binding for non-standard event names.
we don't have any of those constraints here , so in long term I don't think we must have these restrictions.
However, I think when event names are known while authoring the component, the approach finalized in rfc: declarative binding for non-standard event names should be the recommended approach due to better static analyzibility.
So maybe we should have these restrictions until rfc: declarative binding for non-standard event names is implemented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should just support whatever the spec supports (i.e., DOMStrings), which is any valid string. I think the only thing we'd need to look out for are Symbols, as the spec allows either a string or a symbol as the key in an object.

Could you mention that we'll filter out Symbols?

Copy link
Author

@gaurav-rk9 gaurav-rk9 Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the object iterators (Object.keys, for...in, etc.) ignore symbols, so any naive implementation of lwc:on will likely implicitly ignore symbols. If we want to check for symbols, we'd have to do so explicitly. That seems unnecessary, though, because they're not valid event names and probably nobody will ever try to use them.

Copy link
Author

@gaurav-rk9 gaurav-rk9 Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the implementation would be simpler, my thought was what would consumer expectation be, but now that I think about it , it does seem reasonable to say only own enumerable string-keyed properties will be considered.


## Drawbacks

Why should we *not* do this? Please consider:

- implementation cost, both in term of code size and complexity
- whether the proposed feature can be implemented in user space
Copy link

@Templarian Templarian Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, except when lwc:component is used for dynamic component creation since the events are bound after the connectedCallback.

- the impact on teaching people Lightning Web Components
- integration of this feature with other existing and planned features
- cost of migrating existing Lightning Web Components applications (is it a breaking change?)

There are tradeoffs to choosing any path. Attempt to identify them here.


To add : Static Analyzability.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Static Analysis since it's a dynamic component we would run into similar issues as we do withlwc:component in that we cannot easily search for what components these create in the client code.


## Alternatives

What other designs have been considered? What is the impact of not doing this?

To add : lwc:spread and reflecting on* properties as event listeners

## Adoption strategy

If we implement this proposal, how will existing Lightning Web Components developers adopt it? Is
this a breaking change? Can we write a codemod? Should we coordinate with
other projects or libraries?

# How we teach this

What names and terminology work best for these concepts and why? How is this
idea best presented? As a continuation of existing Lightning Web Components patterns?

Would the acceptance of this proposal mean the Lightning Web Components documentation must be
re-organized or altered? Does it change how Lightning Web Components is taught to new developers
at any level?

How should this feature be taught to existing Lightning Web Components developers?

# Unresolved questions

Optional, but suggested for first drafts. What parts of the design are still
TBD?

Need Input on all parts of design.