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: declarative binding for non-standard event names #88

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all 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
125 changes: 125 additions & 0 deletions text/0000-declarative-nonstandard-event-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Declarative Binding for Non-Standard Event Names
status: DRAFTED
created_at: 2024-02-25
updated_at: 2024-02-25
pr: (leave this empty until the PR is created)
---

# Declarative Binding for Non-Standard Event Names

## Summary

LWC support for declarative event binding has always been limited to event names using lower-case
alphabetic characters, underscores, and numeric characters. Event names containing upper-case
characters (e.g., camelCase, CAPScase, PascalCase, etc) have never been supported in LWC due to
the case-insensitive nature of HTML. In addition, event names containing hyphens were never
supported due to the reluctance to avoid introducing ambiguity into the LWC convention of using
hyphens in attribute names to map to upper-case letters in property names.

This has worked well in practice with developers sticking to LWC conventions and using
`addEventListener()` when required as a workaround, but with the recent introduction of
`lwc:external` which provides a paved path for integrating third party web components, there is
greater value in providing a declarative way to listen for non-standard event names.

This RFC proposes the introduction of a new template directive `lwc:on`, which allows us to support
declarative event binding for third party web components, while sidestepping all the aforementioned
limitations and ambiguities.

## Basic example

```js
customElements.define('third-party', class extends HTMLElement{
connectedCallback() {
this.dispatchEvent(new CustomEvent('Foo-Bar-BAZ'));
}
});
```

These are examples of some of the options that are currently on the table:

```html
<!-- Event as part of directive -->
<third-party lwc:external lwc:on:Foo-Bar-BAZ={handleConnected}></third-party>

<!-- Event as part of attribute name (special treatment for lwc:external) -->
<third-party lwc:external onFoo-Bar-BAZ={handleConnected}></third-party>
Copy link
Contributor

Choose a reason for hiding this comment

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

To me, this option is the least disruptive. Yes, it is playing fast-and-loose with HTML semantics, but it is the most similar to what we have today, avoiding the need for a special new lwc:on directive or other mechanism.

Scoping it to lwc:external also brings that advantage that we can pass 100% on Custom Elements Everywhere without changing how LWC components have worked up until today. (We can re-evaluate that later if needed, but I really think it's not a big deal – the important thing is just to support the wide world of third-party custom elements and their wacky naming conventions for events.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree that it's the least disruptive, but I also feel like it might be a source of confusion/surprise. The nice thing about introducing a new directive is that it's easy to understand...if you want to do this thing that you were never able to before, you have to use this new directive.


<!-- Event and listener as part of attribute value -->
<third-party lwc:external lwc:on="Foo-Bar-BAZ:handleConnected"></third-party>
```

This is an example of a less declarative approach but should also be useful for things like creating
components from metadata:

```js
// foo.js
class Foo extends LightningElement {
handlers = {
'kebab-case': (event) => console.log("'kebab-case' handled"),
camelCase: (event) => console.log("'camelCase' handled"),
CAPScase: (event) => console.log("'CAPScase' handled"),
PascalCase: (event) => console.log("'PascalCase' handled"),
};
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

This to me is unnecessary since we already have lwc:spread, which works with onclick/onblur/etc. We may have to consider how lwc:spread would interact, though, with lwc:external if we decided to support the non-standard event names there.


```html
<!-- foo.html -->
<x-bar lwc:external lwc:on={handlers}></x-bar>
```

## Motivation

The primary motivation for this feature is to enable component authors to listen for non-standard
event names in a declarative manner without having to resort to calls to `addEventListener()`. This
should be especially useful when interfacing with non-LWC components that do not follow LWC
conventions, which should become more common with the recent introduction of native web component
support through the use of the `lwc:external` directive.

## Detailed design

The usage of this feature will be restricted to custom elements using `lwc:external`.

The design will be based on the agreed upon API shape, which is still under discussion.

## Drawbacks

A drawback of this feature is that component authors of LWC components may be encouraged to
implement non-standard event names that requires the use of `lwc:on` for declarative consumption.
This is not desirable because it makes static analysis more difficult. Conversely, consumers might
use this directive when they could just as easily have used a standard `on*` attribute in their
template. These scenarios could be avoided, however, by restricting the usage of `lwc:on` to custom
elements that use the `lwc:external` directive.

Choose a reason for hiding this comment

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

Lwc can be parent or owner of components using lwc:external. And these events could reach them if bubbles or composed is true. Do we not want to support those cases?


## Alternatives

### lwc:spread

An alternative design that was considered was to build this feature into the existing `lwc:spread`
directive which currently only supports the setting of property values. In fact, `lwc:spread` can be
used to listen for standard events like `click` or `focus` by assigning event listeners to the
`onclick` and `onfocus` properties. However, adding support for non-standard event names would add
complexity to `lwc:spread`. Introducing a new directive also makes it easier to restrict the usage
of `lwc:on` to `lwc:external` components.
Copy link
Contributor

Choose a reason for hiding this comment

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

It certainly adds complexity if we want to maintain the difference between lwc:external vs LWC components. We'd have to add validation to the client side (runtime) as well as at compile-time. Although we could just not support lwc:spread with non-standard event names entirely – we'd still score 100% on Custom Elements Everywhere, so the value is debatable.


## lwc:on=`${eventName}:${listenerName}`

Encoding both the event name and event handler name as a string using some delimiter would allow us
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a big fan of lwc:on, especially since Svelte v5 is actually moving away from their existing on:click syntax towards a syntax more like ours, since it plays nicer with their equivalent of lwc:spread.

to work around the case-insensitive nature of HTML attributes. This was simply unpopular during an
initial informal survey.

## Adoption strategy

This feature should only be used for interoperability with third party web components. The main
benefit would be an improvement in static analysis when used over listening to events using
`addEventListener()` and we could document it as such.

# How we teach this

We can teach this by adding to the documentation we already have for third party web components.

# Unresolved questions

Would this be sufficiently declarative if the event name does not appear in the template but the
user does not have to invoke `addEventListener()`?