Search code examples
shadow-domslot

How to pass option tags to a custom element as distributed nodes (aka <slot></slot>)


I have a web component custom element defined like so.

 <template id="dropdown-template">
        <select>
            <slot></slot>
        </select>
    </template>
    <script>
        class Dropdown extends HTMLElement {
            constructor() {
                super();
                const shadowRoot = this.attachShadow({mode: 'open'});
                let template = document.getElementById('dropdown-template');
                shadowRoot.appendChild(template.content.cloneNode(true));
            }
        }
        customElements.define("drop-down", Dropdown);
    </script>

When trying to use it, I try and pass option tags with values into the custom element.

<drop-down>
     <option>one</option>
     <option>two</option>
     <option>three</option>
</drop-down>

This doesn't work. The select element is shown to have a <slot> element as its direct child, and it doesn't render the options. Is this not possible to do with the <select> tag?


Solution

  • It not possible to do it like that because the parent element of an <option> tag must be a <select> tag. So you can use with Shadow DOM because the <slot> element will break the parent/child hierachy.

    Solution 1 : move elements

    A workaround is to move the content of the light DOM inside the <select> element in the template.

    class Dropdown extends HTMLElement {
        constructor() {
            super()
            const shadowRoot = this.attachShadow( {mode: 'open'} )
            let template = document.getElementById( 'dropdown-template' )
            shadowRoot.appendChild( template.content.cloneNode(true) )
    
            const select = shadowRoot.querySelector( 'select' )     
            shadowRoot.addEventListener( 'slotchange', ev => {      
                let node = this.querySelector( 'option' )
                node && select.append( node )
            } )
        }
    }
    customElements.define("drop-down", Dropdown);
    <template id="dropdown-template">
        <select></select>
        <slot></slot>
    </template>
    
    <drop-down>
        <option>one</option>
        <option>two</option>
        <option>three</option>
    </drop-down>

    Solution 2: customized <select>

    Another possibility is to avoid Shadow DOM and define your drop-down list as a customized built-in <select> element. Maybe this won't fit your needs if you want to customize the layout.

    <select is="drop-down">
        <option>one</option>
        <option>two</option>
        <option>tree</option>
    </select>