Работа с теневым DOM
Компоненты Lit используют теневой DOM для инкапсуляции своего DOM. Теневой DOM позволяет добавить к элементу отдельное изолированное и инкапсулированное дерево DOM. Инкапсуляция DOM — ключ к обеспечению совместимости с любым другим кодом, включая другие веб-компоненты или компоненты Lit, работающие на странице.
Теневой DOM предоставляет три преимущества:
- Область видимости DOM. API DOM, такие как
document.querySelector, не находят элементы в теневом DOM компонента, поэтому глобальным скриптам сложнее случайно нарушить работу компонента. - Область видимости стилей. Вы можете создавать инкапсулированные стили для теневого DOM, не затрагивающие остальную часть дерева DOM.
- Композиция. Теневой корень компонента, содержащий его внутренний DOM, отделён от дочерних элементов компонента. Вы можете выбрать, как дочерние элементы будут отображаться во внутреннем DOM компонента.
Дополнительную информацию о теневом DOM можно найти здесь:
- Теневой DOM v1: автономные веб-компоненты на сайте Web Fundamentals.
- Использование теневого DOM на MDN.
Старые браузеры. В старых браузерах, где нативный теневой DOM недоступен, можно использовать полифилы веб-компонентов. Обратите внимание, что модуль polyfill-support Lit необходимо загружать вместе с полифилами веб-компонентов. Подробности см. в разделе Требования для старых браузеров.
Доступ к узлам в теневом DOM
Lit отображает компоненты в свой renderRoot, который по умолчанию является теневым корнем. Чтобы найти внутренние элементы, можно использовать API запросов DOM, например this.renderRoot.querySelector().
renderRoot всегда должен быть теневым корнем или элементом, поскольку у них есть общие API, такие как .querySelectorAll() и .children.
Можно запрашивать внутренний DOM после первоначального рендеринга компонента (например, в firstUpdated) или использовать шаблон с геттером:
firstUpdated() {
this.staticNode = this.renderRoot.querySelector('#static-node');
}
get _closeButton() {
return this.renderRoot.querySelector('#close-button');
}
LitElement предоставляет набор декораторов, позволяющих кратко определять подобные геттеры.
Декораторы @query, @queryAll и @queryAsync
Декораторы @query, @queryAll и @queryAsync позволяют удобно получать доступ к узлам внутреннего DOM компонента.
Использование декораторов. Декораторы — это предлагаемая возможность JavaScript, поэтому для их использования понадобится компилятор, например Babel или TypeScript. Подробности см. в разделе Использование декораторов.
@query
Изменяет свойство класса, превращая его в геттер, возвращающий узел из корня рендеринга. Необязательный второй аргумент, если он равен true, выполняет запрос DOM только один раз и кэширует результат. Это можно использовать для оптимизации производительности в случаях, когда запрашиваемый узел не будет меняться.
import {LitElement, html} from 'lit';
import {query} from 'lit/decorators/query.js';
class MyElement extends LitElement {
@query('#first')
_first;
render() {
return html`
<div id="first"></div>
<div id="second"></div>
`;
}
}
Этот декоратор эквивалентен следующему коду:
get _first() {
return this.renderRoot?.querySelector('#first') ?? null;
}
@queryAll
Работает так же, как query, но возвращает все подходящие узлы, а не один. Эквивалентен вызову querySelectorAll.
import {LitElement, html} from 'lit';
import {queryAll} from 'lit/decorators/queryAll.js';
class MyElement extends LitElement {
@queryAll('div')
_divs;
render() {
return html`
<div id="first"></div>
<div id="second"></div>
`;
}
}
В этом примере _divs вернёт оба элемента <div> в шаблоне. В TypeScript тип свойства @queryAll — это NodeListOf<HTMLElement>. Если вы точно знаете, какие именно узлы будут получены, тип можно указать точнее:
@queryAll('button')
_buttons!: NodeListOf<HTMLButtonElement>
Восклицательный знак (!) после buttons — это оператор утверждения ненулевого значения TypeScript. Он указывает компилятору считать, что buttons всегда определён и никогда не равен null или undefined.
@queryAsync
Работает аналогично @query, но вместо непосредственного возврата узла возвращает Promise, который разрешается в этот узел после завершения всех ожидающих рендерингов элемента. Вместо ожидания промиса updateComplete можно использовать этот способ.
Это полезно, например, если узел, возвращаемый @queryAsync, может измениться в результате изменения другого свойства.
Отображение дочерних элементов с помощью слотов
Ваш компонент может принимать дочерние элементы (например, элемент <ul> может иметь дочерние элементы <li>).
<my-element> <p>A child</p> </my-element>
По умолчанию дочерние элементы элемента с теневым деревом вообще не отображаются.
Чтобы отображать дочерние элементы, шаблон должен содержать один или несколько элементов <slot>, которые служат заполнителями для дочерних узлов.
Использование элемента slot
Чтобы отображать дочерние элементы элемента, создайте для них <slot> в шаблоне элемента. Дочерние элементы не перемещаются в дереве DOM, но отображаются так, как если бы они были дочерними элементами <slot>. Например:
Использование именованных слотов
Чтобы назначить дочерний элемент определённому слоту, убедитесь, что атрибут slot дочернего элемента совпадает с атрибутом name слота:
-
Именованные слоты принимают только дочерние элементы с соответствующим атрибутом
slot.Например,
<slot name="one"></slot>принимает только дочерние элементы с атрибутомslot="one". -
Дочерние элементы с атрибутом
slotотображаются только в слоте с соответствующим атрибутомname.Например,
<p slot="one">...</p>будет помещён только в<slot name="one"></slot>.
Задание резервного содержимого слота
Для слота можно задать резервное содержимое. Оно отображается, если слоту не назначен ни один дочерний элемент.
<slot>I am fallback content</slot>
Отображение резервного содержимого. Если слоту назначены какие-либо дочерние узлы, его резервное содержимое не отображается. Слот по умолчанию без имени принимает любые дочерние узлы. Он не будет отображать резервное содержимое, даже если единственные назначенные ему узлы — это текстовые узлы, содержащие пробелы, например <example-element> </example-element>. При использовании выражения Lit в качестве дочернего элемента пользовательского элемента при необходимости используйте неотображаемое значение, чтобы отобразилось резервное содержимое слота. Дополнительную информацию см. в разделе Удаление дочернего содержимого.
Доступ к дочерним элементам, назначенным слотам
Для доступа к дочерним элементам, назначенным слотам в теневом корне, можно использовать стандартные методы slot.assignedNodes или slot.assignedElements вместе с событием slotchange.
Например, можно создать геттер для доступа к элементам, назначенным определённому слоту:
get _slottedChildren() {
const slot = this.shadowRoot.querySelector('slot');
return slot.assignedElements({flatten: true});
}
Также можно использовать событие slotchange, чтобы реагировать на изменение назначенных узлов. В следующем примере извлекается текстовое содержимое всех дочерних элементов, назначенных слотам.
handleSlotchange(e) {
const childNodes = e.target.assignedNodes({flatten: true});
// ... do something with childNodes ...
this.allText = childNodes.map((node) => {
return node.textContent ? node.textContent : ''
}).join('');
}
render() {
return html`<slot @slotchange=${this.handleSlotchange}></slot>`;
}
Дополнительную информацию см. в разделе HTMLSlotElement на MDN.
Декораторы @queryAssignedElements и @queryAssignedNodes
Декораторы @queryAssignedElements и @queryAssignedNodes преобразуют свойство класса в геттер, который возвращает результат вызова slot.assignedElements или slot.assignedNodes соответственно для заданного слота в теневом дереве компонента. Используйте их, чтобы получить элементы или узлы, назначенные указанному слоту.
Оба декоратора принимают необязательный объект со следующими свойствами:
| Свойство | Описание |
|---|---|
flatten |
Логическое значение, указывающее, нужно ли уплощать назначенные узлы, заменяя любые дочерние элементы <slot> назначенными им узлами. |
slot |
Имя слота, для которого выполняется запрос. Не указывайте значение, чтобы выбрать слот по умолчанию. |
selector (только для queryAssignedElements) |
Если указано, возвращаются только назначенные элементы, соответствующие этому селектору CSS. |
Выбор декоратора зависит от того, нужно ли вам запрашивать текстовые узлы, назначенные слоту, или только узлы-элементы. Это зависит от конкретного случая использования.
Использование декораторов. Декораторы — это предлагаемая возможность JavaScript, поэтому для их использования понадобится компилятор, например Babel или TypeScript. Подробности см. в разделе Использование декораторов.
@queryAssignedElements({slot: 'list', selector: '.item'})
_listItems!: Array<HTMLElement>;
@queryAssignedNodes({slot: 'header', flatten: true})
_headerNodes!: Array<Node>;
Приведённые выше примеры эквивалентны следующему коду:
get _listItems() {
const slot = this.shadowRoot.querySelector('slot[name=list]');
return slot.assignedElements().filter((node) => node.matches('.item'));
}
get _headerNodes() {
const slot = this.shadowRoot.querySelector('slot[name=header]');
return slot.assignedNodes({flatten: true});
}
Настройка корня рендеринга
У каждого компонента Lit есть корень рендеринга — узел DOM, служащий контейнером для его внутреннего DOM.
По умолчанию LitElement создаёт открытый shadowRoot и выполняет рендеринг внутри него, формируя следующую структуру DOM:
<my-element>
#shadow-root
<p>child 1</p>
<p>child 2</p>
Есть два способа настроить корень рендеринга, используемый LitElement:
- Задать
shadowRootOptions. - Реализовать метод
createRenderRoot.
Задание shadowRootOptions
Самый простой способ настроить корень рендеринга — задать статическое свойство shadowRootOptions. Реализация createRenderRoot по умолчанию передаёт shadowRootOptions в качестве аргумента options методу attachShadow при создании теневого корня компонента. Это свойство можно задать, чтобы настроить любые параметры, допустимые в словаре ShadowRootInit, например mode и delegatesFocus.
class DelegatesFocus extends LitElement {
static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
}
Дополнительную информацию см. в разделе Element.attachShadow() на MDN.
Реализация createRenderRoot
Реализация createRenderRoot по умолчанию создаёт открытый теневой корень и добавляет в него стили, заданные в поле класса static styles. Дополнительную информацию о стилях см. в разделе Стили.
Чтобы настроить корень рендеринга компонента, реализуйте createRenderRoot и верните узел, в который нужно выполнить рендеринг шаблона.
Например, чтобы выполнять рендеринг шаблона в основном дереве DOM в качестве дочерних элементов вашего элемента, реализуйте createRenderRoot и верните this.
Рендеринг в дочерние элементы. Обычно не рекомендуется выполнять рендеринг в дочерние элементы, а не в теневой DOM. Ваш элемент не будет иметь доступа к области видимости DOM и стилей и не сможет компоновать элементы во внутреннем DOM.
© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/v2/components/shadow-dom/