lit / lit-element

LEGACY REPO. This repository is for maintenance of the legacy LitElement library. The LitElement base class is now part of the Lit library, which is developed in the lit monorepo.
https://lit-element.polymer-project.org
BSD 3-Clause "New" or "Revised" License
4.49k stars 319 forks source link

Feature Request: API for slots without Shadow DOM #553

Closed daniel-nagy closed 4 years ago

daniel-nagy commented 5 years ago

Description

Without primitives for theming Web Components, Shadow DOM is proving to be too great of a burden for my team. I thought I could just opt-out of Shadow DOM and things would be fine, only to realize that slots do not work in the absence of Shadow DOM. Now I'm thinking I may need to abandon Web components entirely.

API Proposal

Provide a way to slot content when choosing not to use Shadow DOM.

Motivation

I've attempted hacking slots with the current API provided by Lit Element only to find that it breaks down in certain cases. The workaround described here is not a good developer experience and may have problems of its own.

moebiusmania commented 5 years ago

@daniel-nagy I've followed a similar discussion here https://github.com/Polymer/lit-element/issues/42, it is intentional to do not implement a slot-polyfill-like feature for non-Shadow DOM components created with lit-element, since by the spec <slot> is a Shadow DOM specific feature, and the focus of this project is to stay as close as possible to the standards.

At first I was skeptical about the decision, but now I'm finding it quite reasonable. Web Components with Shadow DOM solves a specific need to isolate DOM & styles that by now is a unique feature of this solution, every other components library achieve similar results in regular DOM.

So I guess that yes, if you need a solution to have slotted elements with regular DOM components you should consider a library like Vue which is by far the closest solution to Web Components and plays very nice with them. It's about picking the tool that fits most your needs 😃.

I'm not talking on behalf of the lit-html/element team, just summing up the mentioned discussion and giving my opinion 😉

sorvell commented 5 years ago

Thanks for the feedback. We understand the pain here. The styling API for Shadow DOM has taken a lot time to work out and it's been frustrating waiting for it.

Today, you can use css custom properties to penetrate Shadow DOM and this works pretty well for styling targeted properties.

div {
  --button-border: 2px solid orange;
}

For more general styling CSS Shadow Parts will be shipping very soon in Chrome.

my-element::part(ok-button) { 
  border: 2px solid orange;
  color: red;
  padding: 4px;
}

In the meantime, as mentioned in #42 we're not inclined to add a significant feature specifically to make a polyfill the <slot> element since it's specifically designed to work in Shadow DOM. That said, the approach suggested here of performing composition via lit-html TemplateResult's could be viable for your needs. This technique is obviously specific to lit-html and not suitable for creating widely reusable elements, but it does work regardless of whether or not a given element is using Shadow DOM.

To illustrate, here's an example of how this can work:

https://stackblitz.com/edit/lit-element-demo-ieub8y?file=src%2Fslot-like.js.

It's possible to style the "light dom" from the outside as you requested, and there's an example of how the element could (cooperatively) style itself.

daniel-nagy commented 5 years ago

I appreciate the quick response. Maybe I'm doing something wrong but CSS custom properties are not working for me in IE11 with the webcomponentsjs polyfill. I'm also observing behavioral differences when using CSS custom properties in Edge compared to other browsers.

In addition, even if CSS custom properties were working for me in both IE11 and Edge, I don't think they are a valid solution to the problem. They place a great burden on the component author to think of every little thing someone might want to change visually about their component, or tree of components.

The ::part/::theme API sounds promising but it would have to be shimmed and, honestly, when I see a shim for a CSS feature it makes me cringe because I know it is using JavaScript and I fear performance issues/limitations.

If Lit Element were to expose an API for slots without Shadow DOM I absolutely agree it should not attempt to use the <slot> element. It would be nice to use a similar declarative API though. Note that display: contents is not available IE11 or Edge, so the solution should probably leverage DocumentFragment instead.

Here is an example of an API I would like:

@customElement('foo-component')
export class FooComponent extends LitElement {
  createRenderRoot() {
    return this;
  }

