Как иметь "connectedCallback", когда все дочерние пользовательские элементы были подключены

Я использую Web Components v1.

Предположим, что два пользовательских элемента:

Родитель-element.html

<template id="parent-element">
    <child-element></child-element>
</template>

ребенок-element.html

<template id="child-element">
<!-- some markup here -->
</template>

Я пытаюсь использовать connectedCallback в parent-element чтобы инициализировать всю структуру DOM родительского/дочернего, когда он присоединен, что требует взаимодействия с методами, определенными в child-element.

Тем не менее, кажется, что child-element не определен правильно в момент, когда connectedCallback запускается для customElement:

родитель-element.js

class parent_element extends HTMLElement {
    connectedCallback() {
        //shadow root created from template in constructor previously
        var el = this.shadow_root.querySelector("child-element");
        el.my_method();
    }
}

Это не сработает, потому что el является HTMLElement а не child-element как ожидалось.

Мне нужен обратный вызов для parent-element только все дочерние пользовательские элементы в его шаблоне будут правильно прикреплены.

Решение в этом вопросе, похоже, не работает; this.parentElement - null внутри child-element connectedCallback().

ilmiont

Ответы

Ответ 1

Существует проблема с синхронизацией connectedCallback Он вызывается в первый раз, прежде чем обновляется любой из его дочерних элементов элемента. <child-element> является лишь HTMLElement когда connectedCallback называется.

Чтобы получить обновленный дочерний элемент, вам нужно сделать это в тайм-аут.

Выполните приведенный ниже код и посмотрите вывод консоли. Когда мы пытаемся вызвать дочерний метод, он терпит неудачу. Опять же, это связано с тем, как создаются веб-компоненты. И время, когда будет connectedCallback connectCallback.

Но в рамках setTimeout вызов дочернего метода. Это связано с тем, что вы разрешили время для дочернего элемента обновляться до вашего настраиваемого элемента.

С любопытством, если ты спросишь меня. Хотелось бы, чтобы была еще одна функция, которая была вызвана после того, как все дети были обновлены. Но мы работаем с тем, что имеем.

class ParentElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = '<h2>Parent Element</h2><child-element></child-element>';
  }
  
  connectedCallback() {
    let el = this.shadowRoot.querySelector("child-element");
    console.log('connectedCallback', el);
    try {
      el.childMethod();
    }
    catch(ex) {
      console.error('Child element not there yet.', ex.message);
    }
    setTimeout(() => {
      let el = this.shadowRoot.querySelector("child-element");
      console.log('setTimeout', el);
      el.childMethod();
    });
  }
}

customElements.define('parent-element', ParentElement);


class ChildElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = '<h3>Child Element</h3>';
  }

  childMethod() {
    console.info('In Child method');
  }
}

customElements.define('child-element', ChildElement);
<parent-element></parent-element>

Ответ 2

После некоторой дополнительной работы у меня есть решение.

Конечно, этот this.parentElement не работает в дочернем элементе; это в корне тени DOM!

Мое текущее решение, которое подходит для моего конкретного сценария, выглядит следующим образом:

родитель-element.js

init() {
    //Code to run on initialisation goes here
    this.shadow_root.querySelector("child-element").my_method();
}

дети-element.js

connectedCallback() {
    this.getRootNode().host.init();
}

Таким образом, в дочернем элементе мы получаем корневой узел (шаблон shadow DOM), а затем его узел, родительский элемент и вызов init(...), после чего родитель может получить доступ к дочернему элементу и полностью определить.

Это решение не идеально подходит по нескольким причинам, поэтому я не считаю его принятым.

1) Если есть несколько детей, чтобы ждать или глубже вложенность, становится сложнее организовать обратные вызовы.

2) Я обеспокоен последствиями для child-element, если я хочу использовать этот элемент в автономной емкости (т.е. Где-то еще, полностью отдельно от вложенного в parent-element), мне придется изменить его, чтобы явно проверить, getRootNode().host - это экземпляр parent-element.

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

Ответ 3

Если вы хотите избежать каких-либо визуальных сбоев, вызванных задержкой setTimeout, вы можете использовать MutationObserver.

class myWebComponent extends HTMLElement 
{
      connectedCallback() {

        var instance = this;
        var childrenConnectedCallback = () => {
            var addedNode = instance.childNodes[(instance.childNodes.length - 1)];
            /*  callback here */
        }

        var observer = new MutationObserver(childrenConnectedCallback);
        var config = { attributes: false, childList: true, subtree: true };
        observer.observe(instance, config);

        //make sure to disconnect
        setTimeout(() => {
            observer.disconnect();
        }, 0);

     }
}

Ответ 4

Мы столкнулись с очень связанными проблемами, когда дети недоступны в connectedCallback наших пользовательских элементов (v1).

Сначала мы попытались исправить connectedCallback с помощью очень сложного подхода, который также используется командой Google AMP (комбинация mutationObserver и проверка на nextSibling), что в итоге привело к https://github.com/WebReflection/html-parsed-element

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

Ответ 5

Используйте элементы слота в шаблоне ShadowDOM.

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

Элементы слота

Для решения этой проблемы был представлен элемент слота. С помощью элементов слота вы можете создавать заполнители внутри вашего шаблона ShadowDOM. Эти заполнители можно использовать, просто поместив элемент внутри пользовательского элемента в DOM.

Но как узнать, что заполнитель заполнен элементом?

Элементы слота могут прослушивать уникальное событие под названием slotchange. Это будет срабатывать всякий раз, когда элемент (или несколько элементов) помещается в позицию элемента slot.

Внутри слушателя события вы можете получить доступ ко всем элементам в заполнителе с помощью методов HTMLSlotElement.assignedNodes() или HTMLSlotElement.assignedElements(). Они возвращают массив с элементами, помещенными в slot.

class ParentElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = '<h2>Parent Element</h2><slot></slot>';
    console.log("I'm a parent. I have slots.");
    
    // Select the slot element and listen for the slotchange event.
    const slot = this.shadowRoot.querySelector('slot');
    slot.addEventListener('slotchange', (event) => {
      const children = event.target.assignedElements();
      children.forEach(child => child.shout());
    });

  }
}

customElements.define('parent-element', ParentElement);

class ChildElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = '<h3>Child Element</h3>';
  }

  shout() {
    console.log("I'm a child, placed inside a slot.");
  }

}

customElements.define('child-element', ChildElement);
<parent-element>
  <child-element></child-element>
  <child-element></child-element>
  <child-element></child-element>
</parent-element>

Ответ 6

document.addEventListener('DOMContentLoaded', defineMyCustomElements);

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