Шаблоны
Добавьте шаблон в компонент, чтобы определить внутреннюю DOM-структуру для реализации компонента.
Чтобы инкапсулировать DOM, создаваемый шаблоном, LitElement использует теневой DOM. Теневой DOM обеспечивает три преимущества:
- Изоляция DOM. DOM API, такие как
document.querySelector, не находят элементы в теневом DOM компонента, поэтому глобальным скриптам сложнее случайно нарушить работу компонента. - Изоляция стилей. Вы можете писать инкапсулированные стили для теневого DOM, которые не влияют на остальную часть дерева DOM.
- Композиция. Теневой DOM компонента (управляемый компонентом) отделён от дочерних элементов компонента. Вы можете выбирать, как дочерние элементы будут отображаться в DOM, созданном шаблоном. Пользователи компонента могут добавлять и удалять дочерние элементы с помощью стандартных DOM API, не нарушая при этом работу теневого DOM.
Если нативный теневой DOM недоступен, LitElement использует полифил Shady CSS.
Определение и отображение шаблона
Чтобы определить шаблон для компонента LitElement, напишите функцию render в классе элемента:
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
render() {
return html`<p>template content</p>`;
}
}
Напишите шаблон на HTML внутри JavaScript-строкового литерала template literal, заключив исходный HTML в обратные кавычки (
``).Добавьте к строковому литералу шаблона функцию-тег
html.Метод
renderкомпонента может возвращать всё, что способен отобразить lit-html. Обычно он возвращает один объектTemplateResult(тот же тип, который возвращает функция-тегhtml).
Пример
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
// Implement `render` to define a template for your element.
render(){
/**
* Return a lit-html `TemplateResult`.
*
* To create a `TemplateResult`, tag a JavaScript template literal
* with the `html` helper function.
*/
return html`
<div>
<p>A paragraph</p>
</div>
`;
}
}
customElements.define('my-element', MyElement);
LitElement использует шаблоны lit-html; на этой странице описаны их основные возможности. Подробнее см. разделы Создание шаблонов и Справочник по синтаксису шаблонов в документации lit-html.
Создание производительного шаблона
LitElement выполняет отображение и повторное отображение асинхронно, обновляясь в ответ на объединённые изменения свойств (подробнее см. раздел Жизненный цикл обновления элемента).
Во время обновления повторно отображаются только изменившиеся части DOM. Чтобы воспользоваться преимуществами производительности этой модели, проектируйте шаблон элемента как чистую функцию его свойств.
Для этого убедитесь, что функция render:
- Не изменяет состояние элемента.
- Не имеет побочных эффектов.
- Зависит только от свойств элемента.
- Возвращает одинаковый результат при одинаковых значениях свойств.
Также избегайте обновления DOM за пределами render. Вместо этого задавайте шаблон элемента как функцию его состояния и храните состояние в свойствах.
В следующем коде используется неэффективная обработка DOM:
dom-manip.js
// Anti-pattern. Avoid!
constructor() {
super();
this.addEventListener('stuff-loaded', (e) => {
this.shadowRoot.getElementById('message').innerHTML=e.detail;
});
this.loadStuff();
}
render() {
return html`
<p id="message">Loading</p>
`;
}
Мы можем улучшить шаблон, объявив сообщение как свойство и связав это свойство с шаблоном. Объявление свойства указывает компоненту повторно отображать шаблон при изменении свойства.
update-properties.js
static get properties() {
return {
message: {type: String}
}
}
constructor() {
super();
this.message = 'Loading';
this.addEventListener('stuff-loaded', (e) => { this.message = e.detail } );
this.loadStuff();
}
render() {
return html`
<p>${this.message}</p>
`;
}
В следующих разделах рассматриваются различные типы привязок свойств. Информацию об объявлении свойств см. в разделе Свойства.
Использование свойств, циклов и условных выражений в шаблоне
При создании шаблона элемента можно привязать свойства элемента к шаблону; шаблон будет повторно отображаться при каждом изменении свойств.
Свойства
Чтобы добавить значение свойства в шаблон, вставьте его с помощью ${this.propName}:
static get properties() {
return {
myProp: {type: String}
};
}
...
render() {
return html`<p>${this.myProp}</p>`;
}
Циклы
Переберите массив:
html`<ul>
${this.myArray.map(i => html`<li>${i}</li>`)}
</ul>`;
Директива repeat. В большинстве случаев Array.map — наиболее эффективный способ создать повторяющийся шаблон. В некоторых случаях стоит рассмотреть директиву repeat из lit-html, особенно если повторяющиеся элементы хранят состояние или их повторное создание требует значительных ресурсов. Подробнее см. раздел Повторяющиеся шаблоны с директивой repeat в документации lit-html.
Условные выражения
Отобразите содержимое в зависимости от логического условия:
html`
${this.myBool?
html`<p>Render some HTML if myBool is true</p>`:
html`<p>Render some other HTML if myBool is false</p>`}
`;
Примеры
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
myString: { type: String },
myArray: { type: Array },
myBool: { type: Boolean }
};
}
constructor() {
super();
this.myString = 'Hello World';
this.myArray = ['an','array','of','test','data'];
this.myBool = true;
}
render() {
return html`
<p>${this.myString}</p>
<ul>
${this.myArray.map(i => html`<li>${i}</li>`)}
</ul>
${this.myBool?
html`<p>Render some HTML if myBool is true</p>`:
html`<p>Render some other HTML if myBool is false</p>`}
`;
}
}
customElements.define('my-element', MyElement);
Привязка свойств к элементам шаблона
В качестве заполнителей для текстового содержимого HTML, атрибутов, логических атрибутов, свойств и обработчиков событий можно вставлять выражения JavaScript.
- Текстовое содержимое:
<p>${...}</p> - Атрибут:
<p id="${...}"></p> - Логический атрибут:
?disabled="${...}" - Свойство:
.value="${...}" - Обработчик события:
@event="${...}"
Выражения JavaScript могут включать свойства элемента. LitElement отслеживает изменения свойств и реагирует на них, поэтому шаблоны обновляются автоматически.
Привязки данных всегда однонаправленные (от родителя к дочернему элементу). Чтобы передать данные от дочернего элемента родителю, отправьте событие и сохраните нужные данные в свойстве detail.
Привязка к текстовому содержимому
Привяжите prop1 к текстовому содержимому:
html`<div>${this.prop1}</div>`
Привязка к атрибуту
Привяжите prop2 к атрибуту:
html`<div id="${this.prop2}"></div>`
Значения атрибутов всегда являются строками, поэтому привязка к атрибуту должна возвращать значение, которое можно преобразовать в строку.
Привязка к логическому атрибуту
Привяжите prop3 к логическому атрибуту:
html`<input type="text" ?disabled="${this.prop3}">`
Логические атрибуты добавляются, если выражение вычисляется в истинное значение, и удаляются, если оно вычисляется в ложное значение.
Привязка к свойству
Привяжите prop4 к свойству:
html`<input type="checkbox" .value="${this.prop4}"/>`
Привязка к обработчику события
Привяжите clickHandler к событию click:
html`<button @click="${this.clickHandler}">pie?</button>`
Контекст события по умолчанию для выражений @event — это this, поэтому привязывать функцию-обработчик не нужно.
Примеры
my-element.js
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
prop1: {type: String},
prop2: {type: String},
prop3: {type: Boolean},
prop4: {type: String}
};
}
constructor() {
super();
this.prop1 = 'text binding';
this.prop2 = 'mydiv';
this.prop3 = true;
this.prop4 = 'pie';
}
render() {
return html`
<!-- text binding -->
<div>${this.prop1}</div>
<!-- attribute binding -->
<div id="${this.prop2}">attribute binding</div>
<!-- boolean attribute binding -->
<div>
boolean attribute binding
<input type="text" ?disabled="${this.prop3}"/>
</div>
<!-- property binding -->
<div>
property binding
<input type="text" .value="${this.prop4}"/>
</div>
<!-- event handler binding -->
<div>event handler binding
<button @click="${this.clickHandler}">click</button>
</div>
`;
}
clickHandler(e) {
console.log(e.target);
}
}
customElements.define('my-element', MyElement);
Отображение дочерних элементов с помощью элемента slot
Компонент может принимать дочерние элементы (например, элемент <ul> может содержать дочерние элементы <li>).
<my-element> <p>A child</p> </my-element>
По умолчанию, если у элемента есть теневое дерево, его дочерние элементы вообще не отображаются.
Чтобы отображать дочерние элементы, шаблон должен содержать один или несколько элементов <slot>, которые служат заполнителями для дочерних узлов.
Поиск распределённых дочерних элементов. Если компоненту нужна информация о распределённых дочерних элементах, см. раздел Доступ к дочерним элементам в слотах.
Использование элемента slot
Чтобы отобразить дочерние элементы элемента, создайте для них <slot> в шаблоне элемента. Например:
render(){
return html`
<div>
<slot></slot>
</div>
`;
}
Теперь дочерние элементы будут отображаться в <slot>:
<my-element> <p>Render me</p> </my-element>
Дочерние элементы не перемещаются в дереве DOM, но отображаются так, как если бы они были дочерними элементами <slot>.
В один слот можно поместить произвольное количество дочерних элементов:
<my-element> <p>Render me</p> <p>Me too</p> <p>Me three</p> </my-element>
Использование именованных слотов
Чтобы назначить дочерний элемент определённому слоту, убедитесь, что атрибут slot дочернего элемента совпадает с атрибутом name слота:
render(){
return html`
<div>
<slot name="one"></slot>
</div>
`;
}
index.html
<my-element> <p slot="one">Include me in slot "one".</p> </my-element>
-
Именованные слоты принимают только дочерние элементы с соответствующим атрибутом
slot.Например,
<slot name="one"></slot>принимает только дочерние элементы с атрибутомslot="one". -
Дочерние элементы с атрибутом
slotотображаются только в слоте с соответствующим атрибутомname.Например,
<p slot="one">...</p>будет помещён только в<slot name="one"></slot>.
Примеры
my-element.js
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
render(){
return html`
<div>
<slot name="one"></slot>
<slot name="two"></slot>
</div>
`;
}
}
customElements.define('my-element', MyElement);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script type="module" src="./my-element.js"></script>
<title>lit-element code sample</title>
</head>
<body>
<!-- Assign child to a specific slot -->
<my-element>
<p slot="two">Include me in slot "two".</p>
</my-element>
<!--
Named slots only accept children with a matching `slot` attribute.
Children with a `slot` attribute can only go into a slot with a matching name.
-->
<my-element>
<p slot="one">Include me in slot "one".</p>
<p slot="nope">This one will not render at all.</p>
<p>No default slot, so this one won't render either.</p>
</my-element>
</body>
</html>
Для выбора слотов используйте name, а не id.
Обратите внимание: атрибут id элемента slot не влияет на результат!
my-element.js
render(){
return html`
<div>
<slot id="one"></slot>
</div>
`;
}
index.html
<my-element> <p slot="one">nope.</p> <p>ohai..</p> </my-element>
Составление шаблона из других шаблонов
Вы можете составлять шаблоны LitElement из других шаблонов LitElement. В следующем примере мы составляем шаблон элемента с именем <my-page> из небольших шаблонов для заголовка страницы, нижнего колонтитула и основного содержимого:
function headerTemplate(title) {
return html`<header>${title}</header>`;
}
function articleTemplate(text) {
return html`<article>${text}</article>`;
}
function footerTemplate() {
return html`<footer>Your footer here.</footer>`;
}
class MyPage extends LitElement {
...
render() {
return html`
${headerTemplate(this.article.title)}
${articleTemplate(this.article.text)}
${footerTemplate()}
`;
}
}
Шаблоны также можно составлять, импортируя другие элементы и используя их в шаблоне:
import './my-header.js';
import './my-article.js';
import './my-footer.js';
class MyPage extends LitElement {
render() {
return html`
<my-header></my-header>
<my-article></my-article>
<my-footer></my-footer>
`;
}
}
Указание корневого узла отображения
Узел, в который будет отображаться шаблон компонента, называется корневым узлом отображения.
По умолчанию LitElement создаёт открытый shadowRoot и отображает содержимое внутри него, создавая следующую структуру DOM:
<my-element>
#shadow-root
<p>child 1</p>
<p>child 2</p>
Чтобы настроить корневой узел отображения компонента, реализуйте createRenderRoot и верните узел, в который нужно отображать шаблон.
Например, чтобы отобразить шаблон в основном дереве DOM как дочерние элементы вашего элемента:
<my-element> <p>child 1</p> <p>child 2</p>
Реализуйте createRenderRoot и верните this:
class LightDom extends LitElement {
render() {
return html`
<p>This template renders without shadow DOM.</p>
`;
}
createRenderRoot() {
/**
* Render template without shadow DOM. Note that shadow DOM features like
* encapsulated CSS and slots are unavailable.
*/
return this;
}
}
Краткая справка по синтаксису шаблонов
Отображение
render() { return html`<p>template</p>`; }
Свойства, циклы, условные выражения
// Property
html`<p>${this.myProp}</p>`;
// Loop
html`${this.myArray.map(i => html`<li>${i}</li>`)}`;
// Conditional
html`${this.myBool?html`<p>foo</p>`:html`<p>bar</p>`}`;
Привязки данных
// Attribute
html`<p id="${...}">`;
// Boolean attribute
html`<input type="text" ?disabled="${...}">`;
// Property
html`<input .value="${...}">`;
// Event handler
html`<button @click="${this.doStuff}"></button>`;
Композиция
// From multiple templates on same class
render() {
return html`
${this.headerTemplate}
<article>article</article>
`;
}
get headerTemplate() {
return html`<header>header</header>`;
}
// By importing elements
import './my-header.js';
class MyPage extends LitElement{
render() {
return html`
<my-header></my-header>
<article>article</article>
`;
}
}
Слоты
render() { return html`<slot name="thing"></slot>`; }
<my-element> <p slot="thing">stuff</p> </my-element>
Использование других возможностей lit-html
Поскольку LitElement использует функцию-тег html из lit-html для определения шаблонов, вы можете использовать весь набор возможностей lit-html при их создании. В него входят директивы lit-html — специальные функции, настраивающие способ отображения привязки в lit-html.
Чтобы напрямую импортировать возможности из lit-html, добавьте lit-html в проект как прямую зависимость. Рекомендуем использовать максимально широкий практически применимый диапазон версий lit-html, чтобы снизить вероятность установки npm двух разных версий lit-html:
npm i lit-element@^2.0.0 npm i lit-html@^1.0.0
Импорт и использование директивы lit-html
Вы можете импортировать и использовать директиву lit-html, как показано в документации lit-html.
import { LitElement, html } from 'lit-element';
import { until } from 'lit-html/directives/until.js';
const content = fetch('./content.txt').then(r => r.text());
html`${until(content, html`<span>Loading...</span>`)}`
Список директив, поставляемых с lit-html, см. в разделе Встроенные директивы справочника по синтаксису шаблонов.
Доступ к узлам теневого DOM
Результат метода render() обычно отображается в теневом DOM, поэтому узлы не являются непосредственными дочерними элементами компонента. Используйте this.shadowRoot.querySelector() или this.shadowRoot.querySelectorAll(), чтобы найти узлы в теневом DOM.
Можно выполнить поиск в DOM, созданном шаблоном, после его первоначального отображения (например, в firstUpdated) или использовать шаблон с геттером, например:
get _closeButton() {
return this.shadowRoot.querySelector('#close-button');
}
LitElement предоставляет набор декораторов, позволяющих кратко определять подобные геттеры.
Дополнительная информация:
- Element.querySelector() на MDN.
- Element.querySelectorAll() на MDN.
Декораторы @query, @queryAll и @queryAsync
Декораторы @query, @queryAll и @queryAsync предоставляют удобный способ доступа к узлам в корневом узле теневого DOM компонента.
Использование декораторов. Декораторы — предлагаемая возможность JavaScript, поэтому для их использования понадобится компилятор, например Babel или TypeScript. Подробнее см. раздел Использование декораторов.
Декоратор @query изменяет свойство класса, превращая его в геттер, который возвращает узел из корневого узла отображения. Необязательный второй аргумент — флаг кэширования: если он имеет значение true, запрос к DOM выполняется только один раз, а результат кэшируется. Это можно использовать для повышения производительности, если ожидается, что запрашиваемый узел не будет меняться.
import {LitElement, html} from 'lit-element';
import {query} from 'lit-element/lib/decorators.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');
}
shadowRoot и renderRoot. Свойство renderRoot указывает контейнер, в который отображается шаблон. По умолчанию это shadowRoot компонента. Декораторы используют renderRoot, поэтому они должны работать корректно, даже если вы переопределите createRenderRoot, как описано в разделе Указание корневого узла отображения.
Декоратор @queryAll идентичен query, но возвращает все соответствующие узлы, а не один. Он эквивалентен вызову querySelectorAll.
import {LitElement, html} from 'lit-element';
import {queryAll} from 'lit-element/lib/decorators.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, может измениться в результате изменения другого свойства.
Доступ к дочерним элементам в слотах
Чтобы получить доступ к дочерним элементам, назначенным слотам в теневом корне, можно использовать стандартный метод slot.assignedNodes и событие slotchange.
Например, можно создать геттер для доступа к назначенным узлам определённого слота:
get _slottedChildren() {
const slot = this.shadowRoot.querySelector('slot');
const childNodes = slot.assignedNodes({flatten: true});
return Array.prototype.filter.call(childNodes, (node) => node.nodeType == Node.ELEMENT_NODE);
}
Также можно использовать событие slotchange, чтобы выполнить действие при изменении назначенных узлов. В следующем примере извлекается текстовое содержимое всех дочерних элементов в слотах.
handleSlotchange(e) {
const childNodes = e.target.assignedNodes({flatten: true});
// ... do something with childNodes ...
this.allText = Array.prototype.map.call(childNodes, (node) => {
return node.textContent ? node.textContent : ''
}).join('');
}
render() {
return html`<slot @slotchange=${this.handleSlotchange}></slot>`;
}
Дополнительная информация:
- HTMLSlotElement на MDN.
Декоратор @queryAssignedNodes
Декоратор @queryAssignedNodes преобразует свойство класса в геттер, возвращающий все узлы, назначенные заданному слоту в теневом дереве компонента. Если необязательный второй аргумент имеет значение true, назначенные узлы уплощаются: любые назначенные узлы, являющиеся элементами slot, заменяются узлами, назначенными им. Необязательный третий аргумент — селектор CSS, отбирающий только соответствующие ему элементы.
Использование декораторов. Декораторы — предлагаемая возможность JavaScript, поэтому для их использования понадобится компилятор, например Babel или TypeScript. Подробнее см. раздел Использование декораторов.
// First argument is the slot name
// Second argument is `true` to flatten the assigned nodes.
@queryAssignedNodes('header', true)
_headerNodes;
// If the first argument is absent or an empty string, list nodes for the default slot.
@queryAssignedNodes()
_defaultSlotNodes;
Первый пример выше эквивалентен следующему коду:
get headerNodes() {
const slot = this.shadowRoot.querySelector('slot[name=header]');
return slot.assignedNodes({flatten: true});
}
В TypeScript тип свойства queryAssignedNodes — NodeListOf<HTMLElement>.
Ресурсы
Дополнительная информация о теневом DOM:
- Shadow DOM v1: автономные веб-компоненты на сайте Web Fundamentals.
- Использование теневого DOM на MDN.
Дополнительная информация о шаблонах lit-html:
© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/v1/components/templates/