  render()
    return html`
      <yield>Default content</yield>
      <div>
        <yield to="bar">Default content</yield>
      </div>
    `;
  }
}
<!-- base case -->
<foo-component></foo-component>

<foo-component>
  hello
  <div content-for="bar">world</div>
</foo-component>

would produce

<!-- base case -->
<foo-component>
  Default content
  <div>
    Default content
  </div>
</foo-component>

<foo-component>
  hello
  <div>
    <div content-for="bar">world</div>
  </div>
</foo-component>
daniel-nagy commented 5 years ago

I experimented with this some more this morning. I have produced the following which is working, for the most part, to slot children in lit-html, Angular, and AngularJs.

import { html, LitElement, PropertyValues } from 'lit-element';

type AdoptedNode = ChildNode & {contentFor?: string};

export const Shadowless = (Base: typeof LitElement) => class extends Base {
  protected slots: {[name: string]: AdoptedNode[] | undefined} = {};

  // Set this to true to adopt children.
  protected willYield = false;

  protected createRenderRoot() {
    return this;
  }

  // I'm just recording which slot the children should render in.
  protected adoptChildren() {
    Array.from(this.childNodes).forEach((child: AdoptedNode) => {
      const slotName = this.getSlotNameForChild(child);
      const {[slotName]: content = []} = this.slots;

      Object.assign(this.slots, {
        [slotName]: [...content, child]
      });
    });
  }

  protected getSlotNameForChild(child: AdoptedNode): string {
    // Both Angular and AngularJS will decorate nodes with comments when they
    // compile their template expressions. When we see a comment directly before
    // an element look ahead to find the slot.
    if (child instanceof Comment && child.nextSibling instanceof Element) {
      return this.getSlotNameForChild(child.nextSibling);
    }

    if ('contentFor' in child) {
      return child.contentFor || '';
    }

    if (child instanceof Element && child.hasAttribute('content-for')) {
      return child.getAttribute('content-for') || '';
    }

    return '';
  }

  protected isTextNodeEmpty(node: Text): boolean {
    return !node.textContent || !node.textContent.trim();
  }

  // I'm not sure what the behavior here should be. If there is an expression
  // but it evaluates to nothing being rendered is the slot empty or not? I'm
  // inclined to think that it is not empty; however, I'm not sure how to deal
  // with the fact that lit-html inserts a bunch of empty text placeholder
  // nodes.
  protected isSlotEmpty(slot: string): boolean {
    const content = this.slots[slot];

    return !content || content.every((child) => {
      return child instanceof Comment
        || child instanceof Text && this.isTextNodeEmpty(child);
    });
  }

  // Adopting children needs to happen here, opposed to connectedCallback,
  // otherwise AngularJS template expressions will not work. You may think that
  // beating all frameworks to the childNodes would be the answer but Angular,
  // for example, will pre-compile the templates. So it is impossible to beat
  // Angular to the childNodes.
  protected update(changedProperties: PropertyValues) {
    if (!this.hasUpdated && this.willYield) {
      this.adoptChildren();
    }

    super.update(changedProperties);
  }

  protected yield(slot: string, defaultConent?: any) {
    const slotContent = this.slots[slot];

    return html`
      ${slotContent}
      ${this.isSlotEmpty(slot) ? defaultConent : undefined}
    `;
  }
}

This mixin can be used to extend your components like so:

@customElement('my-component')
class MyComponent extends Shadowless(LitElement) {
  protected willYield = true;

