Git Product home page Git Product logo

Comments (21)

moebiusmania avatar moebiusmania commented on August 30, 2024 16

@LarsDenBakker this is a real good point, and for my experience a shared one. BTW YouTube itself doesn't use full Shadow DOM for the same reasons.

The _createRoot => this trick may be intuitive or not, but at least as opposite of Polymer it let you decide to use Shadow DOM or not individually, the main downside is that you lose support for the <slot> element.

It would be nice to have a sort of bultin slot polyfill activated when you opt to not use Shadow DOM.

from lit-element.

aaronanderson avatar aaronanderson commented on August 30, 2024 13

This worked for me:

@customElement('test-element')
export class TestElement extends LitElement {

  elementChildren: Array<Element> = [];

  createRenderRoot() {
    return this;
  }

  connectedCallback() {
    this.elementChildren = Array.from(this.children);
    super.connectedCallback();
  }

  render() {
    return html`<div>${this.elementChildren}</div>`;
  }  

}

from lit-element.

jolleekin avatar jolleekin commented on August 30, 2024 12

@asbachb @atla5 Here's one workaround to the missing slot functionality.

@customElement('my-element' as any)
export class MyElement extends LitElement {
  /** The header content. Usually a [TemplateResult] but could be anything. */
  header?: any;

  /** The footer content. Usually a [TemplateResult] but could be anything. */
  footer?: any;

  /**
   * The body template function.
   *
   * The return value is usually a [TemplateResult] but could be anything.
   */
  bodyTemplate?: () => any;

  protected createRenderRoot() {
    return this;
  }

  protected render(): TemplateResult {
    return html`
      <style>
        /*
        Can't use :host here since we're rendering into the light DOM.

        NOTE:
        Duplicated styles will happen if there are more than one
        my-element in the same scope.
        */
        my-element {
          display: block;
        }
      </style>
      ${this.header}
      ${this.bodyTemplate ? this.bodyTemplate() : null}
      ${this.footer}
    `;
  }
}

Usage

<my-element
  .header=${html`<h2>Header</h2>`}
  .bodyTemplate=${() => html`<p>Body</p>`}
>
</my-element>

from lit-element.

sorvell avatar sorvell commented on August 30, 2024 8

We'd like to encourage a default of using Shadow DOM because it's the right option when making a re-usable element since these need encapsulated dom and css.

You have a point that when making an application, it is sometimes reasonable not to use Shadow DOM, and this is why we provide the _createRoot override point.

We're open to making this API simple and would welcome a proposal / PR.

from lit-element.

pgoforth avatar pgoforth commented on August 30, 2024 8

The issue is compounded by the fact that returning any TemplateResult in your render method will automatically remove the children that used to be in your component and replace them with what was rendered in the template. What we need is a way to easily re-add the children into the generated template in much the same way that React allows you to add children.

the only way I've been able to get around it is by doing something similar to the following:

const makeSlot = (name) => {
    const slot = document.createElement('slot');
    if (name) {
        slot.name = name;
    }
    return slot;
};

class CustomElement extends LitElement {
    constructor() {
        super();

        this.shadowRoot.appendChild(makeSlot('before'));
        this.shadowRoot.appendChild(makeSlot());
        this.shadowRoot.appendChild(makeSlot('after'));
    }

    connectedCallback() {
        super.connectedCallback();
        this.observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.removedNodes.forEach((node) => {
                    if (node.nodeType !== Node.COMMENT_NODE) {
                        this.appendChild(node);
                    }
                });
            });
        });

        this.observer.observe(this, {
            childList: true,
        });
    }

    firstUpdate() {
        this.observer.disconnect();
    }

/* ... */

    createRenderRoot() {
        this.attachShadow({mode: "open"});
        return this;
    }

    render() {
        return html`
            <div slot="before">Templated Content Before</div>
            <div>Main Content - will show up before children</div>
            <div slot="after">Templated Content After</div>
        `;
    }
}

