-
Notifications
You must be signed in to change notification settings - Fork 25
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
base: master
Are you sure you want to change the base?
Conversation
Changes to be committed: new file: text/0000-dynamic-events.md
Thanks for the contribution! Unfortunately we can't verify the commit author(s): Gaurav Kochar <g***@s***.com>. One possible solution is to add that email to your GitHub account. Alternatively you can change your commits to another email and force push the change. After getting your commits associated with your GitHub account, refresh the status of this Pull Request. |
Changes to be committed: modified: text/0000-dynamic-events.md
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, except when lwc:component
is used for dynamic component creation since the events are bound after the connectedCallback
.
text/0000-dynamic-events.md
Outdated
There are tradeoffs to choosing any path. Attempt to identify them here. | ||
|
||
|
||
To add : Static Analyzability. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This proposal actually looks great to me. I think using lwc:on
is much cleaner than the previous lwc:spread
-based proposal.
One edge case not covered: are we planning to just use a simple Object.keys()
on the supplied object? Or Object.getOwnPropertyNames
? This matters if someone passes in e.g. a subclass that inherits properties from a superclass.
Another thing missing from this document is a review of what other frameworks are doing – how is Svelte, Vue, Lit, Solid, React, etc. handling this case?
</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 . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense to me. If something is dynamically applied, it "feels" like it should take precedence. So onfoo
is essentially the "default" listener.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
||
childEventHandlers = { | ||
foo: function (event) {console.log('foo');} , | ||
bar: function (event) {console.log('bar');} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if one of these examples used this
, to demonstrate that the listeners are bound to the component instance.
text/0000-dynamic-events.md
Outdated
|
||
### 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently , when consumers use addEventListener
, (https://github.com/salesforce/lwc/blob/8d3bc0848e77fb1a2f74019d570dbfb684010c0c/packages/%40lwc/engine-core/src/framework/restrictions.ts#L182, https://github.com/salesforce/lwc/blob/8d3bc0848e77fb1a2f74019d570dbfb684010c0c/packages/%40lwc/engine-core/src/framework/base-lightning-element.ts#L336) , we don't do any checks on the type
. we just let the browser's addEventListener
throw.
we should do the same here right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
text/0000-dynamic-events.md
Outdated
|
||
Currently `lwc:spread` can be used to listen for standard events like `click` or `focus` by assigning event handlers to the `onclick` and `onfocus` properties. This works because the element on which these properties are applied inherits from HTMLElement. This implies that the event handlers would be bound to these elements itself and not the owner component as `onevent` directly on template does. Without `lwc:spread`, there is not a completely equivalent way of doing this. So modifying this behaviour would break components with no straightforward workaround. However it must also be noted that this difference is also a frequent source of confusion. | ||
|
||
Implementing of this design would cause significant degradation of performance, since we would need to parse the object passed to `lwc:spread` in each render. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt it would be significant, FWIW.
In my mind, the best argument against lwc:spread
is 1) it's a potentially breaking change, and 2) its a potential point of confusion. lwc:on
is much cleaner in my opinion because the intention is clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nolanlawson I'm fine with either approach, but was under the impression lwc:spread
would complicate the existing compiled code. Ex: lwc:spread={simpleProps}
,
props: {
"name": "template",
...$cmp.simpleProps // <- This would need a runtime filter out .startsWith('on')
},
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, it would complicate our code, but that's not the main argument against it IMO. Yes you're right that we would need a filter there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated this section. I am not sure what qualifies as significant, I have removed it for now.
PR link and grammer Co-authored-by: Nolan Lawson <[email protected]>
Thanks for the contribution! Before we can merge this, we need @gaurav-rk9 to sign the Salesforce Inc. Contributor License Agreement. |
Changes to be committed: modified: text/0000-dynamic-events.md
|
||
At present, `lwc:spread` can be used to listen for standard events like `click` or `focus` by assigning event handlers to the `onclick` or `onfocus` properties. This works because these properties are applied to the rendered element, meaning the event handlers are bound to the elements themselves, not to the owner component as `onevent` on the template does. This behavior might be unexpected for consumers. The distinction between the rendered element and the `LightningElement` component is an implementation detail. For consumers, it is simply an Lwc, and this behavior cannot be explained as the application of properties to Lwc. The proposed design would be a breaking change. However, any component that relies on the current behavior can be rewritten to accommodate the new design. | ||
|
||
Although properties and event listeners use the same HTML attribute syntax, consumers understand their differences. They recognize whether a variable in an HTML template is a property, attribute, or event listener and reason about them separately. Combining them into a single directive does not enhance the developer experience. In fact, a combined directive might lead consumers to treat them as a unified concept, causing confusion. Additionally, the usage patterns of properties and event listeners are different, and a combined directive would complicate consumer code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consumers understand their differences
a combined directive might lead consumers to treat them as a unified concept, causing confusion
counterpoint: consumers are already confused 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, we wouldn't want to confuse them more. 😃
</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 . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
### 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 the owner component's intial render, We can cache them to improve performance. Note that this is same as how `onevent` in template HTML 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is essentially just sugar for doing addEventListener
once and moving on? We wouldn't care/explode if a user did something weird like
{
foo: function handleFoo() {
this.removeEventListener('foo', handleFoo)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this would do addEventListener
once immediately after construction and move on. We don't care what the function does, we take the function input, wrap it into a new function that binds it to the owner component and and then add it as event handler to the element.
{
foo: function handleFoo() {
this.removeEventListener('foo', handleFoo)
}
}
Here if the expectation is to remove the event listener added for event foo
, this won't work as expected, because the event handler added won't be handleFoo
, it will be a different function, whose reference the consumer doesn't have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the event handler dynamically placed on the host doesn't seem like it is a required feature for these once attached.
This proposal adds a declarative mechanism to add a collection of event listeners to elements in an LWC template using a new directive
lwc:on
.