  render() {
    return html`
      <div>
        ${this.yield('', html`I'm default content`)}
        <div>
          ${this.yield('other-slot')}
        </div>
      </div>
    `;
  }
}

Allowing your component to be used as follows:

<!-- AngularJs example -->
<my-component>
  {{$ctrl.foo}}
  <div ng-repeat="item in $ctrl.items">{{item}}</div>
  <div content-for="other-slot" ng-if="someExpression">
    This will be placed in the other slot.
  </div>
</my-component>
// lit-html example
html`
  <my-component>
    ${this.items.map((i) => html`<div>${i}</div>`)}
    <div .contentFor="${'other-slot'}">${this.name}</div>
  </my-component>
`;

Maybe this will help provide some context to the challenges of implementing this.

daniel-nagy commented 5 years ago

This is looking promising

import { customElement, LitElement, PropertyValues } from 'lit-element';

// Angular and the native template element are incompatible. If you only use
// lit-html as your template engine you can use the native template element
// instead.
@customElement('template-like')
class TemplateLike extends HTMLElement {
  for?: string;
}

export const Shadowless = (Base: typeof LitElement) => class extends Base {
  protected templateMap = new Map<string, TemplateLike>();

  protected createRenderRoot() {
    return this;
  }

  protected getSlotForTemplate(template: TemplateLike): string {
    return template.for || template.getAttribute('for') || '';
  }

  // Save a reference to the templates before lit-element removes them.
  protected saveTemplates() {
    Array.from(this.childNodes).forEach((child) => {
      if (!(child instanceof TemplateLike)) {
        return;
      }

      const slot = this.getSlotForTemplate(child);

      if (!this.templateMap.has(slot)) {
        this.templateMap.set(slot, child);
      }
    });
  }

  protected update(changedProperties: PropertyValues) {
    if (!this.hasUpdated) {
      this.saveTemplates();
    }

    super.update(changedProperties);
  }

  protected yield<T>(slot: string, defaultConent?: T): ChildNode[] | T | undefined {
    const slotContent = this.templateMap.get(slot);

    return slotContent
      ? Array.from(slotContent.childNodes)
      : defaultConent;
  }
}

Your component:

@customElement('my-component')
class MyComponent extends Shadowless(LitElement) {
  render() {
    return html`
      <div>
        ${this.yield('', html`I'm default content`)}
        <div>
          ${this.yield('test')}
        </div>
      </div>
    `;
  }
}

Then just wrap the content you want to slot in a TemplateLike:

<!-- AngularJs example -->
<my-component>
  <template-like>
    hello world
    <div ng-repeat="num in $ctrl.nums">{{num}}</div>
    <button ng-click="$ctrl.addNum()">Add Num</button>
    <button ng-click="$ctrl.showDate = !$ctrl.showDate">Toggle Date</button>
  </template-like>
  <template-like for="test">
    <div ng-if="$ctrl.showDate">{{$ctrl.date}}</div>
  </template-like>
</my-component>
moebiusmania commented 5 years ago

@daniel-nagy you can also force Shady DOM polyfill with this, before loading the polyfill itself

window.ShadyDOM = { force: true }

you achieve the result of a shadowDOM-less webcomponent with slot functionality and it's less work for you. But note that this is a global solution, so you must be sure that you will never use a Shadow DOM component in that context.

https://github.com/webcomponents/shadydom#usage

ghost commented 5 years ago

@daniel-nagy Could lit-html's classMap/styleMap directives help with any part of your pain points?

I will soon be adding info to the LitElement docs for how to import & use these directives, & was going to use a LitElementized version of these types of simple examples.

If you see an application of the directives (I'm not sure how they'd work without shadow DOM though?) it would be good to understand use cases so I can give more meaningful examples in the docs.

daniel-nagy commented 5 years ago

@katejeffreys classMap and styleMap are nice tools for the component author to toggle classes and styles (internally) based on the component's state.

I don't think classMap or styleMap are suitable for the component consumer. You could have an input for every element inside your component but that's not scalable.

I think the CSS Shadow Parts API is what I need but I don't anticipate having that anytime soon. As such, I don't think Shadow DOM is an option for me at this time. If I am unwilling to use Shadow DOM then I need to find a way to slot content without it.

I'm using a slightly modified version of the code I posted above that allows me to slot "default" content without using a template. So far it is working really well, for example I wrapped the Material Components Web TabBar in a Shadowless element and I'm able to use it like so (both <mat-tab-bar> and <mat-tab> are instances of Shadowless):

lit-html

html`
  <mat-tab-bar>
    ${Array.from({length: 3}).map((_, index) => {
      return html`
        <mat-tab>
          <span class="mdc-tab__icon material-icons" aria-hidden="true">favorite</span>
          <span class="mdc-tab__text-label">Tab ${index + 1}</span>
        </mat-tab>
      `;
    })}
  </mat-tab-bar>
`;

AngularJS

<mat-tab-bar>
  <mat-tab ng-repeat="num in [1, 2, 3]">
    <span class="mdc-tab__icon material-icons" aria-hidden="true">favorite</span>
    <span class="mdc-tab__text-label">Tab {{num}}</span>
  </mat-tab>
</mat-tab-bar>

Angular

<mat-tab-bar>
  <mat-tab *ngFor="let num of [1, 2, 3]">
    <span class="mdc-tab__icon material-icons" aria-hidden="true">favorite</span>
    <span class="mdc-tab__text-label">Tab {{num}}</span>
  </mat-tab>
</mat-tab-bar>

Because there is no shadow DOM, the consumer of the component is free to style it using their current, and familiar, set up.

AndreasGalster commented 5 years ago

@sorvell as I understand it according to the webkit tracker the shadow parts spec is not yet agreed upon in terms of forwarding syntax, is this correct? So does this mean there might still be changes to the syntax that's shipping with Chrome?

dahfazz commented 5 years ago

Hi @daniel-nagy I was looking for slot elements inside Light DOM components and I finally did it with StencilJS.

I know it’s an edge case but Stencil ws rhe only one able to answer my need.

daniel-nagy commented 5 years ago

@dahfazz

Global Styles: To externally style a component with Shadow DOM you must use CSS Custom Properties or the proposed CSS Shadow Parts.

Have you successfully used CSS Shadow Parts with stencil and have you tested it in IE11/Edge?

ghost commented 5 years ago

@katejeffreys classMap and styleMap are nice tools for the component author to toggle classes and styles (internally) based on the component's state.

I don't think classMap or styleMap are suitable for the component consumer. You could have an input for every element inside your component but that's not scalable.

Thanks for the reply & added detail, and for sharing your solution!

dahfazz commented 5 years ago

CSS Shadow Parts

@daniel-nagy

Have you successfully used CSS Shadow Parts with stencil and have you tested it in IE11/Edge?

I only heard about CSS Shadow Parts recently but I know it's not supported on IE.

web-padawan commented 5 years ago

AFAIK, Stencil is only using Custom Elements without Shadow DOM, which is easy to confirm by looking at their online demos in dev tools. That explains why they do not suffer from this problem. So their approach is a compromise as well, and they haven't invented a silver bullet here.

Shadow DOM is all about CSS and DOM encapsulation. With the proper architecture, exposing enough styling API in form of custom CSS properties and state attributes, is should not be the problem, especially for design systems (which generally tend to ensure consistent look and feel).

dahfazz commented 5 years ago

@web-padawan Totally agree but... I'm writing components for my company who also has its own Design System. In the end, we want web components able to inherit every global styles from the Design System and, able to provide customizable Slots.

...and we support IE11

daniel-nagy commented 5 years ago

I read the Stencil docs last night. They compile the code and replace the shadow DOM bits with scoped CSS. So you can still use the syntax (:host, ::part) without having to load any polyfills at runtime. This is similar to how Angular's emulated view encapsulation works.

I find Stencil intriguing. I think compiling and scoping the CSS is a better solution than loading a large polyfill with known limitations. I'm already compiling my code using tools like Angular and Typescript anyway.

I intend to play around with stencil when I have some time. It seems the Stencil folks are aware of lit-html and want to offer template literals as an alternative to JSX. However, they've tagged it as post 1.0 which is a bummer. They do their own async rendering so I have a bad feeling JSX is more coupled with their tooling then they led on.

blikblum commented 5 years ago

They do their own async rendering so I have a bad feeling JSX is more coupled with their tooling then they led on.

They use a heavy modified snabbdom emulating slot right into the virtual rendering, so they would have to modify lit-html (or create their own tagged template library) to support what they do now with virtual dom

daniel-nagy commented 5 years ago

Now that I think of it, I don't see how CSS Parts would work when I drop my stencil component in a non-stencil application. How would I target the part using CSS in my Angular application when my Angular application is not compiled using stencil?

@blikblum wouldn't using virtual-dom with lit-html be counterintuitive?

blikblum commented 5 years ago

@blikblum wouldn't using virtual-dom with lit-html be counterintuitive?

Yes, i did not say that would work together. I was replying to your comment (see below) that ionic was planning to support tagged templates. To do so they would need to replace virtual dom with a modified lit-html or their own library. Alternatively could use something like htm on top of virtual dom. None of that alternatives would be interoperable with other libraries / framework (one of the reasons i discarded Stencil to my use)

It seems the Stencil folks are aware of lit-html and want to offer template literals as an alternative to JSX

NiNJAD3vel0per commented 5 years ago

Please give a full example sample project to integrate lit-element with Angular7+ (not Angular.js). and What is different between Angular element vs. lit-element?? which is better?

kevinmpowell commented 5 years ago

@NiNJAD3vel0per I think you would be better off creating new Issue for this question or posting on Stack Overflow. This seems unrelated to the thread.

kevinmpowell commented 5 years ago

I too am wrestling with the need for this feature. One of the Design System principles we adhere to is "Products own their own Destiny." So while shipping components with encapsulated styles is desirable and a first step, we never prevent consuming products from overriding whatever they need to within a component.

Short of exposing every possible property via CSS variables or CSS parts, this isn't possible with a ShadowDOM-based component.

There are other shortcomings of ShadowDOM too:

For those reasons we're struggling to go "all in" on ShadowDOM and yet we really want <slot> functionality.

StencilJS does have a light DOM <slot> implementation, but also a lot of runtime overhead and more opinions around how your library of components should be architected (versioned at the library level, not the component level).

So I see an opening here for a Web Component framework that has:

I know light dom <slot>'s aren't official spec, but neither is automatic property/attribute reflection or rendering via HTML template literals. Those are features lit-element has built on top of the official specs. I'd love to see this added.

justinfagnani commented 5 years ago

I know light dom <slot>'s aren't official spec

They're not just not official, it's not viable. There is one set of children elements. You can't combine two sets into one without something like Shadow DOM.

daniel-nagy commented 5 years ago

There is one set of children elements. You can't combine two sets into one without something like Shadow DOM.

@justinfagnani Can you elaborate a little more? Are you referring to the fact that lit-html is going to replace the children of a light DOM element? I'm able to work around this using an explicit container (template or template like) element which I obtain a reference to before lit-html replaces the content.

chase-moskal commented 5 years ago

hello friends,

i'm a simple man, and my application is like a big style dictator -- always bossing around the components, and overriding anything it wants... but the components don't mind too much… and i don't mind either -- just so long as one component doesn't tell a sibling or parent what to do, i'm pretty cool with it

i don't know about all this highfalutin shadow-dom business though, where the components are so independent that they won't bow to anyone (sounds like capitalism!), or maybe worse, components are sharing and exposing their styles with one-another (sounds like communism!)

just silly bad jokes people, i apologize ;)

of course, there's a use-case for every scenario -- but i get the impression that folks who share my mindset may need to wait until the shadow dom lightens up a little before using lit-element?

for now, maybe lit-element is more focused on web components that don't like to be bossed around, and are more opinionated about their appearance?

i understand that lit-html itself doesn't use the shadow dom, so a different solution built on lit-html, like haunted, might be interesting? i need to figure out how this slotting problem might affect other solutions..

@justinfagnani

There is one set of children elements. You can't combine two sets into one without something like Shadow DOM.

also, i don't really understand what this means and would like to know more

cheers, loving the lit revolution, big ups!

LarsDenBakker commented 5 years ago

If you use lit-html across your application, you can easily implement light dom slots using template as properties:

https://stackblitz.com/edit/lit-element-example-odcruw

There are quite some creative solutions possible to decide where to render your element's content into, but a major complication is that you cannot assume that rendering is synchronous and light dom children can be changed at any time. It's really difficult to take this into account, and to do the proper timing.

Shadow dom is the correct abstraction, it solves many issues of the web. It is simply a matter of time before the rest of the world catches up.

justinfagnani commented 5 years ago

@chase-moskal

@justinfagnani

There is one set of children elements. You can't combine two sets into one without something like Shadow DOM.

also, i don't really understand what this means and would like to know more

Given:

<my-element>
  <p>I'm a child</p>
</my-element>

And <my-element> wants to render to light DOM, what's supposed to happen?

daniel-nagy commented 5 years ago

@justinfagnani

And my-element wants to render to light DOM, what's supposed to happen?

I would say in this case the <my-element> custom element would ignore the slotted content and render its own content. There needs to be an explicit container for the consumer to communicate their intent to slot content. e.g.

<my-element>
  <template>
    <p>I'm a child</p>
  </template>
</my-element>

I can go into more detail about how named slots could work, or share my solution which has worked pretty well.

Edit

The <template> element is a good candidate because it doesn't actually render any content to the DOM, however the <template> element doesn't work with frameworks that precompile their templates, such as Angular.

Aside

Sorry for opening and closing this issue. I have a habit of using keyboard shortcuts to enter newlines above/below where I'm typing which seems to trigger the close and comment function.

justinfagnani commented 5 years ago

I would say in this case the custom element would ignore the slotted content and render its own content.

Ok. Now let's use the programmatic API that all elements have:

const myElement = new MyElement();

const updateStuff = () => {
  myElement.innerHTML = `<p>I'm a child</p>`;
};

// some time later
updateStuff();

Now we've just blown away the content that <my-element> rendered.

And this:

