Как иметь "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);
Вы можете отложить определение своих классов до тех пор, пока домен не будет загружен.