lit-element: litElement doesn't support nested components

It doesn’t seem litElement allows you to nest elements in the HTML page itself.

For example If I wanted to do this:

<parent-element>
    <nested-element></nested-element>
    <nested-element></nested-element>
</parent-element

And I declared these elements like so:

class ParentElement extends LitElement {

    constructor() {
        super();
    }

    render() {
        return html`<div class="parent"><slot></slot></div>`;
    }
}

customElements.define('parent-element', ParentElement);

class NestedElement extends LitElement {

    constructor() {
        super();
    }

    render() {
        return html`<div class="nested"></div>`;
    }
}

customElements.define('nested-element', NestedElement);

It ignores the nesting and prints them like:

<parent-element>
    <!---->
    <div class="parent"><slot></slot></div>
    <!---->
    <nested-element>
        <!---->
        <div class="nested"></div>
        <!---->
    </nested-element>
    <nested-element>
        <!---->
        <div class="nested"></div>
        <!---->
    </nested-element>
</parent-element>

So whilst it keeps the nested components it’s not honouring that the HTML itself should be nested inside each other.

In fact, if I remove the <slot></slot> it still prints it exactly the same… so it seems litElement doesn’t care about the nesting of the components…

Does litElement support nested components? Or is this an error in the code?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 16 (8 by maintainers)

Most upvoted comments

@justinfagnani How can I nest Web Components but allow them to inherit the styling from the page that they are on? Without the createRenderRoot it blocks the CSS from the host page from affecting the components which isn’t what I want. We really need the page CSS to be able to bleed into the components and we also need to be able to nest components.

This has nothing to do with web components or lit-element, it’s simply how HTML and the DOM work.

In most js frameworks, the framework has control over the entire tree so it can render things in different places as it sees fit. Web components are actual dom nodes, so anything they render goes into that dom node. Rendering web components is async, so it cannot assume anything about it’s parent or child nodes.

In the browser the slotting issue is solved with shadow dom.

You can nest web components just fine:

<element-a>
  <element-b></element-b>
</element-a>

This will render element-a, with element-b inside. However, if element-a says:

this.innerHTML = ''

it will remove element-b.

The default behavior of lit-element is to render into a shadow root, in which it case it doesn’t overwrite it’s child nodes. In your case you changed createRenderRoot to return this, so you’re always overwriting your child nodes.

You could do something like this:

class ElementA extends LitElement {
  createRenderRoot() {
    const contentWrapper = document.createElement('div');
    this.appendChild(contentWrapper);
    return contentWrapper;
  }
}

this way element-a will render into a wrapper div, and won’t overwrite the other child nodes.

If you want to wrap the content of element-a around the “slotted” content given by the parent, it gets pretty complex. Web components render async, so the child nodes could come in at any later time, and the parent could make changes which need to be tracked. Moving around live nodes like that isn’t a good idea.

However, if both elements use lit-html, you could set the content to render as a property:

class ElementA extends LitElement {
  static get properties() {
    return {
      slotTemplate: { type: Object }
    };
  }

  createRenderRoot() {
    return this;
  }

  render() {
    return html`
      <div>${this.slotTemplate}</div>
    `;
  }
}
<element-a .slotTemplate=${html`<element-b></element-b>`}></element-a>

This is essentially how most frameworks work as well, just with a more declarative syntax.

I can’t imagine how Light DOM rendering would be conceived as a misfeature for anyone who seeks to build entire apps or sites with a pure LitElement approach and not least for the reason @iamdriz states. I would prefer to offer a static light = true setting to enable this feature along with an upfront explanation of its consequences with regards to slot support and style encapsulation. Until components may offer an interface for controlled bleeding of the context cascade, official support for Light DOM components will only make the library stronger since CSS encapsulation is simply not always desired, at least not by everyone, and would in any case often be better served with <style scoped> instead of manhandling of the Shadow DOM. I having fun with LitElement both in and out the Shadow DOM and it has certainly restored faith in my career path.

the root of this issue, and lit/lit-element#533, is that developers are frustrated by the shadow-dom’s lack of options for application-level themeing

our current options for application-level themeing:

  • use large amounts of –custom-properties, and keep adding new style hooks for every feature relevant to possible application themeing needs
      → downside: tedious
  • use a light-dom hack like vampire on top of lit-element to emulate slotting
      → downside: hacky

future solutions for application-level themeing:

I wonder if combining the two strategies above you could arrive at a generic ‘Styled component’ that would inject a theme into whatever LitElement you provide, eg:

styled.js:

export default (Base, theme) => {
  return class extends Base {
    static get styles() {
      return [theme, super.styles]
    }
  }
}
import Component from './component.js'
import Styled from './styled.js'
import theme from './theme.js'

customElements.define('my-component', Styled(Component, theme))

You can also use themes in an inheritance pattern.

class BaseElement extends LitElement {
  static get styles() {
    return css`...`;
  }
}

class SpecificElement extends BaseElement {
  static get styles() {
    return [
      super.styles,
      css`...`,
    ];
  }
}

For those who want to “pierce” or “inject” a broader theme stylesheet into all instances of a LitElement, there’s another option that hasn’t been mentioned so far: LitElement’s static styles getter can return an Array instead of a single CSSResult.

So, instead of this:

customElements.define('x-my-element', class extends LitElement {
  static get styles() {
    return css`.whatever{}`
  }
})

You can export something like this:

export default theme => {
  customElements.define('x-my-element', class extends LitElement {
    static get styles() {
      return [theme, css`.whatever{}`]
    }
  }
}

Then you can use your globally-themed component like:

import MyElement from './my-element.js'

const theme = css`.global-things{}`

MyElement(theme)