Вопрос или проблема
Важно ЗАМЕТИТЬ: почти все среды разработки автоматически откладывают выполнение javascript, что означает, что вы, возможно, не сможете воспроизвести эту проблему, используя их.
При работе с API Web Components в ситуации, когда скрипт, определяющий компонент, выполняется до разбора HTML документа, вы сталкиваетесь с неприятной проблемой: внутренний HTML элемента еще не будет разобран во время вызова жизненного цикла connectedCallback
. Это происходит потому, что метод выполняется на открывающем теге пользовательского элемента, прежде чем начнется разбор его дочерних элементов.
<script>
customElements.define('my-element', MYElement);
</script>
<my-element>
<span>Содержимое</span>
</my-element>
Чтобы исправить это, я хочу реализовать свой собственный метод parsedCallback
, однако то, что у меня есть на данный момент, не работает.
Идея заключается в том, что поскольку браузер разбирает HTML синхронно, мы можем знать, что все внутри компонента разобрано, как только разобран “младший” элемент-сосед.
Поэтому я добавляю наблюдатель за мутациями к родительскому элементу компонента, чтобы отслеживать, когда добавляется новый дочерний узел, который не является самим компонентом.
Более того, поскольку в разметке могут отсутствовать “натуральные” соседи, я также добавляю commentNode после компонента, чтобы гарантировать, что это всегда будет работать.
class MYElement extends HTMLElement {
static counter = 0;
constructor() {
super();
this._id = MYElement.counter++;
}
connectedCallback() {
console.log(`${this._id} подключен`);
console.log(`${this._id} имеет innerHTML при подключении = ${this.innerHTML !== ""}`);
const parent = this.parentElement;
const marker = document.createComment(this._id);
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node !== this) {
console.log(`${this._id} обнаружен младший сосед`);
this.parsedCallback();
marker.remove();
observer.disconnect();
}
});
});
});
observer.observe(parent, { childList: true });
parent.appendChild(marker);
}
parsedCallback() {
console.log(`${this._id} разобран`);
console.log(`${this._id} имеет innerHTML при разборе = ${this.innerHTML !== ""}`);
}
}
Мой ожидаемый вывод, конечно, был бы следующим:
0 подключен
0 имеет innerHTML при подключении = false
0 обнаружен младший сосед
0 разобран
0 имеет innerHTML при разборе = true
Однако, вместо этого я вижу то же самое, за исключением последней записи в консоль:
0 имеет innerHTML при разборе = false
Ответ или решение
Для решения проблемы с вызовом метода parsedCallback
в Web Components, в первую очередь необходимо понимать, как происходит синхронное парсинг HTML в браузере и когда выполняется метод connectedCallback
. Как вы правильно заметили, этот метод вызывается на открывающем теге кастомного элемента до того, как его дочерние элементы будут полностью разобраны. Это приводит к тому, что в connectedCallback
вы не получаете доступ к innerHTML
, так как он еще не был загружен.
Ваш план использовать MutationObserver
для отслеживания изменений в родительском элементе является разумным, но необходимо внести некоторые уточнения и изменения для корректной работы.
Проблема и решение
Основная проблема с вашим кодом заключается в том, что вы не учитываете момент, когда выводится содержание элемента. Когда вы добавляете комментарий после элемента, элемент сам по себе может еще не содержать разметки из-за моментального изменения дочерних элементов.
Вот исправленный вариант вашего кода:
class MYElement extends HTMLElement {
static counter = 0;
constructor() {
super();
this._id = MYElement.counter++;
}
connectedCallback() {
console.log(`${this._id} connected`);
console.log(`${this._id} has innerHTML on connected = ${this.innerHTML !== ""}`);
const parent = this.parentElement;
const marker = document.createComment('marker-for-' + this._id); // Уникальный маркер
parent.appendChild(marker);
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node !== this && node.nodeType === Node.ELEMENT_NODE) {
console.log(`${this._id} younger sibling detected`);
this.parsedCallback();
observer.disconnect();
marker.remove(); // Удаляем маркер только после вызова parsedCallback
}
});
});
});
observer.observe(parent, { childList: true });
}
parsedCallback() {
// Поскольку используется MutationObserver, мы теперь уверены, что содержимое уже парсится
console.log(`${this._id} parsed`);
console.log(`${this._id} has innerHTML on parsed = ${this.innerHTML !== ""}`); // Теперь должно возвращать true
}
}
customElements.define('my-element', MYElement);
Объяснение кода
-
Добавление уникального маркера: Я изменил код для добавления комментария после элемента на уникальный маркер, что снижает вероятность конфликта с другими элементами.
-
Применение MutationObserver: Мы проверяем добавленные узлы и, если находим "младший" сосед, вызываем метод
parsedCallback
. -
Удаление маркера: Маркер удаляется только после вызова метода
parsedCallback
, чтобы избежать потенциальной ситуации, когда он будет удален до завершения всех необходимых операций.
Ожидаемый вывод
С данным исправлением вы должны увидеть ожидаемый вывод:
0 connected
0 has innerHTML on connected = false
0 younger sibling detected
0 parsed
0 has innerHTML on parsed = true
Заключение
Этот подход позволит вам корректно обработать контент вашего кастомного элемента после его парсинга и получить доступ к его внутреннему содержимому. Убедитесь, что проверяете все добавленные элементы, чтобы избежать ошибок и непредвиденных ситуаций в будущем.