Search code examples
javascriptweb-componentshadow-domstenciljs

Bubbled event in StencilJs does not have correct event.target


I started with Stencil some time ago and today I did an experiment with event bubbling.

Basically, I clicked on a child element and listened on its parent for mousedown. What I expected was that event.target to be my child component. This is super simple, for example if I run this test with regular DOM elements (not Stencil) it all works as expected

enter image description here

DEMO

But in Stencil event.target is the parent element. Here is what I did. My parent component looks like this:

@Component({
    tag: 'app-root',
    styleUrl: 'app-root.scss',
    shadow: true 
})
export class MyApp {
    @Listen('mousedown')
    onMousedown(event) {
         console.log(event.target, event.currentTarget);
    }

    render() {
        return (
            <div>
                <my-element></my-element>
           </div> );
   }
}

And the child

@Component({
    tag: 'my-element',
    styleUrl: './my-element.scss',
    shadow: true
})
export class MyElement {
    @Listen('mousedown')
    onMouseDonw(e) {
        console.log(e.target);
    }

    render() {
       return ( <div>YO</div> );
    }
}

Now, as I said, when I click my-element, I first see the console.log from my-element, which confirms that event.target is my-element. But then the event arrives at its parent and event.target (but also event.currentTarget) contains app-root. I noticed that when I set shadow to false of my app-root it all seems to work, but I don't understand why, and is this the only solution to my problem?


Solution

  • It's considered a feature of the encapsulation of the Shadow DOM. The original target is hidden as "internal implementation", and instead the event is re-targeted to the host element.

    When you attach the listener with the Listen decorator within MyApp, it gets attached to the component's host element, which is app-root in this case.

    You can use event.composedPath() to get the real target though (not supported in IE and old Edge).

    Or you can catch the event from within the component and re-emit it as a custom event and forward the real event or target element:

    export class MyApp {
        @Event() myMousedown: EventEmitter<MouseEvent>;
    
        handleElementMousedown = (event: MouseEvent) => {
            this.myMousedown.emit(event);
        }
    
        render() {
            return (
                <div>
                    <my-element onMousedown={handleElementMousedown} />
               </div> );
       }
    }
    
    document
        .querySelector('app-root')
        .attachEventListener('my-mousedown', console.log);
    

    (disclaimer: this code was not tested)