Кастомный метод parsedCallback в Web Components не работает

Вопрос или проблема

Важно ЗАМЕТИТЬ: почти все среды разработки автоматически откладывают выполнение 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);

Объяснение кода

  1. Добавление уникального маркера: Я изменил код для добавления комментария после элемента на уникальный маркер, что снижает вероятность конфликта с другими элементами.

  2. Применение MutationObserver: Мы проверяем добавленные узлы и, если находим "младший" сосед, вызываем метод parsedCallback.

  3. Удаление маркера: Маркер удаляется только после вызова метода parsedCallback, чтобы избежать потенциальной ситуации, когда он будет удален до завершения всех необходимых операций.

Ожидаемый вывод

С данным исправлением вы должны увидеть ожидаемый вывод:

0 connected
0 has innerHTML on connected = false
0 younger sibling detected
0 parsed
0 has innerHTML on parsed = true

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...