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

add: event support to @lwc/engine-server #2920

Closed
wants to merge 10 commits into from

Conversation

divmain
Copy link
Collaborator

@divmain divmain commented Jul 6, 2022

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:

  • add full test coverage
  • drop the canary-ssr version bump commits
  • delete ephemeral-notes.md

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

GUS work item

@divmain divmain marked this pull request as draft July 6, 2022 06:30
- **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.
Copy link
Member

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;
Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

@nolanlawson nolanlawson Jul 6, 2022

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) {
Copy link
Contributor

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 []).

Copy link
Collaborator Author

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();
Copy link
Contributor

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];
Copy link
Contributor

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?

@caridy
Copy link
Collaborator

caridy commented Aug 23, 2022

I'm still very much confused about what this does, and why it does it?

@divmain
Copy link
Collaborator Author

divmain commented Nov 9, 2022

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.

@divmain divmain closed this Nov 9, 2022
@ravijayaramappa ravijayaramappa deleted the dbustad/engine-server-events branch December 13, 2022 23:26
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

Successfully merging this pull request may close these issues.

4 participants