dom icon indicating copy to clipboard operation
dom copied to clipboard

Declarative Shadow DOM

Open mfreed7 opened this issue 4 years ago • 237 comments

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:

<host-element>
    <template shadowroot="open">
        <style>shadow styles</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </template>
    <h2>Light content</h2>
</host-element>

would be parsed into this DOM tree:

<host-element>
  #shadow-root (open)
    <style>shadow styles</style>
    <h2>Shadow Content</h2>
    <slot>
        ↳ <h2> reveal
    </slot>
  <h2>Light content</h2>
</host-element>

mfreed7 avatar Feb 07 '20 23:02 mfreed7

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 <template shadowroot="open"><td>Foo</td></template>, what is the final dom?

(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?

cc @hsivonen @smaug---- @edgarchen

emilio avatar Feb 10 '20 05:02 emilio

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...

emilio avatar Feb 10 '20 05:02 emilio

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 template elements to this day, which isn't reassuring.)

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.

annevk avatar Feb 10 '20 15:02 annevk

I wonder if converting Firefox UI code from XBL to more web component-y has brought up any ideas related to this issue. @bgrins

smaug---- avatar Feb 10 '20 15:02 smaug----

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:

The entire motivation for this feature is no-JS environments

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:

  1. Many of our Custom Elements are used many times throughout a single document, so this would require duplication of the template. For instance we have a couple hundred menuitems and toolbarbuttons in the DOM in browser.xhtml at startup.
  2. Some Custom Elements are used in a lot of documents across the tree or are used inside of other Custom Elements. In these cases the duplication would be spread across multiple files. This would still require duplication even in the "Instead of inline contents, use an idref to an existing template" alternative.
  3. Many Custom Elements get created from JS so AIUI we'd need to programmatically insert the template, or create the same shadow content from JS in another way.

FWIW: what we do now is more-or-less:

  • Add a static string getter for markup in an Custom Element class
  • Pass that into DOMParser.parseFromString to get a DocumentFragment
  • Cache the fragment and do essentially this.shadowRoot.appendChild(document.importNode(fragment, true)) in the constructor or connectedCallback.

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:

Why not wait for, or link this to, declarative custom elements? At first blush, it would seem that these two proposals go together. However, the primary motivating use case for declarative Shadow DOM is SSR and No-JS. Custom element definitions need javascript to function; therefore, this is a different use case/proposal and the two should not be tied together

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.

bgrins avatar Feb 11 '20 17:02 bgrins

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 shadowroot attribute to an HTMLTemplateElement completely changes the semantics of the element. We should have something like <target template="templateID"> that would clone and insert the template’s contents declaratively. Of course that introduces a scoping problem that would need to be figured out since ids are scoped to the shadow root.

calebdwilliams avatar Feb 11 '20 19:02 calebdwilliams

Would there be a way to share the the shadow template and/or style across multiple instances of a host-element?

vikerman avatar Feb 11 '20 19:02 vikerman

See also https://github.com/ebidel/declarative-web-components by @ebidel.

FluorescentHallucinogen avatar Feb 11 '20 20:02 FluorescentHallucinogen

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

UpperCod avatar Feb 11 '20 22:02 UpperCod

@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.

justinfagnani avatar Feb 12 '20 05:02 justinfagnani

For me, I think the "slot hoisting" is weird and/or counterintuitive. I also wonder what happens when I need 10x <host-elements> on the page. Do I provide 10x styles and and templates and slots?

Not to bikeshed too much, but I think lots of people would prefer auto-instantiating Custom Elements from a single template like...

<template customelement="host-element" shadowroot="open">
  <style>shadow styles</style>
  <h2>Shadow Content</h2>
  <slot></slot>
</template>

<host-element>
  <h2>Light Content</h2>
</host-element>

Then from a SSR authoring standpoint, I could <?php include('host-element.php') ?> once at the top of my document and author freely.

davatron5000 avatar Feb 12 '20 15:02 davatron5000

@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.

justinfagnani avatar Feb 12 '20 17:02 justinfagnani

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 <template use=#id></template> that lets it refer to templates already in the DOM? And avoiding that for now lets us skip some more complex scenarios, making the MVP here easier to define.

tabatkins avatar Feb 12 '20 18:02 tabatkins

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 this.attachShadow(...) an error if there's already a declarative shadow root? Because that would likely cause all sorts of problems.

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 innerHTML, we would find ourselves nuking the existing shadow DOM rather than gracefully hydrating it, which seems like it could have negative consequences (performance, but also blowing away state in <input> elements and so on).

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 styles

As @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 <template> between separate instances because the shadow DOM will often differ. Imagine the clock example above also accounted for timezones, and came with styles — the serialized result of using it might look like this:

<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.

Serialization

I don't think it makes sense for components to declare their shadow roots to be serializable. For one thing, it's unfortunate if serializable: true, which is presumably the intended default, is something you have to opt in to, though the web compat argument is obviously persuasive.

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 case

I 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.

Rich-Harris avatar Feb 12 '20 19:02 Rich-Harris

@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 <template> is being converted into Shadow DOM and then the Light DOM is being revealed, but it wasn't very intuitive.

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).

