Declarative Shadow DOM


#2

Can you explain why these server-rendering solutions are able to insert <shadowroot> elements but are not able to insert <script>document.querySelector("...").attachShadow().appendChild(document.querySelector("..."));</script>?


#3

CSP often limits inline scripting usage. Or if scripting is disabled entirely by the UA.

It seems like there are other HTML parsing performance benefits for a browser to render markup as its coming in without having to context switch between hundreds of blocking inline <script> tags on the page.


#4

CSP can be configured to allow select (known ahead of time) inline scripts. We should not be adding new elements to work around CSP.

I don’t think making performance conjectures without data is very helpful here.


#5

You never know how exactly scripts are blocked by a script blocker.


#6

Yes, just like you never know what elements are blocked by a script blocker either. If you introduce nonstandard ways of messing with your page’s contents into the mix, all bets are off. Inventing new elements to get around such blockers is not likely to work for long.


#7

It’s not about working around script blockers, it’s about making those able to do their job without inevitably and unreasonably breaking other parts of document that can be expressed via nonscript means. Script blockers don’t block CSS or HTML since this is not what those are intended for, not because those couldn’t technically.

Even if a standardized Shadow-DOM-related subset of JS would be defined by a spec, most script blockers would most likely keep blocking JS completely, including (and regardless of) the subset formally called “safe” in the spec.


#8

I created a gist that does a performance comparison between using the inline script technique that @domenic suggested vs. normal inlined HTML: https://gist.github.com/matthewp/f68d892ceb54ccc44f42691b41638f12

This creates 1000 divs, and in the shadow DOM case it moves the HTML from a <template> into the div’s shadow root using an inline script.

On my local machine I get about 4ms load time in the inlined HTML version and ~200ms in the shadow DOM version in Chrome Canary. Safari 10.1 does a bit better at ~4ms for the inlined and ~50ms for the shadow DOM version.

I draw no conclusions from this, just presenting the data. A <shadow-root> element might have a similar cost.

Happy to improve the benchmark if there are any suggestions! I tried using parentNode/previousElementSibling instead of getElementById but that didn’t change the numbers at all.


#9

The other use case is for frameworks that have declarative templating, e.g. GlimmerJS. Being able to define your shadow dom in the template is powerful and clear at the same time.


#10

It isn’t necessarily about server-side rendering. It could be about language. Someone can write the shadow-root element in HTML similar to how they can write a new block scope in JavaScript in order to introduce a new scope for variables. By writing a <shadow-root> element then placing style and elements inside, a scope can be created for CSS isolation when that is all that is desired (for example) without needing JavaScript. It can be a sort of convenience as far as writing HTML goes, which is a much different use case compared to using shadow dom to define component internals. Plus, having JS-only API means that some CSS features are disabled if JS is disabled (if we can’t create shadow roots with JS, then we can’t have CSS scope).


#11

Also writing that in server code is much uglier too.


#12

Not completely to grips with the use-case here. You don’t use Javascript at all? Deferred till condition X? Just want to speed up query selectors?

Anyway do <SLOT> elements not help?


#13

I think it is a huge benefit to be able to write CSS isolation in HTML without requiring JS. This doesn’t sound right: if JS is turned off, then we loose the feature of CSS isolation. If it was supported in HTML, then jS can be turned off, and CSS isolation can still work. That’s a pretty worth reason I think.

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
<body>

That is plain and simple, and I think JavaScript should not be required to achieve this.

EDIT: but things may get tricky, for exampe someone might be inclined to write multiple shadow-roots to create multiple areas with scoped styling (note that multiple shadow roots are not allowed):

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  <shadow-root>
    <style>
      /* other scoped style without JavaScript */
    </style>
    <div> Some other content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
<body>

In this case the solution may be to use wrappers around the shadow-roots:

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>

  <div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  </div>

  <div>
  <shadow-root>
    <style>
      /* other scoped style without JavaScript */
    </style>
    <div> Some other content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  </div>
