Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

litElement doesn't support nested components #824

Closed
iamdriz opened this issue Sep 26, 2019 · 16 comments
Closed

litElement doesn't support nested components #824

iamdriz opened this issue Sep 26, 2019 · 16 comments

Comments

@iamdriz
Copy link

iamdriz commented Sep 26, 2019

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?

@jordanaustin
Copy link
Contributor

Yep this is definitely supported. With web components you need a name attribute on your slot element, and then you need to add a slot attribute to your nested components that matches the slot's name

@iamdriz
Copy link
Author

iamdriz commented Sep 26, 2019

@jordanaustin Tried that. And the result is the same.

<parent-element>
    <nested-element slot="inner"></nested-element>
    <nested-element slot="inner"></nested-element>
</parent-element>
class ParentElement extends LitElement {

    constructor() {
        super();
    }

    render() {
        return html`<div class="parent"><slot name="inner"></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);
<parent-element>
    <!---->
    <div class="parent"><slot name="inner"></slot></div>
    <!---->
    <nested-element slot="inner">
        <!---->
        <div class="nested"></div>
        <!---->
    </nested-element>
    <nested-element slot="inner">
        <!---->
        <div class="nested"></div>
        <!---->
    </nested-element>
</parent-element>

Using lit-element version: "^2.2.1"

@abraham
Copy link
Contributor

abraham commented Sep 26, 2019

You're initial samples seem to be working fine in my test: https://stackblitz.com/edit/as9syq?file=my-element.js

@iamdriz
Copy link
Author

iamdriz commented Sep 26, 2019

@abraham Ah bugger so I forgot to add to my question I use:

    createRenderRoot() {
        return this;
    }

This is because I want all components to inherit the CSS from the main stylesheet of the page and NOT have encapsulation. But it seems this then causes the problem I have posted above...

So how can I allow the nesting of components but allow the components to use the styles from the top-level page they are used on?

@justinfagnani
Copy link
Contributor

You can't not use shadow DOM and use <slot>, because <slot> is a feature of shadow DOM.

I'd really recommend not using createRenderRoot. It's for very, very special cases and probably a misfeature for general use.

@iamdriz
Copy link
Author

iamdriz commented Sep 27, 2019

@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.

@wiredearp
Copy link

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.

@iamdriz
Copy link
Author

iamdriz commented Sep 27, 2019

Is this actually a limitation of LitElement or is it Web Components themselves that are where this limitation lies? If it's purely the way in which LitElement currently works then it'd be great to see someway of solving this... if not... I'm not sure what we can do... seems a shame if the ability to have a Light DOM and nest components is impossible though.

@LarsDenBakker
Copy link
Contributor

LarsDenBakker commented Sep 27, 2019

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.

@web-padawan
Copy link
Contributor

There has been a similar discussion in #553 and it was closed. See #553 (comment).

So this is likely a duplicate and should be closed as well.

@chase-moskal
Copy link

the root of this issue, and #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:

@hunterloftis
Copy link

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)

@abraham
Copy link
Contributor

abraham commented Oct 6, 2019

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`...`,
    ];
  }
}

@hunterloftis
Copy link

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))

@danielbarion
Copy link
Contributor

@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.

I have solved the inherit problem + shadowRoot with a base class like this:

Base Class (Lit-Element)

import { LitElement, html, css, unsafeCSS } from 'lit-element'
import i18next from 'i18next'
import style from './style.styl'

/**
 * This class contains the generic things
 * who need to have in all components and modules
 */
class LitElementTranslation extends LitElement {
	static get styles() {
		return [css`${unsafeCSS(style)}`]
	}

	connectedCallback() {
		super.connectedCallback()

		this.i18nextLanguageWatcher()
	}

	/**
	 * Funcs
	 */
	i18nextLanguageWatcher() {
		i18next.on('languageChanged', () => {
			this.requestUpdate()
		})
	}

	/**
	 * HTML
	 */
	render() {
		return html`
			<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/material-components-web.min.css">
			${this.icons()}
		`
	}

	/**
	 * Import material design icons
	 */
	icons() {
		return html`
			<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin>
			<link href='https://fonts.googleapis.com/icon?family=Material+Icons' rel='stylesheet'>
		`
	}
}

export default LitElementTranslation

My component who use my custom base component:

import { html, css, unsafeCSS } from 'lit-element'
import LitElement from 'utils/lit-element'
import style from './style.styl'

class Button extends LitElement {
	static get properties() {	return {
		id: { type: String },
		label: { type: String }
	}}

	static get styles() {
		return [
			super.styles,
			css`${unsafeCSS(style)}`
		]
	}

	constructor() {
		super()

		this.id = ''
		this.label = ''
	}

	/**
	 * Funcs
	 */
	handleClick() {}

	/**
	 * HTML
	 */
	render() {
		return html`
			${super.render()}
			${this.main()}
		`
	}

	main() {
		return html`
			<div
				class='button'
				id=${this.id ? this.id : ''}
				@click=${this.handleClick}
			>
				${this.label}
			</div>
		`
	}
}

customElements.define('wc-button', Button)

We just need export the base component as we want, import him and call super methods.

Lit-element are very smart, the import of material icons (request) will occur just one time if you take a look in console network, so, this is very cool! Thank you Lit devs!

@justinfagnani
Copy link
Contributor

So... this issue is mostly about how standard shadow DOM works. Shadow roots don't inherit page styles, and it's required if you want to use <slot>. We think these are important topics for future standards and we'll be bringing this issue up in the relevant meetings soon.

Until then the approach of importing styles is good, and we're working on a theming system that you can track here: https://github.com/Polymer/lit-element/issues/835

I'm going to close this in favor of the more specific and actionable topic in #825 and for the standards side I just opened WICG/webcomponents#864 . It'd be great to get some actual web component users in there with their real-world use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants