-
Notifications
You must be signed in to change notification settings - Fork 407
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: event support to @lwc/engine-server #2920
Conversation
- **What version of Node is required?** Node >= 16 | ||
- **Why redefine types in `engine-server` that are implicit DOM types (see `lib.dom.d.ts`)?** Unfortunately, it is not permitted to import specific types explicity. Typescript assumes that if you want one DOM type, you are in a browser and will want _all_ DOM types to be implicityly available globally. That may lead to confusion in the future when browser-only types are available in Node-only code. The best alternative is to copy the types we need into `engine-server`. | ||
- **Which DOM event phases will be supported?** At present, there does not appear to be a use case for supporting the `capture` or `target` phases. As such, server-side DOM-like events will only support the `bubble` phase of event propagation. | ||
- **Is `Event#preventDefault` handled in any way?** No. Firstly, we're not providing an actual `Event` type - that's up to consumers. Secondly, there is no default behavior to prevent, so this isn't a consideration in the SSR context. |
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.
we're not providing an actual
Event
type
Should we? Consumers would not only have to provide Event
, but also CustomEvent
. Is the goal to rely on node global Event
constructor that has been introduced in Node v15?
On the same topic, Event.prototype.target
and Event.prototype.composedPath()
are 2 of the most used properties on dispatched events. Does it make sense to implement those properties on SSR? If yes, it means that we would have to expose the internal HostElement
to userland code. This would certainly require a better encapsulation of the HostElement
internal properties.
} | ||
} | ||
} | ||
currentNode = currentNode.parent; |
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.
The event should only invoke all the ancestor elements if bubbles
is set to true
.
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.
The behavior of bubbles: false, composed: true
is especially counter-intuitive here. I had to look at PM's post a few times before I got it. 😆
[W]hen the dispatched event is composed only, the event propagates from one host element to another [...] without propagating through the intermediary nodes.
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 unresolved this convo, because I don't think the logic is correct for the case @nolanlawson mentioned bubbles: false, composed: true
. If bubbles
is false
, stop
is true
which prevents the event to propagate to the closest shadow root ancestor.
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.
Thank you for this, btw. I'll make sure there's good test coverage here.
- **Why redefine types in `engine-server` that are implicit DOM types (see `lib.dom.d.ts`)?** Unfortunately, it is not permitted to import specific types explicity. Typescript assumes that if you want one DOM type, you are in a browser and will want _all_ DOM types to be implicityly available globally. That may lead to confusion in the future when browser-only types are available in Node-only code. The best alternative is to copy the types we need into `engine-server`. | ||
- **Which DOM event phases will be supported?** At present, there does not appear to be a use case for supporting the `capture` or `target` phases. As such, server-side DOM-like events will only support the `bubble` phase of event propagation. | ||
- **Is `Event#preventDefault` handled in any way?** No. Firstly, we're not providing an actual `Event` type - that's up to consumers. Secondly, there is no default behavior to prevent, so this isn't a consideration in the SSR context. | ||
|
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.
One question I have reading this is how is this related to prior art, e.g. Lit: lit/lit#2309
I.e. is our approach compatible with theirs? Could we extract it into a standalone package that would be sharable with other custom element frameworks?
let savedCallback = callback; | ||
|
||
if (useCaptureOrOptions) { | ||
if (useCaptureOrOptions === true || useCaptureOrOptions.capture) { |
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.
The spec is not clear on this, but browsers also allow truthy objects like 1
and "foo"
here as well. This gets weird because the empty object {}
, however, is treated as an actual property bag (i.e. capture: false
rather than capture: true
).
There is some background discussion on this in WICG/EventListenerOptions#21 (comment) and this Chromium issue.
I was a bit unsure what behavior they landed on, so I just wrote a small test. Luckily all 3 browser engines behave the same. Basically useCapture
is true in these cases:
true
1
"foo"
(non-empty string){ capture: true }
It's false in all the other cases I tested (including truthy values like {}
and []
).
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 is super helpful, thank you! I'll add these conditions w/ comments.
const removeEventListener = noop as ( | ||
target: HostNode, | ||
if (!(type in node.eventListeners)) { | ||
node.eventListeners[type] = new Set(); |
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 would suggest we use a Map
here for eventListeners
instead of an object. Otherwise weird things can happen if a developer does node.addEventListener('constructor', ...)
or node.addEventListener('__proto__', ...)
.
Probably it's not vulnerable to any kind of security issue (e.g. prototype pollution), but why take the risk? 🙂
if (node.type !== 'element') { | ||
return; | ||
} | ||
const eventListeners = node.eventListeners[type]; |
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.
Nitpick: I find it a bit confusing that eventListeners
can refer to both a Record<string, Set>
and the Set
. Maybe we can call this variable listenersSet
or something? Or rename node.eventListeners
?
I'm still very much confused about what this does, and why it does it? |
Closing this. After discussion on the team, we will be disallowing event support on the server. And we'll add a separate engine-server implementation for wire context. |
Details
This adds support for dispatching events during SSR. Everything is synchronous, and only the
bubble
phase of event propagation is supported.This implementation seems to be working as expected in the LWR PoC, so I'd like to start working on tests. However, before doing that, I'd like feedback on the implementation in case folks want to see changes.
Before this PR can be merged, the following tasks need to be completed:
canary-ssr
version bump commitsephemeral-notes.md
Does this pull request introduce a breaking change?
Does this pull request introduce an observable change?
GUS work item