Search code examples
javascriptdomweb-componentcustom-eventsnative-web-component

How to catch custom events in Web Components?


To let Web Components communicate with each other, I'm using custom events. Let's imagine the following:

WebComponentA uses or contains WebComponentB which sends a CustomEvent (bubbles: true, composed: true) if a button is clicked. WebComponentA wants to do something if WebComponentB sends this event.

How should I dispatch the event within WebComponentB?

window.dispatchEvent(customEvent);

or

this.shadowRoot.dispatchEvent(customEvent);

How should I catch the event within WebComponentA?

window.addEventListener(custom-event, () => {
);

or

this.shadowRoot.addEventListener(custom-event, () => {
);

Are there any negative effetcts I should consider using one or another?

Thank you!


Solution

  • Dispatch the event on the component itself with bubble: true, composed: true so the event will bubble up to anything watching for it. When you want to very specifically have a lot of predictability and shared state, and I mean this is really a tight coupling desired, then just orchestrate events on global self (ie the window in the browser). Here are a few random examples I hope help, what they're doing is relatively useless just to show an example. The idea overall is to loosely couple things when that makes sense, events simply pass messages relating to state change. The component can always be fairly isolated and whatever context it's operating in can be concerned separately with what it does with that info (both preparing and receiving the model--this is where functional patterns are highly applicable). If more detail is needed or confusing feel free to spell it out and we can try to help.

    Note too that because I didn't setup any shadowRoot the composed flag isn't useful at all in this sample.

    broadly:

    • global: self (synonym for window or a worker in other contexts); coordinate events here that are intended to be tightly coupled across many things--it is by far the simplest organization scheme when very specific coordinated systems are needed; simply listen and dispatch events here for this scenario; add and remove the event listeners in the connectedCallback and disconnectedCallback. Then either dispatch anywhere with bubbling or directly self.dispatchEvent('type', {detail:...}) and bubbling, etc is no longer necessary.

    • nodes: at any level of the tree, when a component has any state or events of any kind, handle the scenario and create an appropriate message as the event.detail and a sensible name as the event.type, then dispatch this from the node that is handling that logic. Other nodes--on themselves for parent nodes, etc--can watch the event.bubble up, and when dispatching from shadow nodes, those events can use the composed:true flag to allow continuation of the event outside the shadowRoot. Or the element can handle internal events and dispatch a new type of event and payload that fits that type.

        <my-global>my global </my-global>
        <my-aye> aye (parent)
            <my-bee> bee (child) </my-bee>
        </my-aye>
        <my-bee> bee </my-bee>
      
      
        function eventdetails(event){
            const {type, detail} = event;
            console.log({type, detail, self:this, path: event.composedPath(), event});
        }
      
        customElements.define('my-global', class MyGlobalWatch extends HTMLElement{
            constructor(){
                super();
                this.global = this.global.bind(this);
            }
            global(event){
                eventdetails.call(this, event);
            }
            connectedCallback(){
                self.addEventListener('global', this.global);
            }
            disconnectedCallback(){
                self.removeEventListener('global', this.global);
            }
        });
        customElements.define('my-aye', class MyAye extends HTMLElement{
            constructor(){
                super();
                this.addEventListener('hi-aye', this.handle);
                this.addEventListener('hi-bee', this.handle);
            }
            handle(event){
                eventdetails.call(this, event);
                if(event.type === 'hi-bee'){
                    self.dispatchEvent(new CustomEvent('global', {detail: event.detail, cancelable: true, composed: true, bubbles: false}));
                }
            }
        });
        customElements.define('my-bee', class MyBee extends HTMLElement{
            constructor(){
                super();
                this.addEventListener('hi-aye', this.handle);
                this.addEventListener('hi-bee', this.handle);
                this.ticker = this.ticker.bind(this);
            }
            handle(event){
                eventdetails.call(this, event);
            }
            ticker(){
                // 3 events of the same type, different configuration
                this.dispatchEvent(new CustomEvent('hi-aye', {detail: {payload:'> -composed +bubbles'}, cancelable: true, composed: false, bubbles: true}));
                this.dispatchEvent(new CustomEvent('hi-aye', {detail: {payload:'> +composed +bubbles'}, cancelable: true, composed: true, bubbles: true}));
                this.dispatchEvent(new CustomEvent('hi-aye', {detail: {payload:'> -composed -bubbles'}, cancelable: true, composed: false, bubbles: false}));
      
                this.dispatchEvent(new CustomEvent('hi-bee', {detail: {stuff:'things'}, cancelable: true, composed: true, bubbles: true}));
      
                this._timer = setTimeout(this.ticker, 1234);
            }
            connectedCallback(){
                this.ticker();
            }
            disconnectedCallback(){
                clearTimeout(this._timer);
            }
        });