-
Notifications
You must be signed in to change notification settings - Fork 294
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
Declarative Shadow DOM #831
Comments
One interesting question about the proposal is how does it affect all the other weird html parsing things like table fixups and what not. If I have (Other than that kind of stuff, I agree that this is something worth addressing) Also, it seems a bit weird/unfortunate that you are forced to have a template for every shadow host, when I assume the common thing for a given component is to always have the same shadow root... But I don't have a great solution for that off-hand, maybe you should be able to reference a template from the host by ID? Something else? |
Well I guess the parsing insertion may or may not be much of an issue, as you don't have to insert the template contents in the parent, but instead goes directly into the shadowroot... |
There's still a risk here in that a previous harmless template can now be used for script injection if you can do some attribute injection. (Also, browsers continue to have security issues around It'd be good to complete the algorithm so it deals with the element already having a shadow root and it details what "moving" means. |
I wonder if converting Firefox UI code from XBL to more web component-y has brought up any ideas related to this issue. |
I don't think this would make sense for the Firefox frontend. That said, it seems like we aren't the target audience because of https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md:
We don't have to support no-js environments or Server Side Rendering at all. We also don't currently use Shadow DOM outside of Custom Elements anywhere (it's possible we may want to do it sometime, but because we always have JS we'd probably just make it a Custom Element in that case). In addition there are some things from my reading of the proposal that would make it inconvenient for our use cases (specifically with shared widgets), and might also be inconvenient for sites that do want to support SSR:
FWIW: what we do now is more-or-less:
I have sort of wished in the past we could have a more declarative way to define the markup in (x)html files alongside scripts and styles, so it's nice to see this being explored though. @mfreed7 I'd be interested to hear more about this point:
Specifically if there's a reason that SSR tools couldn't/shouldn't be taught to parse a syntax like that to work even in an environment without JS? So you could declare a custom element with only a template, then have a tool end up creating the same output they would with Declarative Shadow DOM. I'm not familiar with the tooling here, so it's possible I'm missing something. |
I mean, it seems to me what we really need is a declarative way to instantiate at template at which case we could potentially provide some directive to that node that renders the template inside a shadow DOM. I love this idea, but adding a |
Would there be a way to share the the shadow template and/or style across multiple instances of a host-element? |
Hi, this should not be standard, since it is rather a hydration technique that can be easily adopted by any library, please analyze the example attached in the documentation, it should generate the polyfill in connectedCallback and not in the constructor. I think the standards associated with the web should not cover the SSR, this is the work of libraries. Summary: as a technique it is excellent and I think I'll adopt it atomico , but not this should be a standard |
@emilio @calebdwilliams and @vikerman I think standardized templating, along the lines of Template Instantiation is a very valuable feature to explore, but it's a bit different than declarative shadow roots. I look at this proposal mainly as a way to re-establish the ability to meaningfully serialize a DOM tree (now that include shadow roots). There are a few applications this addresses, including SSR, and some of them overlap with declarative custom elements and templating, but not all of them. I think it's better to keep these proposals separate for now. |
For me, I think the "slot hoisting" is weird and/or counterintuitive. I also wonder what happens when I need 10x Not to bikeshed too much, but I think lots of people would prefer auto-instantiating Custom Elements from a single template like...
Then from a SSR authoring standpoint, I could |
@davatron5000 what do you mean by "slot hoisting"? Instantiating declarative custom elements is definitely good future work, but it requires a lot more than what you've sketched to be practically useful. The key thing to understand here is that this lets us serialize instances of shadow roots. Most shadow roots of the same class of component are not identical - they have been produced by some kind of templating layer or DOM manipulation that makes each instance unique. So you usually can't simply refer to a template and stamp that out, you would need to provide it data and enable the template to specify the transform from data to actual DOM. Again, great future work and where the template instantiation and declarative custom elements ideas/proposals are pointing, but quite a bit different from this. Even once we have declarative custom elements, it's quite likely that this proposal will be needed as is, since for serialization purposes we'll still need to describe the actual shadow root state of the particular instances in cases where we don't have the data that produced the DOM yet, or in the numerous cases where a shadow root wasn't produced by a declarative custom element. |
I'm still somewhat on the fence about whether or not we should be able to refer to a pre-existing template so we can avoid repetition. One of the base use-cases - getting "scoped styles" for a section of your page using the shadow DOM composition boundary without needing JS - is satisfied without that. If you're hand-authoring a no-JS page, you have to repeat all your structure for each element; this doesn't change anything about that, it just adds a little bit more text to each repetition to establish the boundary. Another satisfied base use-case is shipping server-rendered HTML using shadows that'll be hydrated into full JS-driven custom elements later. You can write custom elements (without having to repeat the contents each time) on your server, then serialize them out into this form; compression should take care of most of the cost of repetition, and post-parsing DOM sizes are comparable. The use-case not satisfied is wanting to get the less-structural-repetition benefit of a custom element without requiring JS if all you're doing is filling in some DOM and nothing else. That's a reasonable case, I think! But also a less important case than the two I mentioned above. I think if we go without that for now, we're not blocking ourselves from having such a solution later, such as having a |
I was asked to offer feedback on this proposal in my capacity as a framework author, to help ensure that these additions are relevant to those of us not currently using web components. Let me first say that I'm glad the no-JS use case is being taken seriously — the lack of SSR support (various WC framework hacks notwithstanding) has made web components a non-starter for many of us. I have a few questions and observations. Most importantly, I agree with @annevk that it's essential to clarify what happens when declarative and programmatic shadow roots collide. Is Is the expectation that custom element authors would do this sort of thing? class Clock extends HTMLElement {
constructor() {
super();
if (this.shadowRoot) {
// declarative shadow root exists
this.hours = this.shadowRoot.querySelector('.hours');
this.minutes = this.shadowRoot.querySelector('.minutes');
this.seconds = this.shadowRoot.querySelector('.seconds');
} else {
// declarative shadow root doesn't exist
this.attachShadow({ mode: 'open', serializable: true });
this.hours = document.createElement('span');
this.hours.className = 'hours';
this.minutes = document.createElement('span');
this.minutes.className = 'minutes';
this.seconds = document.createElement('span');
this.seconds.className = 'seconds';
this.shadowRoot.append(
this.hours,
document.createTextNode(' : '),
this.minutes,
document.createTextNode(' : '),
this.seconds
);
}
}
connectedCallback() {
this.update();
this.interval = setInterval(() => {
this.update();
}, 1000);
}
disconnectedCallback() {
clearInterval(this.interval);
}
update() {
const d = new Date();
this.hours.textContent = pad(d.getHours());
this.minutes.textContent = pad(d.getMinutes());
this.seconds.textContent = pad(d.getSeconds());
}
} Importantly, this doesn't handle the case where the declarative shadow DOM is malformed for whatever reason (a different version of the custom element, for example), so in reality the code would likely be more complex. Furthermore, in the (probably fairly common) case that the shadow root is populated via In other words, it's hard to see how we can introduce declarative shadow DOM without introducing significant new complexities for custom element authors. Duplication of content and stylesAs @davatron5000 and others have noted, it looks as though this proposal results in duplication of styles and content. But I don't think it's practical to share a <p>The time in London is
<world-clock timezone="GMT">
<template shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">12</span> :
<span class="minutes">34</span> :
<span class="seconds">56</span>
</template>
</world-clock>
</p>
<p>The time in New York is
<world-clock timezone="EDT">
<template shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">07</span> :
<span class="minutes">34</span> :
<span class="seconds">56</span>
</template>
</world-clock>
</p>
<p>The time in Hong Kong is
<world-clock timezone="HKT">
<template shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">20</span> :
<span class="minutes">34</span> :
<span class="seconds">56</span>
</template>
</world-clock>
</p> By contrast, here's what you might get with a non-web-component framework: <style>
span.svelte-xyz123 {
font-variant: tabular-nums;
}
.seconds.svelte-xyz123{
font-size: 0.8em;
}
</style>
<p>The time in London is
<span class="svelte-xyz123">18</span> :
<span class="svelte-xyz123">59</span> :
<span class="seconds svelte-xyz123">36</span>
</p>
<p>The time in New York is
<span class="svelte-xyz123">13</span> :
<span class="svelte-xyz123">59</span> :
<span class="seconds svelte-xyz123">36</span>
</p>
<p>The time in Hong Kong is
<span class="svelte-xyz123">02</span> :
<span class="svelte-xyz123">59</span> :
<span class="seconds svelte-xyz123">36</span>
</p> Clearly, the non-custom-element version results in many fewer bytes, and a less complex (i.e. more memory-efficient) DOM. SerializationI don't think it makes sense for components to declare their shadow roots to be serializable. For one thing, it's unfortunate if But more to the point, it's not the component's job to determine that. Whether or not shadow DOM should be serialized is a decision that should be taken at the point of serialization, i.e. by the component consumer. In other words, something like this (after a round of bikeshedding) would make a lot more sense to me: const html = element.innerHTMLWithShadowDOM; Intended use caseI expect most people are in agreement about this, but I haven't seen it explicitly addressed, so I'll note it here: we're probably not expecting people to write declarative shadow DOM by hand. That would defeat much of the point of web components, which is to encapsulate the component's behaviour in such a way that HTML authors don't need to worry about it, and would vastly increase the likelihood of errors. Which is to say that this is a capability directed at frameworks. But this means that those frameworks will, in order to take advantage of this for server-side rendering, need to implement a declarative-shadow-DOM-aware DOM implementation that runs in Node.js (or wherever). Such things add non-trivial complexity, and even performance overhead, to something that is today accomplished using straightforward string concatenation. In summary, while I welcome this discussion, I fear that declarative shadow DOM only gets us part way to what we can already do without web components, but at the cost of additional complexity. |
@justinfagnani I'm probably not describing it well, but the Light DOM getting consumed by a sibling element (getting "hoisted up" into the slot) was somewhat confusing. I know the sibling If this is a stepping stone towards something great, then I can support that but Rich's summary is pretty spot on for me (except that I want to be able to hand-author stuff). |
Thanks to everyone for the great comments here. There seem to be a few themes - I'll try to summarize and respond:
Thanks again for the great points raised here! |
Personally I think it would be preferable that closed shadow roots can still be SSR-ed, I understand that class MyComponent extends HTMLElement {
#shadowRoot;
constructor() {
if (this.shadowRoot) {
this.#shadowRoot = this.shadowRoot;
this.#shadowRoot.close(); // Changes the shadow root from open to closed
} else {
this.#shadowRoot = this.attachShadow({ mode: 'closed' });
// Initialize shadow root ...
}
// ....
}
}
One suggestion I had on the original discourse thread was to use template instantiation so that data can be injected into a single template with even less duplication than current SSR approaches as they don't even need to duplicate the rendered DOM. This suggestion would address @Rich-Harris concerns about duplication but depends on a very early proposal for template instantiation. Although as a plus the approach could still be used even with duplication because if template instantiation were added later it could be added on without changing the elements significantly e.g.: <template id="world-clock-template" shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">{{hours}}</span> :
<span class="minutes">{{minutes}}</span> :
<span class="seconds">{{seconds}}</span>
</template>
<template id="prerendered-2" shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">07</span> :
<span class="minutes">34</span> :
<span class="seconds">56</span>
</template>
<template id="prerendered-3" shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">20</span> :
<span class="minutes">34</span> :
<span class="seconds">56</span>
</template>
<p>The time in London is
<world-clock shadowroot="#prerendered-1" timezone="GMT"></world-clock>
</p>
<p>The time in New York is
<world-clock shadowroot="#prerendered-2" timezone="EDT"></world-clock>
</p>
<p>The time in Hong Kong is
<world-clock shadowroot="#prerendered-3" timezone="HKT"></world-clock>
</p> However with template instantiation this could just become: <!-- With template instantiation -->
<template id="world-clock-template" shadowroot="open">
<style>
span {
font-variant: tabular-nums;
}
.seconds {
font-size: 0.8em;
}
</style>
<span class="hours">{{hours}}</span> :
<span class="minutes">{{minutes}}</span> :
<span class="seconds">{{seconds}}</span>
</template>
<p>The time in London is
<world-clock
shadowroot="#world-clock-template"
shadowrootdata='{ "hours": 12, "minutes": 34, "seconds": 56 }'
timezone="GMT"
></world-clock>
</p>
<p>The time in New York is
<world-clock
shadowroot="#world-clock-template"
shadowrootdata='{ "hours": 7, "minutes": 34, "seconds": 56 }'
timezone="EDT"
></world-clock>
</p>
<p>The time in Hong Kong is
<world-clock
shadowroot="#world-clock-template"
shadowrootdata='{ "hours": 20, "minutes": 34, "seconds": 56 }'
timezone="HKT"
></world-clock>
</p> |
Can you elaborate? All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware. |
From @sebmarkbage:
|
Hmm - can you elaborate? I do know that duplicate |
How can it know they are dupes without parsing them? |
Hashmap from text to parsed stylesheet representation effectively.
They do: You don't get a pointer-identical |
(That's what happens with |
@emilio thanks for the code links. With that in mind, it would seem that inline |
What text should those stylesheets have? The serialized representation of their CSS rules? Or the original text that was passed to Because for inline style you don't get serialized back the result of CSSOM mutations. It'd be weird if |
I think it's important to be able to preserve the semantics of adopted stylesheets after round-tripping through shadow DOM serialization. Constructible StyleSheets are a shared CSSStyleSheet and that's observable as well. @mfreed7 has seen this, but I've been vaguely proposing the idea of a new <html>
<style type="adopted-css" id="style-one">
/* ... */
</style>
<style type="adopted-css" id="style-two">
/* ... */
</style>
<div>
<template shadowroot="open" adopted-styles="style-one style-two">
<!-- ... -->
</template>
</div>
</html> Having a type other than The ids in How this plays with Note that these |
Initial thought is that this is great for SSR/SSG (already noted as primary motivation) and also acknowledging this isn't necessarily meant to be hand-coded which again aligns with SSR. In regards to Ionic, I think this would be a big win as it'd help us reduce/remove JS that converts a component's flat dom tree styled with scoped css, into a shadow root with encapsulated css once the JS kicks in. Concerns:
Overall I'm excited to see this discussion and absolutely can see how it'll benefit Ionic's use-case. |
Is this the right place to discuss the features of the declarative shadow DOM and how they might be implemented into libraries. Specifically:
I recently modified a rendering library I've worked on for some years to use the declarative shadow DOM. I discovered some challenges and opportunities when it comes to the spec when you consider things like component driven design, design systems, and modern web app development practices in general. If not shoot me a link to a place that might be more appropriate? |
@rniwa @mfreed7 WICG/sanitizer-api#193 made me realize that we currently don't restrict declarative shadow roots to elements that can have |
Sorry - can you clarify? We currently restrict declarative shadow roots to only those things that support shadow roots, which is custom elements but also plain elements. Are you suggesting adding |
@mfreed7 I'm saying that we restrict it to custom elements as only those have an internal |
Oh! You want DSD to be different from what you can do with |
@mfreed7 how can you do a closed declarative shadow root with a (In retrospect it was probably a mistake to allow them on anything but custom elements (in userland, that is). |
<div>
<template shadowrootmode=closed> I'm closed! </template>
</div> Maybe I'm missing what you're asking.
Hmm, I suppose I'd be interested in hearing more about why. The biggest complaint I hear about web components is that too many behaviors are tied together into one package. E.g. shadow dom encapsulates everything, and you can't opt out of parts of that. Further tying things together seems to run counter to that developer feedback. |
How can you manipulate it from script? |
From a developer perspective, I'd rather you remove the ability to have |
<custom-element>
<template shadowrootmode=closed>blah</template>
</custom-element>
<script>
customElements.define("custom-element", class CustomElement extends HTMLElement {
constructor() {
super();
this.internals = this.attachInternals();
console.log(this.internals.shadowRoot);
}
});
</script> |
Yes, the WebKit blog has a similar example of manipulating closed Declarative Shadow DOM from script. Their example also uses a custom element. https://webkit.org/blog/13851/declarative-shadow-dom/ <some-component>
<template shadowrootmode="closed">hello, world.</template>
</some-component> customElements.define('some-component', class SomeComponent extends HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
// This will log "hello, world."
console.log(this.#internals.shadowRoot.textContent.trim());
}
}); It's true that you can't do this if the element is a But I think your mistake was to think that if you can't manipulate a Shadow DOM created with But closed DSD is useful in its own right, even if you never manipulate it via script. The fact that the closed DSD can't be manipulated via script is a good thing; it's arguably the whole point of closed DSD, to truly lock down a shadow root. Since |
You can still reach the div’s closed shadow root if the shadow root’s content includes a custom element (or even a builtin with an appropriate <div>
<template shadowroot=closed>
<x-x></x-x>
</template>
</div>
<script>
customElements.define("x-x", class extends HTMLElement {
connectedCallback() {
console.log(this.parentNode);
}
});
</script> |
<div>
<template shadowrootmode=closed>
<my-fancy-wrapper>
Some fancy wrapper that decorates the slotted content.
This custom element also has access to the shadow root.
<slot></slot>
</my-fancy-wrapper>
</template>
Some light DOM content that script can see and manipulate
</div> I'm sure there are many creative examples.
@annevk I think this last question is the best one - how do developers benefit from arbitrarily restricting this functionality? And how do they benefit from having a lack of parity between imperative and declarative shadow dom? It means you can build things with Javascript that you can't use with server side rendering, at a minimum. |
I don't think that's the relevant question. We have a design goal of parity and this doesn't meet that design goal. That's the high-level bit for me. |
The parity goal is about adding functionality, not removing it. It is a misunderstanding of the goal of parity to remove useful features just to be "fair" to both scripting and non-scripting environments. If you care about this parity bit, then I suggest raising a separate issue to add parity functionality to imperative shadow DOM, rather than proposing removing functionality out of misapplied consistency. |
To me, developers, and their end users, are the reason we're working on this platform. The point of having a design goal for anything should be that it benefits developers and/or users. If it instead limits developers, and we do it just to make the spec more pure, then I think this link is relevant. It sounds like perhaps the "parity" design goal needs to be re-evaluated? The |
See update here: whatwg/html#5465 (comment) |
I think declarative custom components is a good step forward, but maybe it can be improved upon... to me its really not ideal to to repeat the template HTML inards for every instance where it is used, it makes consuming the components much more complicated than simply writing their name. The joy of custom web components is their use. I have read the rational, of why it is this way and can appreciate it. Just a thought, why can't we include the template in the first instance at the top of the page, and every instance thereafter that does not detail a template, uses the previous component that does have a template. E.g. <host-element>
<template shadowroot="open">
<style>shadow styles</style>
<h2>Shadow Content</h2>
<slot></slot>
</template>
<h2>Light content 1</h2>
</host-element>
<!-- No template defined use previously defined template -->
<host-element>
<h2>Light content 2</h2>
</host-element> However the above idea is not great because it's hard to keep track of which is the first component that has the template. A method that I think will be less error prone is to have a blueprint component at the top of the page, or in the header. <host-element definition>
<template shadowroot="open">
<style>shadow styles</style>
<h2>Shadow Content</h2>
<slot></slot>
</template>
</host-element>
<script>
...
</script>
<p>Now we actually use the component</p>
<host-element>
<h2>Light content Instance</h2>
</host-element> You may say why not just use imperative components, which is what I do. The downside of this is one has to jump through so many hoops trying to not create a new style sheet (more memory usage) for each custom component instance. And trying to share external stylesheets is not trivial. External stylesheets are re-downloaded for every instance. The hoop is jumpable, using constructable stylesheets with the polyfilling that goes with along it. I don't want to hi jack this thread so this was moved to: https://github.com/whatwg/meta/issues/298<!-- Pie in the sky dreaming mode activated -->
<!-- my-foo.html is served with a cached header -->
<tagdef name="my-foo" src="static/components/my-foo.html">
<!-- static/components/my-foo.html -->
<style>shadow styles</style>
<link rel="stylesheet" href="static/my-forms.css">
<h2>Shadow Content</h2>
<slot></slot>
<script>
...
</script> Its hard to argue how that is not simply to use and understand compared to what we currently have. |
Corresponding HTML PR: whatwg/html#5465. Tests: https://wpt.fyi/results/shadow-dom/declarative. Closes whatwg#831.
Corresponding HTML PR: whatwg/html#5465. Tests: https://wpt.fyi/results/shadow-dom/declarative. Closes whatwg#831. nits Fix up use of init Convert tri-state to boolean Address comment
A longer explanation of this feature can be found at https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md. It was originally discussed in whatwg/dom#831. Corresponding DOM PR: whatwg/dom#892. Tests: shadow-dom/declarative in WPT with web-platform-tests/wpt#42833 being the latest PR as of this commit. Closes #7069.
I would like to re-open the topic of declarative Shadow DOM. This has been discussed in the past, here on WHATWG, in W3C here and here, and in WICG. The last substantive public discussion was at the Tokyo Web Components F2F, where it was resolved not to proceed. I would like to revisit that decision.
I think declarative Shadow DOM is an important feature that is missing from the Web, and is something that we should try to implement. The primary motivating use case for declarative Shadow DOM is Server Side Rendering (SSR), which is practically difficult or impossible to use in combination with Shadow DOM. There are also other compelling use cases such enabling scoped styles without requiring Javascript. The rationale behind the prior decision not to proceed with this feature was largely a) implementation complexity and b) lack of developer need.
To address these points, and to explore the topic further, I've written up an explainer, here:
https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md
I believe this document captures most of the details of the motivation, required features, contentious points, and prior history. But I would love to hear your thoughts and feedback so that this proposal can evolve into something implementable and standardizable. I'm hoping we can use this thread as a discussion forum.
As a quick summary of the proposed syntax, this HTML:
would be parsed into this DOM tree:
The text was updated successfully, but these errors were encountered: