Императивный подход создания shadow dom
📅
Что такое императивный подход
Императивный подход — это создание Shadow DOM через JavaScript с полным программным контролем. Вы сами управляете каждым этапом создания компонента.
Создание через attachShadow() 🛠️
attachShadow() — это метод DOM-элемента, который создаёт и присоединяет к нему Shadow Root — корень теневого DOM. Метод возвращает ссылку на теневое дерево или null, если теневое дерево не удалось создать
const shadowRoot = element.attachShadow({ mode: 'open' });
Параметр mode - уровень изоляции теневого дерева. Может принимать два значения:
'open'— теневой DOM доступен из внешнего кода через свойствоelement.shadowRoot.'closed'— теневой DOM скрыт, к нему нельзя обратиться извне.
Поговорим об этом позже в разделе "Shadow DOM - Открытый и закрытый режимы".
🟡
- Для каждого элемента мы можем создать только один shadow root, то есть attachShadow() можно вызвать только один раз.
- В качестве
elementможет быть использован пользовательский элемент (Custom Element), либо один из следующих элементов: «article», «aside», «blockquote», «body», «div», «footer», «h1…h6», «header», «main», «nav», «p», «section» или «span». Остальные, например,, не могут содержать теневое дерево.
Где создавать
1. В конструкторе
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
}
✅ Преимущества:
- Инкапсуляция с самого начала: Элемент сразу рождается с изолированным DOM и CSS, нет момента, когда его внутренности видны в основном документе.
- Производительность: DOM строится один раз при создании элемента.
- Предсказуемость: Вы точно знаете, где и когда создается Shadow Root.
❌ Недостатки
- Потенциально лишняя работа если элемент не будет добавлен в DOM
🟡 Не забывайете в конструкторе вызывать super();
2. В connectedCallback
class MyElement extends HTMLElement {
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
}
}
Когда это может быть полезно:
- 🚀 Производительнее, особенно, если нет уверенности, что элемент действительно будет добавлен в разметку. Если компонент тяжелый и вы хотите отложить его создание до момента реальной необходимости (когда его вставят в страницу). Хотя в современных браузерах разница обычно незначительна.
- 🧩 Нетипичный код, когда, например, требуется использовать значения атрибутов при создании теневого дерева, ведь в конструкторе таких данных еще нет. Но обычно эту логику лучше переносить в
attributeChangedCallback.
🔴 В connectedCallback всегда проверяйте if (!this.shadowRoot), чтобы случайно не создать несколько Shadow Root на одном элементе (это выбросит ошибку).
🟢 Начните с конструктора и переходите к connectedCallback если:
-
Нужно читать атрибуты для конфигурации Shadow DOM
-
Shadow DOM нужен не всегда
-
Инициализация очень тяжелая и ее нужно отложить
-
Вы не уверены, что элемент появится в разметке
3. Отложенная инициализация (по требованию)
class MyElement extends HTMLElement {
initializeShadow() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
}
}
Используется редко.
4. Вне класса компонента
const element = new MyElement();
element.attachShadow({ mode: 'open' }); // Может работать, но антипаттерн
❌ Недостатки
- Нарушение инкапсуляции (Вмешательство извне)
- Нарушение принципов ООП
- Уязвимость к ошибка
- Проблемы с переиспользованием элемента
- Сложнее тестировать и отлаживать
В общем, по-простому, для кастомных элементов точно лучше все это делать в одном месте, в классе. Если уж нужно как-то нестандартно создавать теневое дерево - создайте публичный метод у элемента.
Рекомендация
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
//Дополнительная инициализация на основе атрибутов
this.updateContent();
}
}
Такой подход сочетает преимущества 1-го и 2-го подходов.
Наполнение shadow DOM
Метод attachShadow() возвращает ссылку на теневое дерево c которым мы можем работать точно так же как и с любым другим элементом. Добавлять и удалять узлы, совершать поиск по элементам и так далее.
Простой компонент
class SimpleButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
padding: 10px 20px;
border: none;
background: blue;
color: white;
}
</style>
<button>Кнопка</button>
`;
}
}
Динамический компонент
class DynamicList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
const items = JSON.parse(this.getAttribute('items') || '[]');
this.shadowRoot.innerHTML = `
<ul>
${items.map(item => `<li>${item}</li>`).join('')}
</ul>
`;
}
}
Преимущества императивного подхода
- Динамичность и гибкость
// Генерация на основе данных
render(data){
this.shadowRoot.innerHTML = `
<ul>
${data.map(item => `<li>${item.name}</li>`).join('')}
</ul>
`;
}
- Полный контроль
// Динамическое создание элементов
const div = document.createElement('div');
div.className = 'container';
this.shadowRoot.appendChild(div);
// Постепенное наполнение
const title = document.createElement('h3');
title.textContent = 'Заголовок';
div.appendChild(title);
- Оптимизация производительности
- Гибкая логика
// Условное создание
if (this.hasAttribute('advanced')) {
this.createAdvancedUI();
} else {
this.createSimpleUI();
}
Когда использовать
✅ Идеальные случаи:
- Динамические UI-компоненты
- Сложная бизнес-логика
- Компоненты с изменяемым состоянием
⚠️ Ограничения:
- Требует JavaScript
- Не работает в SSR
- Сложнее для статичного контента