Как создать пользовательский элемент без attachShadow?

#javascript #html #web-component #shadow-dom

#javascript #HTML #веб-компонент #shadow-dom

Вопрос:

Допустим, у меня есть такой код:

 class MyElem extends HTMLElement {
  constructor() {
    super();
    
    let templateContent = document.getElementById('template-elem').content;
    this.innerHTML = templateContent.cloneNode(true);
  }
}

window.customElements.define('my-elem', MyElem); 
 <template id="template-elem">
  <div class="a">
    <div class="b">b</div>
    <div class="c">c</div>
  </div>
</template>

<my-elem></my-elem> 

Почему это не работает? В инспекторе Chrome пользовательский элемент не содержит HTML внутри него. Я также пытался делать:

 this.append(templateContent.cloneNode(true)); 
 

но это также привело к пустому HTML-дереву.

Во всех руководствах упоминается использование shadow DOM следующим образом:

 this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
 

и хотя это работает, это заставляет вас использовать Shadow DOM для вашего пользовательского элемента. Нет ли способа просто добавить HTML-код шаблона к вашему пользовательскому элементу, не требуя использования Shadow DOM? Я бы предпочел просто использовать глобальный стиль CSS в моем небольшом варианте использования.

Ответ №1:

Вы попадаете в множество ловушек, как и все в своих первых приключениях с компонентами.

  1. Пользовательские элементы (строго говоря, веб-компонентами являются только элементы с ShadowDOM) имеют фазы жизненного цикла и обратные вызовы.
    Эта схема: https://andyogo.github.io/custom-element-reactions-diagram / необходимо понять.
    Вы хотите добавить содержимое DOM на constructor этапе; но на этом этапе еще нет элемента DOM.
    Только в connectedCallback DOM можно добавлять содержимое.
    С ShadowDOM это другая история, его «DocumentFragment» доступен в constructor , вы можете установить содержимое, но это еще не элемент DOM! connectedCallback Сообщает вам, когда ваш пользовательский элемент был присоединен к DOM.
  2. Содержимое шаблонов — это DocumentFragment, но ваш .innerHTML ожидает строку.
    Поскольку (в вашем использовании) <template> является элементом DOM, вы можете прочитать его innerHTML (см. Ниже)

Итак, да, пользовательские элементы без ShadowDOM возможны:

Вы увидите <template> содержимое дважды, демонстрируя 2 способа добавления содержимого.

 <script>
  customElements.define("my-element", class extends HTMLElement {
    connectedCallback() {
      let template = document.getElementById(this.nodeName);
      this.innerHTML = template.innerHTML;
      this.append(template.content.cloneNode(true))
    }
  })

</script>

<template id="MY-ELEMENT">
  Hello, I am an Element!
</template>

<my-element></my-element> 

Это то constructor , где вы готовите свой элемент

Это constructor также выполняется, когда вы это делаете document.createElement("my-element") .

connectedCallback Запускается, когда ваш элемент добавляется в DOM

Если вы не укажете метод, запускается метод из его родительского класса, поэтому в приведенном выше коде выполняется (по умолчанию) constructor из htmlэлемента.
Вот почему вам нужен super() свой собственный constructor … для выполнения элемента constructor from HTMLElement .

Примечание:

 constructor(){
 let template = document.getElementById("MY-ELEMENT").content.cloneNode(true);
 super().attachShadow({mode:"open").append(template);
}
 

это полностью корректный код; Документация Google, в которой говорится, что «сначала нужно запустить super», неверна.
Вам нужно выполнить, super() прежде чем вы сможете получить доступ к собственной области элементов с помощью this

Вот почему я предпочитаю:

 constructor(){

 // do anything you want here, but you can not use 'this'

 super() // Sets AND Returns 'this'
   .attachShadow({mode:"open") // both Sets AND Returns this.shadowRoot
   .append(document.getElementById(this.nodeName).content.cloneNode(true));
}
 

Обратите внимание, что append() не был доступен в IE; поэтому программисты oldskool не будут знать о его универсальности: https://developer.mozilla.org/en-US/docs/Web/API/Element/append

Когда ваши приключения с компонентами будут связаны с наследованием классов;
вы вызываете родительские методы с:

 connectedCallback(){
  super.connectedCallback()
}
 

Комментарии:

1. Ах, хорошо, большое спасибо! Да, мне было интересно, почему я могу добавить в конструктор для shadow DOM, но не обычный DOM. Это имеет смысл.

Ответ №2:

Наиболее простой реализацией пользовательского элемента будет:

 class MyComponent extends HTMLElement {
    connectedCallback() {
        this.innerHTML = `<div>Hello world</div>`
    }
}

customElements.define('my-component', MyComponent) 
 my-component {
  display: block;
  border: 1px dotted #900
} 
 <my-component></my-component> 

Однако, если вы не используете Shadow DOM, вы не сможете инкапсулировать CSS, но должны стилизовать компонент с помощью внешней таблицы стилей.

Самый простой способ написать компонент с теневым DOM будет выглядеть так:

 class MyOtherComponent extends HTMLElement {
    constructor() {
        super()
        this.shadow = this.attachShadow({ mode: "open" })
    }

    connectedCallback() {
        this.shadow.innerHTML = `
            <style>
                :host {
                  display: block;
                  border: 1px dotted #900
                }
            </style>
            <div class="component">Hello World!</div>
        `
    }
}

customElements.define('my-other-component', MyOtherComponent) 
 <my-other-component></my-other-component> 

Таким образом, у вас будет немного больше накладных расходов, но компонент действительно инкапсулирован.