Re: [whatwg/dom] Declarative Shadow DOM (#831)

I was [asked](https://twitter.com/stubbornella/status/1227300749379375104) 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 is 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?

```js
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 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:

```html
<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](https://svelte.dev/repl/a15e5bf484bf4eddafe68996d4235187?version=3.18.2) with a non-web-component framework:

```html
<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 the `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:

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

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/831#issuecomment-585372554

Received on Wednesday, 12 February 2020 19:20:05 UTC