  <template>
    <p>I'm a child</p>
  </template>

Means that anything inside the template is inert until closed. You can't assign properties, add event listeners, etc., to what's actually in the markup and have it survive cloning. So it won't work with lit-html templates. IOW, this won't work:

html`
  <template>
    <button @click=${onClick}>Click Me</button>
  </template>
`

Any "solution" here is going to have big limitations and pretty unsatisfactory answers to composition compared to Shadow DOM, because there are exactly the problems Shadow DOM was designed to fix.

I'd say anyone who really wants to do something like this should just make a contract for their element that content has to go in a specific child, and override createRenderRoot.

Something like:

  createRenderRoot() {
    let root;
    // can't use querySelector because it would break with nesting because we don't
    // have shadow DOM boundaries anymore. We don't know what's the content
    // of this element, vs the content of child elements. 🤷‍♂️
    for (const child of this.childElements) {
      if (child.matches('div.content') {
        root = child;
        break;
      }
    }
    if (root === undefined) {
      root = document.createElement('div');
      root.className = 'content';
      this.appendChild(root);
    }
    return root;
  }

This really isn't that different from old frameworks like bootstrap that just tell you what the DOM has to be for a given element and you have to make it so.

daniel-nagy commented 5 years ago

hmm.. I just tested your button example with my solution and it worked. I have an element with the following render function:

html`
  <p>Light DOM!</p>
  <my-component>
    ${this.items.map((i) => html`<div>${i}</div>`)}
    <template .contentFor="${'test'}">
      <button @click=${this.onClick}>Click Me</button>
      ${this.name}
    </template>
  </my-component>
  <button @click="${this.onAddItem}">
    Add Item
  </button>
  <button @click="${() => this.name = 'fred'}">Change Name</button>
`;

Where <my-component> does not use shadow DOM and accepts slotted content. It seems that I was able to slot children to a default slot without wrapping the children in a container element as well.

I'll admit that there are probably some edge cases with my solution but it is a working POC.

justinfagnani commented 5 years ago

The click handler worked?

daniel-nagy commented 5 years ago

yeah

justinfagnani commented 5 years ago

I don't see how the click handler could possibly be working. It's not how templates, cloning or lit-html work.

See this snippet, the button's click handler is not invoked:

let t = document.createElement('template');
t.innerHTML = `<button>Click Me</button>`;
let b = t.content.querySelector('button');
b.addEventListener('click', () => console.log('click'));
document.body.append(document.importNode(t.content, true));

If you have a repro showing the button is working, I'm quite interested.

edit: this snippet using lit-html doesn't work either:

(async () => {
  const {render, html} = await import('https://unpkg.com/lit-html');

  render(
    html`
      <template>
        <button @click=${() => console.log('click')}>Click Me</button>
      </template>`,
    document.body);
  const t = document.querySelector('template');
  document.body.append(document.importNode(t.content, true));
})();
daniel-nagy commented 5 years ago

Ah, I'm not cloning the document fragment. I'm using it directly. I think this is ok though, as long as the consumer supplies a new template for each instance of the component. So yeah, you wouldn't be able to prepare a template and reuse it, most likely. I'm not sure that is really an issue though, if you need a reusable template you could just package it as a new custom element.

It's also possible to do it without the <template> element, using a different container element or by constructing a document fragment. No cloning is involved though, just moving existing elements.

daniel-nagy commented 5 years ago

For reference this is what I've been doing https://stackblitz.com/edit/typescript-2ufoty.

chase-moskal commented 5 years ago

@justinfagnani

Given: <my-element><p>I'm a child</p></my-element> And <my-element> wants to render to light DOM, what's supposed to happen?

your riddle has shown me the problem — since the light dom will render to the same location as we source the children.. we're stuck with some kind of chicken-egg problem here

@daniel-nagy — your solution seems really cool!

morewry commented 5 years ago

There are numerous reasons I'd want this (some that have already been mentioned). So I thought I'd chime in, although I agree it doesn't strike me as a long term feature of a library like this one.

My perspective is this functionality is mostly needed to work around existing limitations in and issues with Shadow DOM so that Web Components can be effectively used throughout the transitional period while incompatibilities decrease, the platform fills in the gaps, etc. The goal is unblocking Web Component usage by providing a practical stranglehold migration path.

The issues I've encountered or heard of that need workarounds include:

In any case where a component's interface would require slots, but Shadow DOM in its current incarnation has issues that make it not viable to use, you're between a rock and hard place. Either you get no slots, or you get no form participation, accessibility, or styles (perhaps at all, perhaps only with major expense). This can lead to no Web Components.

In the long term, my hope and assumption is that these issues will be fixed by increased support, new specs, or evolutions to the current specs and that eventually using Shadow DOM will have none of these significant downsides.

chase-moskal commented 5 years ago

@daniel-nagy -- could we formalize your light-dom <template> slotting solution into a new package, something like lit-element-light or more generically light-slotting? what do you think?

edit: could it be a class mixin for standard web components, cleanly decoupled from lit?

morewry commented 5 years ago

FYI, we've recently verified that Stencil does this. Unfortunately, it forces TypeScript on you. Also, JSX with its htmlFor requirement.

daniel-nagy commented 5 years ago

@chase-moskal

could we formalize your light-dom \<template> slotting solution into a new package, something like lit-element-light or more generically light-slotting? what do you think?

I don't believe the license would allow me to use lit in the name of the package. I think Vampire would be a cool name.

could it be a class mixin for standard web components, cleanly decoupled from lit?

I've been testing this and I think this would be possible. I was getting hung-up on the fact that white space creates Text nodes and prevents the fallback content within a slot from being rendered but then I noticed that Shadow DOM slots behave the exact same way, https://lit-html.polymer-project.org/guide/writing-templates#nothing-and-the-slot-fallback-content.

I'd like to create a slot API that mimics the spec as close as possible, both in terms of syntax and behavior. That way the migration path to Shadow DOM slots would mostly be renaming things. As @morewry pointed out, for some people this would be a tool to start investing in Web Components today, with the intent of opting into Shadow DOM at a later date. However, I would like to use Custom Elements as the building blocks for my applications. Shadow DOM is not appropriate, in my opinion, for building applications. But without LightDOM slots, Custom Elements are not appropriate for building applications either.

ghost commented 5 years ago

Hello @daniel-nagy is there any reason the solution in here in the referenced issue is problematic?

I was just tooling around with this and it does appear to give the behavior desired as per the discussions I'm seeing in this thread.

It would seem this emulates slots without a real performance hit, that I can tell anyway.

One question I also have for anyone...if I'm reading this thread and the docs correctly, the problem here is that lit-html is overwriting the children, and not that Custom Elements don't support having children natively?

If thats the case, then why doesn't lit-html just track where to render using the append, closest, before, after or prepend apis?

The browser support is pretty much there considering lit-elements target browsers. There are also polyfills if really needed for each of these apis.

Just a thought, I may gravely misunderstand the mechanics of this.

daniel-nagy commented 5 years ago

@ProtonScott The solution you linked to still relies on Shadow DOM. IE 11 and Edge do not support Shadow DOM. Bundling the Shadow DOM polyfill in your app just to get Light DOM slots is disappointing at best.

One question I also have for anyone...if I'm reading this thread and the docs correctly, the problem here is that lit-html is overwriting the children, and not that Custom Elements don't support having children natively?

That's part of the problem but it is not the whole problem, nor is it a problem that is unique to lit-html. Even if you created a pure vanilla custom element (with no shadow root) you would have a hard time managing slotted content.

After some time of thinking about this I have the idea of a <light-root> that your custom element renders to. Then a mutation observer could be used to watch for children being added to your component, ignoring the <light-root>, and move them to the appropriate slot.

ghost commented 5 years ago

@daniel-nagy That sounds fair, and your new idea of using a <light-root> element with a mutation observer sounds pretty much what I would envisions would be a clean native solution to this problem.

The only other thing I could think of for server-side templating, anyway (may not work with all engines, think Jinja2 and PHP-Twig, not Angular or Vue, i imagine) is using the <template> element and just calling its content into a lit-html function and rendering those contents. I think this is a similiar idea to what you previously posted

For what its worth, once Edge fully transitions to Chromium it'll finally have first class support for all this! 🤣

daniel-nagy commented 5 years ago

Well I made an attempt. You can find it here - @boulevard/vampire. It uses Mutation Observer and is decoupled from LitElement as discussed. It seems to work ok with LitElement and Angular based on some simple components I've tested. It does not work ok with AngularJS. For AngularJS you may be able to decorate the $compile service or attach directives to some of the custom elements to get it working. I haven't tested it with any other frameworks.

Empty Text nodes are difficult to deal with and I can't just ignore them because lit-html may use them to keep track of where parts are.

Every slot will create 2 mutation observers (childlist only), one on the parent of the root and another on the assigned content.

Unlike Shadow DOM, where content is only projected to the slot, this will move the content to the slot.

justinfagnani commented 4 years ago

I think since there's a non-LitElement solution, that we can close this. We want to keep LitElement simple and generally aligned with the basic platform primitives. Theming is an issue, so we continue to push on the standards.

gmurray81 commented 4 years ago

@daniel-nagy I've been tooling around with what seems like its actually a similar solution to what you are describing. I think one issue might be that it wouldn't really give you a solution for removing slotted children. Since you have moved them to your light root, they no longer appear to be direct children of the component. so can't really be interacted with anymore.

If you cloned the children, and put clones in the light root, you could continue to observe the direct children to see if they have been removed, and synchronize, but then you'd start to run into related issues with the children having been cloned. Possibly you could leave the clones as direct children and move the originals into the light root....

lehwark commented 1 year ago

If you just need one "slot" you can simply render ${this.children} :

import { html, LitElement } from "lit";
import { customElement } from "lit/decorators";

declare global {
    interface HTMLElementTagNameMap {
        "my-my": MyMy;
    }
}

@customElement("my-my")
export class MyMy extends LitElement {
    createRenderRoot() {
        return this;
    }

    render() {
        return html`
            <div>
                <h2>Mymy</h2>
                <div class="lightSlot">${this.children}</div>
            </div>
        `;
    }
}