ollitietavainen
ollitietavainen

Reputation: 4275

Passing standard JavaScript event with composed=false through Shadow DOM

I have a custom lit web component that contains an <input> element inside its Shadow DOM. I want to react to the change event fired by the input outside the custom element, but the change event has by default composed: false, so the event doesn't pass through the Shadow DOM boundary. I could catch the event inside my component implementation, but the composed property is read-only, so I can't update it and dispatch the same event object. I could create a new object with new Event('change', {'composed': true}), but then it doesn't have the properties like target of the original event. What's a good approach? Should I manually copy the original event's properties to the new event object?

Upvotes: 4

Views: 1419

Answers (1)

Haprog
Haprog

Reputation: 853

It's not possible to dispatch a single Event instance more than once, so even if you could modify the composed property of the original event you couldn't redispatch it anyway.

So you need to create a new event to dispatch from your custom element, but the details of how exactly you want to create the event and what it should contain will probably depend on your use case. I'd suggest to try to keep it as simple as possible and make an event that includes the information you need and then dispatch it.

There's probably a reason why the native change event is not composed, but you can simulate propagating it out of your custom element by creating an event with the name change and dispatching it from your custom element. You probably don't even need to use composed in most cases as just dispatching it from your custom element (this) makes it available in the parent scope (one level up from your shadow root) which is where the event probably should be handled in most cases.

The fact that you have an <input> in your shadow root should probably be treated as an implementation detail (at least in some cases) and not exposed outside unnecessarily, but when you do need to expose it directly, you can make it available e.g. as a property on your custom element (which could then be accessed from your custom event) or you can include a reference to it in the event object (e.g. in detail property of CustomEvent or a property of a custom event class).

For example here's how Vaadin components like <vaadint-text-field> propagate the change event:

const changeEvent = new CustomEvent('change', {
  detail: {
    sourceEvent: e
  },
  bubbles: e.bubbles,
  cancelable: e.cancelable
});
this.dispatchEvent(changeEvent);

Here the original event is explicitly exposed as event.detail.sourceEvent so you could for example get the input value from your custom change event like event.detail.sourceEvent.target.value.

If you expose the input element via a property (e.g. myInput) you would not need to use CustomEvent and detail as then you could do something like event.target.myInput.value, or if the original change event actually causes a value property on your custom element to change, you could just read that instead.

// Dispatch event (in your custom element)
const changeEvent = new Event('change', {
  bubbles: e.bubbles,
  cancelable: e.cancelable
});
this.dispatchEvent(changeEvent);

// Read the input value in event handler (assuming your custom element
// has declared `myInput` as a reference to the `<input>`)
event.target.myInput.value
// Alternatively access via shadowRoot (not very nice)
event.target.shadowRoot.querySelector('input').value

You could also include custom properties or methods in your event if you create a custom event class like this:

// Declare event class once somewhere
class MyChangeEvent extends Event {
  constructor(sourceEvent) {
    super('change', {
      bubbles: sourceEvent.bubbles,
      cancelable: sourceEvent.cancelable,
    });
    this.sourceEvent = sourceEvent;
  }
}

// Trigger a custom event
this.dispatchEvent(new MyChangeEvent(e));

// Access custom event property in an event handler
event.sourceEvent

Instead of using the event name change you could of course use your own custom event name like my-change-event but it should be ok to use change when you want your custom element to behave like a native <input> which might allow your component to be used as a drop in replacement for a native <input> in some situations (with limitations probably) or if you just want to mimic the native change event which is already familiar to developers. In most cases it probably doesn't matter if the change event you dispatch is created as an Event, CustomEvent or other instance extending from Event.

Upvotes: 4

Related Questions