davatron5000 avatar Feb 12 '20 22:02 davatron5000

Thanks to everyone for the great comments here. There seem to be a few themes - I'll try to summarize and respond:

  1. What should the custom element definition look like?

    The explainer does have a section for this, but I really like the example provided by @Rich-Harris in this comment. That is exactly what I was envisioning - a small if (this.shadowRoot) block that just hooks things up if the shadowRoot already exists, and the actual construction code if not. That code works on both client and server (it is isomorphic), and the added code is minimal. @Rich-Harris asked what happens if the declarative content is malformed - if that is a possibility (due to versioning, etc.), then no matter what the declarative solution, you'll need to do extra work. And in the case where you can assume that an existing #shadowroot means your content is "good to go", you'll get a performance win from not having to blow away the existing content and re-create it.

  2. Wouldn't it be better to re-use a single <template> rather than duplicating it for each shadow root?

    Several people already responded to this, but I wanted to point out this section of the explainer that discusses this point at length. The important three points in my mind are: a. As @Rich-Harris and @justinfagnani point out, it is important to remember that we're serializing instances of elements, which likely differ from one another slightly in terms of their DOM content. b. In terms of data/overhead, gzip nicely fixes most of the ills of almost-perfectly-repeated content. Aside from the potentially-shared styles (see the point below), you'll get another copy of the DOM no matter what you do here. So the "overhead" benefits of sharing a single <template> for this seem rather limited. c. Re-using a <template> like this requires a solution to the previously-unsolved "idref" issue. See here for the discussion around ARIA labelled-by. The problem is: how do you deal with nested shadow roots? The idref would then need to cross shadow bounds, potentially in both directions. We don't have a way to allow that, yet.

  3. How to handle styles?

    This is definitely an open question. I'm hoping we can come up with a declarative Shadow DOM solution that isn't tied to a particular solution to the styling problem. To do that, I have proposed just using inline <style>s within each shadow root. As mentioned, this would result in a) more bytes on the wire, and b) more DOM memory used. Of those, I'm least concerned with a). For the example HTML provided in this comment, when gzipped, the inline <style> example takes 290 bytes, while the "shared stylesheet" example takes 223 bytes. Yes, that's 25% more, but not the factor of two that it would appear from the raw HTML. I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate <style> elements and condense them into a single CSSStyleSheet that gets added to adoptedStylesheets? That might be crazy. I do agree that styles need a solution. I don't think it needs to be solved for this declarative Shadow DOM solution to be useful as-is.

  4. Serialization and the "serializable:true" option.

    I love the @Rich-Harris suggestion to add another API (element.innerHTMLWithShadowDOM) that serializes all shadow roots by default. That avoids the need to retrofit existing components with serializable:true, and as you pointed out, this isn't the component's decision to make anyway. I'd be in favor of changing the explainer to match this suggestion.

  5. What about existing html parser behaviors, e.g. table fixup: <template shadowroot=open><td>Foo</td></template>

    The advantage of this proposal is that nearly all of the existing "standard" <template> behavior still applies. For example, for this specific example, the "in template" insertion mode rules apply. I don't think there is any additional ambiguity created by this proposal.

  6. Is script injection a problem? Can't a previously-harmless <template> be made active by attribute injection?

    No, at least according to the existing proposal. This would be a "parser-only" feature, and adding the shadowroot attribute to an existing <template> would have no effect. As pointed out by @hayatoito, you could still imperatively build a <template>, add a shadowroot attribute, and then do element.innerHTML = element.innerHTML. The innerHTML assignment would see the full <template shadowroot> and would attach a shadow as it is parsed. But that doesn't seem like a security risk, since you're blowing away the entire innerHTML in that case anyway. Please correct me if I'm wrong.

  7. It is weird to have a <template shadowroot> turn into a #shadowroot and then have previously-sibling elements get slotted into the #shadowroot.

    Yes, this is definitely different and will take getting used to, no question. But this statement seems like it would apply to any declarative Shadow DOM solution. No matter the semantics, some element will become, or create, a #shadowroot which will then start "pulling in" sibling content into <slot>s.

  8. What happens if there is already a shadow root?

    For compatibility and alignment, this needs to be an error. I mentioned several such scenarios in the explainer, here. Basically, you can (still) only attach a shadow root once, and any subsequent attempts (either declarative or imperative) will result in an error.

  9. Adding shadowroot to <template> changes the semantics of the element, which is weird.

    Yes, it is, I agree. This is discussed here in the explainer. The one point that seems to kill the idea of creating a new element (e.g. <shadowroot>) is the backwards-compat problem. Until all browsers understand the new element, enclosed <style> and <script> elements will be exposed to the parent page, with potentially bad consequences.