<body>

#14

Hi Joseph,

Sorry to be slow on the uptake but can’t this simply be achieved with another CSS class? Or perhaps just prefixing your selectors with " #myHost

"?

Perhaps if you explained what was to be gained by this approach? Especially, given the semi-isolation described here: -

Anyway I’m banned from WICG so good-luck.

Cheers Richard


#15

Yeah, those are the currently-existing techniques, which Less, Sass, etc, use when they compile to CSS. But it isn’t entirely full proof: you can’t guarantee that someone won’t in the future also write #myHost or some other colliding class name (in a big project for example). Also it takes effort to write collision-free names.

CSS scoping is programmatically encapsulated, unlike just following conventions. With CSS scope, it means someone can write a class button without having to write component-subcomponent-button or something else more complicated to just to avoid collision…

Because with scoping the collision is avoided by the program, not by writing conventions. For example, look how easy it is to style scoped button without having to write extra stuff:

<shadow-dom>
  <style>
    button {
      color: red
    }
  </style>
  <button></button>
</shadow-dom>

#16

I tried to aggregate discussions from here and https://github.com/whatwg/dom/issues/510, to a slightly more structured proposal at https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Declarative-Shadow-DOM.md I would really appreciate any feedback.


#17

I like the proposal, but maybe it needs a slightly special syntax to disambiguate from normal DOM elements. For example:

    <#shadowroot mode="open">
        <h2>Shadow Content</h2>
        <slot></slot>
    </#shadowroot>

with the leading # so that it makes the distinction clear. But, then again, <#foo> isn’t a backwards-compatible format, and in older browsers the <#foo> will appear as text in the rendering. But even with back0compat syntax, it will lead to content not rendered as expected in older browsers. So it’s tricky!


#18

Thanks for the review!

I’m afraid that so different syntax would be problematic for parser changes, polyfilling, etc. It does not fit the current definition of an element name https://w3c.github.io/html-reference/syntax.html#tag-name. However, from a users perspective, I don’t see much disadvantages.

We already have not-“normal DOM elements”, the elements with different parsing contexts, like: template, script, tr. So far, to distinct the special behavior, we didn’t use any mean other than the tag name.

At the end it will still be part of the DOM, why should we disambiguate DOM from DOM? Is that this “disappearing” behavior that makes you think it may be that special and confusing?


#19

Well, what happens if we do

document.querySelector('shadowroot').remove()

Does that remove the shadowroot even though that is not supposed to be possible?

Does the parser remove the <shadowroot> element during parsing, so that the result is a real shadow root but the DOM no longer contains a <shadowroot> element? If not, then is it just an inert element after parsing?

If we add children to <shadowroot>, do they somehow get distributed into the content of the actual root?

Is a reference to a <shadowroot> the same as an actual shadow root? If not, then how can elements exist both as content of a <shadowroot> and content of the actual real shadow root? Do we introduce another distribution mechanism just for distributing elements from <shadowroot> to actual shadow root?

What happens if I move the <shadowroot> to another parent?

If it is a normal element, does that mean that <shadowroot> can be distributed into a… shadow root?

What happens if we add more than one <shadowroot> element to the parent element?

<shadowroot> is so different than all other elements in the sense that shadow roots are meant for composing HTMLElements/DOM, and currently they aren’t HTML elements. Making them HTML elements has many implications (which may not be desired).

This is why I thought maybe an alternate syntax would let us sidestep all implications, and specify the behavior from scratch. For example, maybe the <#shadowroot> element is ephemeral, it only exists in your markup, and after being parsed it will not be in the DOM, but the parent element will have a shadow root added to it. That way, if it isn’t in the DOM, it isn’t queryable, isn’t removable, isn’t appendable, can’t have children because it doesn’t exist, can’t be distributed because it doesn’t exist, etc.