customElements.define(`pseudo-shadow-custom`, CustomElement);
<pseudo-shadow-custom>
    <p>Markup Child</p>
<pseudo-shadow-custom>

Would render

<pseudo-shadow-custom>
    <div slot="before">Templated Content Before</div>
    <div>Main Content - will show up before children</div>
    <p>Markup Child</p>
    <div slot="after">Templated Content After</div>
<pseudo-shadow-custom>

@asbachb @LarsDenBakker @Rybadour
In this method, you are still kind-of using the shadow-dom, but all your elements are rendering in the context of the custom element...so your CSS will cascade, you can still make custom elements that use templates and leverage lit-element. Hopefully this solution can accommodate your needs. If this functionality works for everyone, it would be a great addition and I could make a PR for it.

from lit-element.

svdoever avatar svdoever commented on August 30, 2024 4

@aaronanderson seems to work for me as well!

I created a file LitElementLight.ts:

import { LitElement } from 'lit-element';

class LitElementLight extends LitElement {
    elementChildren: Array<ChildNode> = [];
    slotContents: any;

    connectedCallback() {
        this.elementChildren = Array.from(this.childNodes);
        super.connectedCallback();
    }

    get slotElements(): ChildNode[] {
        return this.elementChildren;
    }

    createRenderRoot() {
        return this;
    }
}
    
export {
    LitElementLight
};

and now can create "light" elements as follows:

test-light.ts:

import { html, customElement } from 'lit-element';
import { LitElementLight } from './LitElementLight';

@customElement('test-light')
class TestLight extends LitElementLight {
    render() {
        return html`
            <div>I'm styled by the light!</div>
            <div>${this.slotElements}</div> 
            <div>Styling is global</div> 
    `;
    }
}

declare global {
    interface HTMLElementTagNameMap {
        'test-light': TestLight;
    }
}

from lit-element.

atla5 avatar atla5 commented on August 30, 2024 3

i was able to toggle between shadow and light DOM in LitElement by implementing createRenderRoot() as return this;, as per @sorvell's statement above and fitting the documentation in @thepassle's README (last bullet):

createRenderRoot(){ return this; }

as advertised, it did "break" the <slot> functionality. could someone explain why this is the case?

from lit-element.

LarsDenBakker avatar LarsDenBakker commented on August 30, 2024 1

@sorvell Thanks for your response. I'd be happy to make a PR, but we'd need to iron out some architecture first.

A pattern I'd like to explore is the following:

<app-element-a>
  <style> element-d { border: 1px solid black; } </style>

  <app-element-b>
    <element-d>
      #shadowroot
    </element-d>
  </app-element-b>

  <app-element-c>
    <element-d>
      #shadowroot
    </element-d>
  </app-element-c>

</app-element-a>

In this example (pseudocode) element a, b and c don't use shadow dom, but render their template to the light dom. Element d is a reusable components and uses shadow dom.

Element a renders styles which applies to all instances of element d in the dom of both children.

This works fine on native shadow dom, the styles will automatically apply from element a down to the first shadow root it reaches. But shady dom will scope the styles so that element a cannot select elements into element b. We can't turn off scoping, as it will make the styles apply everywhere, but perhaps there is some solution to this? An alternative could be to define scopes manually and pass them down as properties to sub components.

In terms of API, what I think would make it cleaner, and give more opportunities for optimization, is if we separate out the common functionality from LitElement into a LitElementBase class and then provide a LitElementLight class which renders to light dom. This will make it easier to add other features that make sense for rendering to light dom, and also reduce the required knowledge of internals for developers.

from lit-element.

sorvell avatar sorvell commented on August 30, 2024 1

Sorry for the delay on this. The new way to customize the rendering element is to implement createRenderRoot() (renamed from _createRoot).

We'd like to encourage use of Shadow DOM in LitElement and this is why it's the default. Since it's straightforward to customize this, we're inclined to leave it as is for now. It is completely reasonable to create a subclass that customizes this behavior as you see fit.