Thanks again for the great points raised here!

mfreed7 avatar Feb 12 '20 22:02 mfreed7

That is exactly what I was envisioning - a small if (this.shadowRoot) block that just hooks things up if the shadowRoot already exists, and the actual construction code if not. That code works on both client and server (it is isomorphic), and the added code is minimal.

Personally I think it would be preferable that closed shadow roots can still be SSR-ed, I understand that .attachShadow({ mode: 'closed' }) is kinda weird when a shadow root is already attached so perhaps a way to close a shadow root after the fact would make more sense:

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 ...
    }
    // ....
  }
}

Clearly, the non-custom-element version results in many fewer bytes, and a less complex (i.e. more memory-efficient) DOM.

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>

Jamesernator avatar Feb 12 '20 23:02 Jamesernator

I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate

Can you elaborate? All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

emilio avatar Feb 13 '20 00:02 emilio

From @sebmarkbage:

My initial reaction is very positive. It will take a long time until this ships in enough browsers that we'll actually consider using it. We'd have to really shift the CSS strategy. Even then, I don't think we'd use shadow DOM as the primary encapsulation mechanism given the relative overhead on each level given that our components are so small and many. You might call them micro-components - you heard the buzzword here first. However, for larger encapsulation of large third party or legacy parts of an app inside another one this would be very useful as an alternative to iframes. I'd like to see this exist but probably won't make any immediate plans to adopt it.

stubbornella avatar Feb 13 '20 00:02 stubbornella

I agree that problem b) is a problem that needs a solution. Perhaps the parser could detect exactly-duplicate

Can you elaborate? All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

Hmm - can you elaborate? I do know that duplicate <link rel=stylesheet> links will use a shared stylesheet. But I was not aware that exactly-duplicated inline <style> elements would end up with a single backing CSSStyleSheet object in memory. If that's true, then I would say the inline <style> element solution might be a good one here. Gzip will reduce the network overhead, and this stylesheet de-duplication will eliminate the memory overhead.

mfreed7 avatar Feb 13 '20 00:02 mfreed7

All browsers already avoid parsing duplicate inline stylesheets in Shadow DOM, as far as I'm aware.

How can it know they are dupes without parsing them?

stubbornella avatar Feb 13 '20 00:02 stubbornella

How can it know they are dupes without parsing them?

Hashmap from text to parsed stylesheet representation effectively.

But I was not aware that exactly-duplicated inline

They do:

