Search code examples
javascriptweb-componentshadow-domevent-delegation

Vanilla JavaScript event delegation when dealing with Web Components


My current project uses web components (Custom Elements and Shadow DOM) which allow me to encapsulate complex logic and styles away from the Light DOM.

Unfortunately, I've now hit a snag where I need to be able to switch out elements at will without the hassle of unbinding and rebinding event handlers.

This sounds like a job for event delegation to me so I tried adding an event listener to a parent node in the Light DOM hoping that the event would bubble up from the Shadow DOM.

This seems to go against the encapsulation of the Shadow DOM and the event.target is always the ShadowRoot and never a child.

Is there something I can do to allow classic event delegation in this situation? The code snippet below shows the problem. I want to be able to click the inner DIV and handle the click in the click event handler but the event.target is always the custom-el

/* jshint esversion: 6 */

customElements.define('custom-el', class extends HTMLElement {

	constructor() {
		super();

		this._shadowRoot = this.attachShadow({
			mode: 'open'
		});

		const oInnerDiv = document.createElement('div');
		oInnerDiv.classList.add('inner');
    oInnerDiv.style.border = '2px solid blue';
    oInnerDiv.style.padding = '3rem';
		this._shadowRoot.appendChild(oInnerDiv);
	}

});

document.addEventListener('click', oEvent => {
	document.getElementById('result').innerText = oEvent.target.tagName;
}, true);
html {
	box-sizing: border-box;
}

*,
*::before,
*::after {
	box-sizing: inherit;
}

body {
	margin: 0;
	padding: 0;
}

main,
div,
custom-el {
	display: inline-block;
	border: 2px solid black;
	padding: 3rem;
}
<main>
    <custom-el>
</main>
  
<p id="result"></p>


Solution

  • If the shadow DOM mode is open, it is possible to get the inner element clicked with the help of the Event.composedPath() method, which will return the array of the nodes crossed (innest node first).

    document.addEventListener('click', oEvent => {
        result.innerText = oEvent.composedPath()[0].tagName;
    }, true);
    

    This method replaces the old Event.path property.

    customElements.define('custom-el', class extends HTMLElement {
      constructor() {
        super();
        this._shadowRoot = this.attachShadow({ mode: 'open' });
        const oInnerDiv = document.createElement('div');
        oInnerDiv.classList.add('inner');
        oInnerDiv.style.border = '2px solid blue';
        oInnerDiv.style.padding = '1rem';
    		this._shadowRoot.appendChild(oInnerDiv);      
      }
    });
    
    document.addEventListener('click', oEvent => {
      result.innerText = oEvent.composedPath()[0].tagName;
    });
    html {
    	box-sizing: border-box;
    }
    
    *,
    *::before,
    *::after {
    	box-sizing: inherit;
    }
    
    body {
    	margin: 0;
    	padding: 0;
    }
    
    main,
    div,
    custom-el {
    	display: inline-block;
    	border: 2px solid black;
    	padding: 1rem;
    }
    <main>
        <custom-el></custom-el>
    </main>
      
    <p id="result"></p>