Skip to content
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

Closed
mfreed7 opened this issue Feb 7, 2020 · 276 comments
Closed

Declarative Shadow DOM #831

mfreed7 opened this issue Feb 7, 2020 · 276 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: shadow Relates to shadow trees (as defined in DOM)

Comments

@mfreed7
Copy link
Contributor

mfreed7 commented Feb 7, 2020

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>
@emilio
Copy link
Contributor

emilio commented Feb 10, 2020

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
Copy link
Contributor

emilio commented Feb 10, 2020

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

@annevk annevk added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: shadow Relates to shadow trees (as defined in DOM) labels Feb 10, 2020
@annevk
Copy link
Member

annevk commented Feb 10, 2020

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.

@smaug----
Copy link
Collaborator

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

@bgrins
Copy link

bgrins commented Feb 11, 2020

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.

@calebdwilliams
Copy link

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.

@vikerman
Copy link

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

@FluorescentHallucinogen

@UpperCod
Copy link

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

@justinfagnani
Copy link

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

@davatron5000
Copy link

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.

@justinfagnani
Copy link

justinfagnani commented Feb 12, 2020

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

@tabatkins
Copy link
Contributor

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.

@Rich-Harris
Copy link

Rich-Harris commented Feb 12, 2020

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.

@davatron5000
Copy link

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

@mfreed7
Copy link
Contributor Author

mfreed7 commented Feb 12, 2020

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!

@Jamesernator
Copy link

Jamesernator commented Feb 12, 2020

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>

@emilio
Copy link
Contributor

emilio commented Feb 13, 2020

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.

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

@stubbornella
Copy link

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.

@mfreed7
Copy link
Contributor Author

mfreed7 commented Feb 13, 2020

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.

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.

@stubbornella
Copy link

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?

@emilio
Copy link
Contributor

emilio commented Feb 13, 2020

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

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
Copy link
Contributor

emilio commented Feb 13, 2020

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

@mfreed7
Copy link
Contributor Author

mfreed7 commented Feb 13, 2020

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

@emilio
Copy link
Contributor

emilio commented Feb 13, 2020

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.

@justinfagnani
Copy link

justinfagnani commented Feb 13, 2020

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.

@adamdbradley
Copy link

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.

@EdwardIrby
Copy link

Is this the right place to discuss the features of the declarative shadow DOM and how they might be implemented into libraries.

Specifically:

  • SSR vs CSR
  • Styling within component driven patterns?

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?

@annevk
Copy link
Member

annevk commented May 16, 2023

@rniwa @mfreed7 WICG/sanitizer-api#193 made me realize that we currently don't restrict declarative shadow roots to elements that can have ElementInternals. But I think we should as only that will allow encapsulated usage. Thoughts?

@mfreed7
Copy link
Contributor Author

mfreed7 commented May 16, 2023

@rniwa @mfreed7 WICG/sanitizer-api#193 made me realize that we currently don't restrict declarative shadow roots to elements that can have ElementInternals. But I think we should as only that will allow encapsulated usage. Thoughts?

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 ElementInternals support to ordinary <div>s?

@annevk
Copy link
Member

annevk commented May 16, 2023

@mfreed7 I'm saying that we restrict it to custom elements as only those have an internal shadowRoot accessor.

@mfreed7
Copy link
Contributor Author

mfreed7 commented May 16, 2023

@mfreed7 I'm saying that we restrict it to custom elements as only those have an internal shadowRoot accessor.

Oh! You want DSD to be different from what you can do with attachShadow()? I would really like declarative shadow dom to just be the declarative way to create shadow dom. I'm not sure why we'd want to add further restrictions?

@annevk
Copy link
Member

annevk commented May 16, 2023

@mfreed7 how can you do a closed declarative shadow root with a <div>? I don't think you can. And think we should maintain parity between open and closed.

(In retrospect it was probably a mistake to allow them on anything but custom elements (in userland, that is). attachShadow() on ElementInternals would have removed the need for a lot of bookkeeping.)