You don't get a pointer-identical CSSStyleSheet because that'd be observable, but they share StyleSheetContents (curious how all engines ended up choosing the same name for this), which means that they copy-on-write all the CSS rules and such.

emilio avatar Feb 13 '20 00:02 emilio

(That's what happens with <link> as well, fwiw, you don't get a pointer-identical CSSStyleSheet either, as authors could mutate them independently)

emilio avatar Feb 13 '20 00:02 emilio

@emilio thanks for the code links. With that in mind, it would seem that inline <style> elements might be the most straightforward solution to the styling problem. Perhaps we could even augment the element.innerHTMLIncludingShadowRoots API to also automatically inline <style> elements for any adoptedStylesheets it finds?

mfreed7 avatar Feb 13 '20 00:02 mfreed7

What text should those stylesheets have? The serialized representation of their CSS rules? Or the original text that was passed to replace() / replaceSync()?

Because for inline style you don't get serialized back the result of CSSOM mutations. It'd be weird if adoptedStyleSheets would do that.

emilio avatar Feb 13 '20 00:02 emilio

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 <style> type that would create a non-applied StyleSheet object with the constructed bit set, and a way to refer to it by idref from declarative shadow roots:

<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 text/css means that the styles won't apply to the document scope even in older browsers. It's also what allows it to have an adoptable CSSStyleSheet .sheet, which replace() works on as well.

The ids in adopted-styles would probably have to search in the global scope, or possibly in ancestor scopes, not just in the scope they're defined in (which may be nested in other declarative shadow roots).

How this plays with innerHTML/outerHTML is tricky, but if we have a new property/method for that (I think it should be a method so it can't be set and can take options), it could return the adopted-css in some determined place.

Note that these <style type="adopted-css"> elements would be very similar, maybe identical, to what's proposed for HTML Modules, in that they essentially create a Cascading Stylesheet Module.

justinfagnani avatar Feb 13 '20 01:02 justinfagnani

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:

  • Is it safe to say the solution would not have a noticeable flicker between the time the declarative SD is painted and the component's constructor is called?
  • Will there be performance issues when there's a large list of items, and each item also includes a many child elements with shadow dom? When stress testing this scenario you can see some noticeable differences between a solution using adopted stylesheets, and one inlining style tags into each shadow root (especially on low-end mobile devices which Ionic is targeted for).
  • It'd be pretty easy for devs to explode the size of their html in comparison to traditional html/css. Maybe I'm overthinking it, but what's largely unknown right now is how shared styles could even be possible, but if not, does that also add to the problems that adopted stylesheets should be solving?
  • Is it possible to avoid any additional JS checking the existence of a shadow root? Basically I'd like to avoid any new logic in the constructor if possible.
  • From what I can see, it seems a significant challenge is how styles are handled, especially around reusing styles. Curious if solving declarative adopted stylesheets should come first.

Overall I'm excited to see this discussion and absolutely can see how it'll benefit Ionic's use-case.

adamdbradley avatar Feb 13 '20 04:02 adamdbradley

@mfreed7 : Nice write up on https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md.

Could you get the new perf numbers after replacing template.content.cloneNode with template.content.importNode as well as just template.content? That would avoid adoption of the cloned nodes from the template's document into the destination document and cloning at all respectively. Otherwise, the most of runtime will be spent cloning & adopting nodes, not attaching a shadow root.

rniwa avatar Feb 13 '20 06:02 rniwa

This would be a "parser-only" feature, and adding the shadowroot attribute to an existing <template> would have no effect.

Attribute injection, which is what this is responding to, means injecting them on the parser level. So the fact that this is a parse-only feature is not a protection against attribute injection.

Specifically, if a page has a <template> and you manage to get <script> inside it, right now that is safe. If you can also get this new attribute on the <template>, that is no longer safe...

bzbarsky avatar Feb 13 '20 06:02 bzbarsky

I see you guys love reinventing the bike. This looks like a crappy, less declarative and non-standard version of XSLT 1.0 (which came out in 1999, latest version is 3.0). "Congrats" on wasting 20 years of web potential...

namedgraph avatar Feb 13 '20 12:02 namedgraph