Comments (21)
@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.
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.
@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.
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.
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.
@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.
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.
@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.
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.
I wonder if there's a workaround for having slot
functionality when disabling Shadow DOM?!
from lit-element.
@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.
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.
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.
@atla5 <slot>
is only available when using shadow dom by shadow dom specification.
from lit-element.
@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.
@Rybadour You would have to query the elements and manually append them at load time if I'm not mistaken.
from lit-element.
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.
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.
@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.
This is the reason why I had to move over to using StencilJS, where slots are supported without shadow dom.
from lit-element.
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)
- Typescript error on latest build HOT 1
- mixins don't work when using ESNext in tsconfig to compile HOT 1
- Error throw when accessing queryAssignedNodes getter (No version >2.4.0) HOT 1
- Support for contexts? HOT 2
- Persistance? HOT 5
- idempotent/soft customElement decorator HOT 1
- state() is shown as deprecated in LitElement 2.5 HOT 1
- Should not be necessary to copy TemplateResult
- jsdelivr/+esm: [object Object] HOT 1
- Nested components, slot and text styling HOT 1
- Git tags are not in sync with npm tags HOT 2
- No release major/minor branches available for reference. HOT 2
- Move tests and benchmarks to GitHub Actions
- Accessibility => lit-element + Google Chrome Screen Reader Extension => UI Design is Changed HOT 3
- Property does not re-render when fired from custom event HOT 4
- Add a way to know when rendering of an element, and ALL its sub-elements, is finished HOT 3
- Litelement Boolean property returns undefined in when not set HOT 6
- Forward Compatibility With Lit 2 misleading HOT 2
- Starter projects missing in getting started documentation HOT 1
- How do you get the actual click event target? HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from lit-element.