Search code examples
javascriptweb-componentlit

Set Lit Child Components to implicitly target lit parent component slots


I'd like to make a series of Lit web components that target specific slots in a parent component by default, for ease of use so I don't have to write out the slot attribute every time I want to use these child components.

In the example below, I have a popover component that contains a trigger and a content component. The trigger component should target the "trigger" slot and the content component should target the "content" slot.

I found ways to do this with vanilla JS but not with Lit components yet, which is the framework my company is using.

class Popover extends LitElement {
    render () {
        return html`
            <div class="container">
                <div class="trigger">
                    <slot name="trigger"></slot>
                </div>
                <div class="content">
                    <slot name="content"></slot>
                </div>
            </div>
    }
}

class Trigger extends LitElement {
    render () {
        <button>
            <slot></slot>
        </button>
    }
}

class Content extends LitElement {
    render () {
        <p>
            <slot></slot>
        </p>
    }
}

customElements.define('popover', Popover);
customElements.define('trigger', Trigger);
customElements.define('content', content);

/**
 * Sample Usage:
 * 
 * <popover>
 *   <trigger>My Trigger Button</trigger>
 *   <content>Content that popups goes here</content>
 * </popover>
 */

Solution

  • Lit does not do any special handling for the slot elements and leverages the native browser behavior. LitElement also extends HTMLElement which means the vanilla JS technique you linked will work exactly the same in Lit.

    I made the following changes to your sample code:

    1. Custom element names must contain a hyphen. Currently customElements.define('popover', Popover); throws a SyntaxError because the name isn't a valid custom element name. Added -el suffix to the custom element names.

    2. Added the named slot constructor code (from the linked article): this.slot = this.hasAttribute('slot') ? this.slot : "header";.

    See runnable example:

    <!-- Run this sample to see implicit named slots in action -->
    
    <popover-el>
      <!-- Swapped them to make it clear they were projected in the right slot -->
      <content-el>Content that popups goes here</content-el>
      <trigger-el>My Trigger Button</trigger-el>
    </popover-el>
    
    <script type="module">
    import {
      LitElement,
      html,
    } from 'https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js';
    
    class Popover extends LitElement {
      render() {
        return html` <div class="container">
          <div class="trigger">
            <slot name="trigger"></slot>
          </div>
          <div class="content">
            <slot name="content"></slot>
          </div>
        </div>`;
      }
    }
    
    class Trigger extends LitElement {
      constructor() {
        super();
        // Reference: https://www.aha.io/engineering/articles/web-components-and-implicit-slot-names
        this.slot = this.hasAttribute('slot') ? this.slot : 'trigger';
      }
      render() {
        return html`<button>
          <slot></slot>
        </button>`;
      }
    }
    
    class Content extends LitElement {
      constructor() {
        super();
        // Reference: https://www.aha.io/engineering/articles/web-components-and-implicit-slot-names
        this.slot = this.hasAttribute('slot') ? this.slot : 'content';
      }
      render() {
        return html`<p>
          <slot></slot>
        </p>`;
      }
    }
    
    customElements.define('popover-el', Popover);
    customElements.define('trigger-el', Trigger);
    customElements.define('content-el', Content);
    </script>