Прикрепление теневого DOM к пользовательскому элементу устраняет ошибку, но почему? - PullRequest
0 голосов
/ 07 ноября 2018

Согласно спецификации пользовательского элемента ,

Элемент не должен иметь никаких атрибутов или дочерних элементов, поскольку это нарушает ожидания потребителей, использующих методы createElement или createElementNS.

И Firefox, и Chrome правильно выдают ошибку в этой ситуации. Однако при подключении теневого DOM ошибки не возникает (в любом браузере).

Firefox:

NotSupportedError: Операция не поддерживается

Chrome:

Uncaught DOMException: не удалось создать 'CustomElement': у результата не должно быть потомков

Без тени 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');

С тень 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');

Обратите внимание: для просмотра примеров необходимо использовать (как минимум) одно из следующих: Firefox 63, Chrome 67, Safari 10.1. Край не поддерживается.

У меня такой вопрос:

Правильно ли продемонстрировано поведение согласно спецификации?

Добавление дочернего узла в корень приведет к перекомпоновке DOM; как этого можно избежать без теневого DOM?

Ответы [ 2 ]

0 голосов
/ 07 ноября 2018

Элемент не должен иметь никаких атрибутов или дочерних элементов, поскольку это нарушает ожидания потребителей, которые используют методы createElement или createElementNS.

«Ожидания» createElement() должны быть заданы пустым элементом (без атрибутов HTML или дочерних элементов HTML), как и любой другой стандартный элемент HTML, который вы создаете с помощью createElement().

Поэтому влияние добавления пользовательских элементов в спецификации HTML и DOM (и, как следствие, на реализацию движка HTML) каким-то образом ограничено.

Это ограничение не распространяется на Shadow DOM, потому что раньше оно не входило в спецификации. Это не изменит вышеуказанные ожидания. Отсюда и «странная» разница между обычным DOM-деревом и Shadow DOM-деревом, вы правы.

Кроме того, возможность добавления теневого DOM в contructor(), а не легкого DOM, гарантирует, что при добавлении элементов легкого DOM они будут предварительно отфильтрованы в соответствии с шаблоном Shadow DOM (и моделью события). , если вы слушаете событие slotchange).

0 голосов
/ 07 ноября 2018

Каждый раз, когда создается элемент, это делается через конструктор. Но когда вызывается конструктор, нет ни дочерних элементов, ни каких-либо атрибутов. Все они добавляются ПОСЛЕ создания компонента.

Даже если элемент определен на странице HTML, он все равно создается кодом с использованием конструктора, а затем атрибуты и дочерние элементы добавляются кодом, который анализирует DOM на странице HTML.

Когда вызывается конструктор, дочерних элементов нет, и вы не можете добавить их, поскольку анализатор DOM может добавлять их, как только конструктор завершит работу. То же правило применяется к атрибутам.

В настоящее время нет способа указать дочерние элементы shadowDOM или shadowDOM, кроме как посредством кода JS. Парсер DOM не добавит дочерних элементов в shadowDOM.

Таким образом, согласно спецификации, запрещено иметь доступ, изменять или делать что-либо с атрибутами или дочерними элементами в конструкторе. Но, поскольку парсер DOM не может добавить в shadowDOM компоненты что-нибудь недопустимое.

Я справился с этой проблемой, когда не использовал shadowDOM, используя внутренний элемент шаблона, который создается в конструкторе и затем помещается как дочерний элемент после вызова connectedCallback.

// 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.root для ссылки на него. После вызова connectedCallback я вставляю шаблон в DOM и изменяю this.root, чтобы указать this, чтобы все мои ссылки на элементы все еще работали.

Это быстрый способ позволить вашему компоненту всегда сохранять правильность своих дочерних элементов без использования shadowDOM и только помещать шаблон в DOM как дочерние элементы только после вызова connectedCalback.

...