But then, what if one changes innerHTML of an element, introducing a new <#shadowroot> element? Maybe nothing should happen. It just disappears, and the previous root shadow root that was created from the previous parsing remains.


For me, the main use case for <shadowroot> is in server-side rendering, so that the client can know how to re-create the composition of trees from the HTML received from the server.


#20

I have been experimenting with pre-rendered shadow DOM (https://experiments-pre-rendered-shadow-dom-v2–jamesernator.repl.co/) and it definitely adds improved load speed and avoids an awful FOUC.

However from investigating it I personally don’t think <shadow></shadow> is a good solution. In a lot of cases we’ll have similar objects that share the same shape that we only want to change small amounts of the content.

The solution I wound up using was to simply add an attribute that gives an id number for a template to use.

So for example if we had lots of identical components differing only by a small bit of data in the current proposal:

<blog-post-listing>
  <shadow>
    <link rel="stylesheet" href="./blog-post-listing.css">
    <span class="blog post name">Title 1</span>
    <div class="description">
      Some summary 1
    </div>
  </shadow>
</blog-post-listing>
<blog-post-listing>
  <shadow>
    <link rel="stylesheet" href="./blog-post-listing.css">
    <span class="blog post name">Title 2</span>
    <div class="description">
      Some summary 2
    </div>
  </shadow>
</blog-post-listing>
<blog-post-listing>
  <shadow>
    <link rel="stylesheet" href="./blog-post-listing.css">
    <span class="blog post name">Title 3</span>
    <div class="description">
      Some summary 3
    </div>
  </shadow>
</blog-post-listing>
<blog-post-listing>
  <shadow>
    <link rel="stylesheet" href="./blog-post-listing.css">
    <span class="blog post name">Title 4</span>
    <div class="description">
      Some summary 4
    </div>
  </shadow>
</blog-post-listing>
<blog-post-listing>
  <shadow>
    <link rel="stylesheet" href="./blog-post-listing.css">
    <span class="blog post name">Title 5</span>
    <div class="description">
      Some summary 5
    </div>
  </shadow>
</blog-post-listing>

We could just just have a single template with the data that we want to prerender into the template e.g.:

<template id="blog-post-listing-template">
  <link rel="stylesheet" href="./blog-post-listing.css">
  <span class="blog post name">{{title}}</span>
  <div class="description">
    {{summary}}
  </div>
</template>

<blog-post-listing
  prerender-template="blog-post-listing-template"
  prerender-data="{\"title\":\"Title 1" \"description\": \"Some summary 1\"}"
></blog-post-listing>
<blog-post-listing
  prerender-template="blog-post-listing-template"
  prerender-data="{\"title\":\"Title 2" \"description\": \"Some summary 2\"}"
></blog-post-listing>
<blog-post-listing
  prerender-template="blog-post-listing-template"
  prerender-data="{\"title\":\"Title 3" \"description\": \"Some summary 3\"}"
></blog-post-listing>
<blog-post-listing
  prerender-template="blog-post-listing-template"
  prerender-data="{\"title\":\"Title 4" \"description\": \"Some summary 4\"}"
></blog-post-listing>

This is dependant on the template instantiation proposal however.


#21

Thinking about it some more it, the current experiment I wrote requires storing an arbitrary property on the element so that they can check for it before using attachShadow.

It would be good if a builtin solution were to simply return the pre-rendered shadow root from attachShadow rather than failing as a shadow root is already attached e.g. given this code:

<template id="blah">
  <div>Some content</div>
</template>

<my-element
  prerender-template-id="blah"
  prerender-shadow-mode="closed"
></my-element>

when calling attachShadow we would just return the pre-rendered shadow root:

class MyElement extends HTMLElement {
  #shadowRoot
  constructor() {
    super()
    // This will already be populated with
    // <div>Some content</div>
    this.#shadowRoot = this.attachShadow({ mode: 'closed' })
  }
}