@mfreed7
Copy link
Contributor Author

mfreed7 commented May 16, 2023

@mfreed7 how can you do a closed declarative shadow root with a <div>? I don't think you can. And think we should maintain parity between open and closed.

<div>
  <template shadowrootmode=closed> I'm closed! </template>
</div>

Maybe I'm missing what you're asking.

(In retrospect it was probably a mistake to allow them on anything but custom elements (in userland, that is). attachShadow() on ElementInternals would have removed the need for a lot of bookkeeping.)

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.

@annevk
Copy link
Member

annevk commented May 16, 2023

Maybe I'm missing what you're asking.

How can you manipulate it from script?

@jridgewell
Copy link

From a developer perspective, I'd rather you remove the ability to have closed roots on <div> elements than you remove the DSD from divs entirely. I also don't think it's an issue that you can't programatically modify a closed root after the fact, I'd just SSR the appropriate contents to begin with.

@annevk
Copy link
Member

annevk commented May 16, 2023

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

@dfabulich
Copy link

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

But I think your mistake was to think that if you can't manipulate a <div>'s closed DSD, you simply can't "do" a <div> with closed DSD at all.

Shadow DOM created with attachShadow() requires you to manipulate it via script; if there were a design bug that prevented you from manipulating closed attachShadow() DOM in script, then the whole feature would be useless.

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 <div> with closed DSD works great, and is useful, I can't see why anyone would want to remove the feature. For "parity"? Who benefits from removing this feature?

@bathos
Copy link

bathos commented May 16, 2023

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 onsomething event) because it will be their parentNode. This may not be particularly useful in most cases but figured it was worth mentioning that it’s not the case that there’s definitely no reference to it from script. The following logs the div’s closed shadow root:

<div>
  <template shadowroot=closed>
    <x-x></x-x>
  </template>
</div>

<script>
  customElements.define("x-x", class extends HTMLElement {
    connectedCallback() {
      console.log(this.parentNode);
    }
  });
</script>

@mfreed7
Copy link
Contributor Author

mfreed7 commented Aug 25, 2023

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

Since <div> with closed DSD works great, and is useful, I can't see why anyone would want to remove the feature. For "parity"? Who benefits from removing this feature?

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

@annevk
Copy link
Member

annevk commented Aug 25, 2023

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.

@dfabulich
Copy link

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.

@mfreed7
Copy link
Contributor Author

mfreed7 commented Aug 25, 2023

how do developers benefit

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.

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 open and closed parity is an odd dimension to try to maintain: these are designed to be different, specifically along the dimension of access control to the shadow contents. However, I think there is a parity that is important to try to maintain: imperative vs. declarative versions of the same API. Those should provide equivalent functionality. Any difference is a) confusing, and b) limiting.

@mfreed7
Copy link
Contributor Author

mfreed7 commented Sep 15, 2023

See update here: whatwg/html#5465 (comment)

@mangelozzi
Copy link

mangelozzi commented Sep 22, 2023

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.
One has to have some sort of library of component scripts at the top of the page anyway for all the components using script, something like this:
(PS I am not recommending the definition tag, just the concept: where this instance is not instantiated and is not inserted into the DOM for a user to see, but is just a blueprint).

<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 As someone who uses custom elements almost daily, and loves them, in an idea world, we could define the template/styles, and javascript all declaratively, once, and then just use it. Say something like this:
<!-- 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.
Basically what we would love is reusable junks of HTML (which can have styles/javasript), and refer to those encapsulated chunks with a custom tag name. Unfortunately we still don't have a way to import HTML.

annevk pushed a commit to mfreed7/dom that referenced this issue Oct 2, 2023
mfreed7 added a commit to mfreed7/dom that referenced this issue Oct 2, 2023
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
annevk pushed a commit to whatwg/html that referenced this issue Oct 29, 2023
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.
@annevk annevk closed this as completed in ca756e0 Oct 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: shadow Relates to shadow trees (as defined in DOM)
Development

No branches or pull requests