Attaching shadow DOM to a custom element removes error, but why?

两盒软妹~` 提交于 2020-01-15 11:25:50

问题


Per the custom element specification,

The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.

Both Firefox and Chrome correctly throw an error in this situation. However, when attaching a shadow DOM, no error is present (in either browser).

Firefox:

NotSupportedError: Operation is not supported

Chrome:

Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children

Without shadow DOM

function createElement(tag, ...children) {
  let root;

  if (typeof tag === 'symbol') {
    root = document.createDocumentFragment();
  } else {
    root = document.createElement(tag);
  }

  children.forEach(node => root.appendChild(node));

  return root;
}

customElements.define(
  'x-foo',
  class extends HTMLElement {
    constructor() {
      super();

      this.appendChild(
        createElement(
          Symbol(),
          createElement('div'),
        ),
      );
    }
  },
);

createElement('x-foo');

With shadow DOM

function createElement(tag, ...children) {
  let root;

  if (typeof tag === 'symbol') {
    root = document.createDocumentFragment();
  } else {
    root = document.createElement(tag);
  }

  children.forEach(node => root.appendChild(node));

  return root;
}

customElements.define(
  'x-foo',
  class extends HTMLElement {
    constructor() {
      super();

      // it doesn't matter if this is open or closed
      this.attachShadow({ mode: 'closed' }).appendChild(
        createElement(
          Symbol(),
          createElement('div'),
        ),
      );
    }
  },
);

createElement('x-foo');

Please note: in order to view the examples, you need to be using (at least) one of the following: Firefox 63, Chrome 67, Safari 10.1. Edge is not supported.

My question is as follows:

Is the behavior demonstrated correct, per the specification?

Adding a child node to the root would cause a DOM reflow; how can this be avoided without a shadow DOM present?


回答1:


Every time an element is created it is done through the constructor. But, when the constructor is called there are no children nor any attributes, Those are all added AFTER the component is created.

Even if the element is defined in the HTML page, it is still created by code using the constructor and then the attributes and children are added by the code that is parsing the DOM in the HTML page.

When the constructor is called there are no children and you can not add them since the DOM parser may be adding them as soon as the constructor is finished. The same rule applies for the attributes.

Currently there is no way to specify shadowDOM or shadowDOM children except through JS code. The DOM parser will not add any children to the shadowDOM.

So according to the spec it is illegal to access, change or do anything with attributes or children in the constructor. But, since there is no way for the DOM parser to add anything into a components shadowDOM that is not illegal.

I have gotten around this problem when not using shadowDOM by using an internal template element that is created in the constructor and then placed as a child once the connectedCallback is called.

// Class for `<test-el>`
class TestEl extends HTMLElement {
  constructor() {
    super();
    console.log('constructor');
    const template = document.createElement('template');
    template.innerHTML = '<div class="name"></div>';
    this.root = template.content;
    this.rendered = false;
  }

  static get observedAttributes() {
    return ['name'];
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal !== newVal) {
      console.log('attributeChangedCallback', newVal);
      this.root.querySelector('.name').textContent = newVal;
    }
  }

  connectedCallback() {
    console.log('connectedCallback');
    if (!this.rendered) {
      this.rendered = true;
      this.appendChild(this.root);
      this.root = this;
    }
  }

  // `name` property
  get name() {
    return this.getAttribute('name');
  }
  set name(value) {
    console.log('set name', value);
    if (value == null) { // Check for null or undefined
      this.removeAttribute('name');
    }
    else {
      this.setAttribute('name', value)
    }
  }
}

// Define our web component
customElements.define('test-el', TestEl);

const moreEl = document.getElementById('more');
const testEl = document.getElementById('test');
setTimeout(() => {
testEl.name = "Mummy";
  const el = document.createElement('test-el');
  el.name = "Frank N Stein";
  moreEl.appendChild(el);
}, 1000);
<test-el id="test" name="Dracula"></test-el>
<hr/>
<div id="more"></div>

This code creates a template in the constructor and uses this.root to reference it. Once connectedCallback is called I insert the template into the DOM and change this.root to point to this so that all of my references to elements still work.

This is a quick way to allow your component to always be able to keep its children correct without using shadowDOM and only placing the template into the DOM as children once connectedCalback is called.




回答2:


The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.

The "expectations" of createElement() is to be given a empty element (without HTML attributes, or children HTML elements), like any other standard HTML element that you create with createElement().

Therefore the impact of adding the Custom Elements to the HTML and DOM specification (and as a consequence, on the HTML engine implementation) is somehow limited.

This restriction doesn't apply to the Shadow DOM because it was not part of the specs before. This won't alter the above expectations. Hence the "odd" difference between the normal DOM tree and the Shadow DOM tree, you're right.

Also, the ability to add a Shadow DOM in the contructor() and not a light DOM, ensures that when the elements of the light DOM are added, they will preventively be filtered according to the Shadow DOM template (and event model, if you listen to the slotchange event).



来源:https://stackoverflow.com/questions/53180428/attaching-shadow-dom-to-a-custom-element-removes-error-but-why

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!