from lit-element.

asbachb avatar asbachb commented on August 30, 2024 1

I wonder if there's a workaround for having slot functionality when disabling Shadow DOM?!

from lit-element.

Westbrook avatar Westbrook commented on August 30, 2024

@LarsDenBakker could you clarify this paragraph a little further?

This works fine on native shadow dom, but shady dom will hoist the styles to the head and scope the styles so that element a cannot select elements into element b. We can't turn off scoping, as it will make the styles apply everywhere, but perhaps there is some solution to this? An alternative could be to define scopes manually and pass them down as properties to sub components.

When you say "We can't turn off scoping, as it will make the styles apply everywhere" I can't understand how that's different from element-a having shadow dom so it's styles don't leak, but element-b and element-c using light dom so the styles from element-a apply to them.

It would seem that this approach would be available at current without any API changes, and wanted to make sure I understood you correctly. I've been doing something similar by extending the _createRoot technique as follows when not wanting to use scoped styles:

  /**
   * Set root to `this` so that styles are not contained.
   *
   * @return {object}
   */
  _createRoot() {
    return this;
  }
  /**
   * Prevent the hoisting and scoping of styles.
   */
  _applyRender(result, node) {
    render(result, node);
  }

My use case at current is to apply style to a page (when not building my app from a single shared parent node, and seems like it would do the work you're looking for if applied to element-b and element-c in your example.

Thanks in advance for sharing your thoughts, this is an important sticking point to work though in advance of bringing users in from other style application techniques!

from lit-element.

LarsDenBakker avatar LarsDenBakker commented on August 30, 2024

@Westbrook

Given:

<app-element-a>
  <style> element-d { border: 1px solid black; } </style>

  <app-element-b>
    <element-d>
      #shadowroot
    </element-d>
  </app-element-b>

  <app-element-c>
    <element-d>
      #shadowroot
    </element-d>
  </app-element-c>

</app-element-a>

Compiles to shady dom like so (pseudocode):

<head>
  <style scope="app-element-a">
    element-d.app-element-a {
      border: 1px solid black;
    }
  </style>
</head>

<app-element-a>
  <style> element-d { border: 1px solid black; } </style>

  <app-element-b class="style-scope app-element-a">
    <element-d class="style-scope app-element-b">
      #shadowroot
    </element-d>
  </app-element-b>

  <app-element-c class="style-scope app-element-a">
    <element-d class="style-scope app-element-b">
      #shadowroot
    </element-d>
  </app-element-c>

</app-element-a>

So where in native shadow dom, a selector like element-d would match the elements inside element-b and element-c, in shady dom the selectors are scoped to only the dom of element-a.

We could do what you're saying and use regular render instead of shady render, but it has two downsides. One is that it removes other parts from the shady dom polyfill such as css mixins and css variables. The other is that scoping is turned off. So instead of matching only the instances of element-d up until the first shadow root inside element-a, it will match all instances of element-d on the page.

I'm also interested in elements being able to style upwards like you're describing. We have a very large application with architecturally separated pages developed by different teams. I'd like for the active page to be able to theme the larger application. With native shadow dom it's possible.

from lit-element.

LarsDenBakker avatar LarsDenBakker commented on August 30, 2024

After looking into this further, it seems I'm not entirely correct. I was running into the above situation because I had no top level element with a shadow root, but when there is it works more like expected.

Styles are correctly scoped from the top shadow root down to the bottom shadow root across components. However, styles within custom elements without shadow roots are not hoisted.

I made a jsbin to illustrate what I mean. View both on chrome.
This construction works in shadow dom:
http://jsbin.com/pufosopeki/1/edit?html,output
But not in shady dom (forced polyfill):
http://jsbin.com/mazimigara/1/edit?html,output

I don't understand enough about how shady dom/css works, but perhaps lit/lit#337 will make this work correctly?

from lit-element.

Christian24 avatar Christian24 commented on August 30, 2024

@atla5 <slot> is only available when using shadow dom by shadow dom specification.

from lit-element.

Rybadour avatar Rybadour commented on August 30, 2024

@jolleekin Does this mean there is no way have child element like my below code without shadow DOM?

<custom-list>
   <li>foo</li>
   ...
</custom-list>

from lit-element.

jsilvermist avatar jsilvermist commented on August 30, 2024

@Rybadour You would have to query the elements and manually append them at load time if I'm not mistaken.

from lit-element.

RameezAijaz avatar RameezAijaz commented on August 30, 2024

If you just want to render all the children inside host's dom then using connectedCallback() lifecycle event even with empty body will make child component render inside the host without shadowDom

@customElement('test-element')
export class TestElement extends LitElement {

    connectedCallback(){
    }
    createRenderRoot() {
        return this;
    }

    render() {
        return html`
        `;
    }
}

Then following code

<test-element>
     <p>Hello World</p>
</test-element>

will render

<test-element>
     <p>Hello World</p>
</test-element>

from lit-element.

fouad-j avatar fouad-j commented on August 30, 2024

Hello,

Thanks @svdoever and @aaronanderson for your proposal.

I improved the code to allow displaying Slot in different parts of custom element.

import {LitElement} from 'lit-element';

class LitElementLight extends LitElement {
    slotMap: object;

    connectedCallback() {
        this.slotMap = Array
            .from(this.renderRoot.querySelectorAll('[slot]'))
            .reduce((map, obj) => ({
                ...map,
                [obj.getAttribute('slot')]: obj
            }), {});

        super.connectedCallback();
    }

    protected getSlot(slotName: string): ChildNode {
        return this.slotMap && this.slotMap[slotName];
    }

    createRenderRoot() {
        return this;
    }
}

export {LitElementLight};
@customElement('test-light')
class TestLight extends LitElementLight {
    render() {
        return html`
            <div>LitElement content bla...</div>
            <div>${this.getSlot('actions')}</div>
            <div>Styling is global</div>
    `;
    }
}

declare global {
    interface HTMLElementTagNameMap {
        'test-light': TestLight;
    }
}
<test-light>
  <div slot="actions">
    <button type="button" class="btn btn-primary" (click)="toggleImage()">Angular Action</button>
    <button type="button" class="btn btn-secondary" v-on:click="counter += 1">VueJS Action</button>
  </div>
</test-light>

I created a public gist
Please let me know if you have any suggestion

from lit-element.

justinfagnani avatar justinfagnani commented on August 30, 2024

@svdoever the problem with that approach is that it breaks composition. Since you record the light DOM children on at connected, you can get into a number of situations where the children are overwritten later, or re-recorded later.

The simplest example is disconnecting and reconnecting:

const el = new TestLight();
document.body.append(el);
console.log(el.innerHTML); // shows 3 divs
const container = document.createElement('div');
document.body.append(container);
container.append(el); // should throw as you're trying to add the second div as a child to itself

Another example is using this with a template system:

const go = (children) => render(
    html`<test-light>${children}</test-light>`,
    document.body);
go(html`<p>render 1</p>`); // renders 3 divs, with <p> as child of second
go(html`<p>render 2</p>`); // renders one <p> that overwrites the divs

There are of other cases where this breaks too. This is a big reason I'm not comfortable with most of the proposed solutions in the space. Shadow DOM is a composition primitive that's valuable precisely because composition is hard to impossible to get right without it.

from lit-element.

svdoever avatar svdoever commented on August 30, 2024

This is the reason why I had to move over to using StencilJS, where slots are supported without shadow dom.

from lit-element.

andyjessop avatar andyjessop commented on August 30, 2024

This is the biggest issue for us adopting web components. We would have to use the light DOM because we use CSSModules to style our components, but if we can't have composition then web components are frankly unusable in a modern web app without huge friction.

from lit-element.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.