aboutsummaryrefslogtreecommitdiff
path: root/files/ru/web/web_components/using_custom_elements/index.html
blob: bcb5b5d7333bd966a4657ae1da437f9ad9f9f9df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
---
title: Использование пользовательских элементов
slug: Web/Web_Components/Использование_пользовательских_элементов
translation_of: Web/Web_Components/Using_custom_elements
---
<div>{{DefaultAPISidebar("Web Components")}}</div>

<p class="summary">Одна из ключевых особенностей стандарта Веб-компонент это возможность создавать пользовательские элементы на HTML-странице, инкапсулирующие функциональность, вместо того чтобы создавать длинyю, вложенную группу элементов, которые бы вместе реализовывали нужную пользовательскую фичу. Эта статья является вводной по использованию пользовательских HTML-компонент.</p>

<div class="note">
<p><strong>Заметка</strong>: Пользовательские элементы поддерживаются по умолчанию в Firefox, Chrome и Opera. Safari пока поддерживает только автономные пользовательские компоненты, Edge также работает над реализацией.</p>
</div>

<h2 id="Высокоуровневый_обзор">Высокоуровневый обзор</h2>

<p>Контроллером пользовательских элементов веб-документа является объект {{domxref("CustomElementRegistry")}} — этот элемент позволяет регистрировать пользовательские элементы на веб-странице, возвращает информацию о зарегистрированных элементах и т.п.</p>

<p>Чтобы зарегистрировать пользовательский элемент на странице, используйте метод {{domxref("CustomElementRegistry.define()")}} method. Он принимает аргументы:</p>

<ul>
 <li>{{domxref("DOMString")}} - имя элемента. Обратите внимание, что в именах пользовательских элементах <a href="https://stackoverflow.com/questions/22545621/do-custom-elements-require-a-dash-in-their-name">должен содержаться дефис</a>; они не могут состоять только из одного слова.</li>
 <li>Объект типа <a href="/en-US/docs/Web/JavaScript/Reference/Classes">class</a>, определяющий поведение элемента.</li>
 <li>Опционально объект options, имеющий свойство <code>extends</code>, соответствующее встроенному элементу, от которого наследует объект.</li>
</ul>

<p>К примеру, мы можем определить пользовательский элемент <a href="https://mdn.github.io/web-components-examples/word-count-web-component/">word-count element</a>:</p>

<pre class="brush: js">customElements.define('word-count', WordCount, { extends: 'p' });</pre>

<p>Этот элемент называется <code>word-count</code>, объект соответствующего класса называется <code>WordCount</code>, и он наследует элементу {{htmlelement("p")}}.</p>

<p>Объект класса пользовательского элемента определяется с помощью синтаксиса классов ES 2015. Например, <code>WordCount</code> имеют следующую структуру:</p>

<pre class="brush: js">class WordCount extends HTMLParagraphElement {
  constructor() {
    // Всегда первым делом вызывайте super() в конструкторе
    super();

    // Далее пишется функциональность элемента

    ...
  }
}</pre>

<p>Это простой пример, но его можно дополнить. Можно определить специальные lifecycle callbacks, которые будут вызваны в определенные моменты жизненного цикла элемента. Например, <code>connectedCallback</code> будет вызван, когда пользовательский элемент оказывается впервые встроен в DOM, а <code>attributeChangedCallback</code> вызывается, когда пользовательскому элементу добавляют, удаляют или изменяют какой-то аттрибут.</p>

<p>Подробнее об этом в секции {{anch("Using the lifecycle callbacks")}} ниже.</p>

<p>Есть два типа пользовательских элементов:</p>

<ul>
 <li><strong>Автономные пользователькие элементы</strong> независимы — они не наследуют встроенным HTML-элементам. Их используют на странице просто как обычный HTML-элемент. Например, <code>&lt;popup-info&gt;</code> или <code>document.createElement("popup-info")</code>.</li>
 <li><strong>Модифицированные встроенные элементы</strong> наследуют базовым HTML-элементам. Чтобы создать один из них, вы определяете элемент, от которого они унаследованы (как в примерах выше), и используете их как обычный базовый элемент, но с добавлением атрибута/свойства с именем пользовательского элемента {{htmlattrxref("is")}}. Например <code>&lt;p is="word-count"&gt;</code>, или <code>document.createElement("p", { is: "word-count" })</code>.</li>
</ul>

<h2 id="Разбор_простых_примеров">Разбор простых примеров</h2>

<p>А сейчас давайте разберем еще несколько простых примеров, иллюстрирующих подробности создания пользовательских элементов.</p>

<h3 id="Автономные_пользовательские_элементы">Автономные пользовательские элементы</h3>

<p>Рассмотрим пример автономного пользовательского элемента — <code><a href="https://github.com/mdn/web-components-examples/tree/master/popup-info-box-web-component">&lt;popup-info-box&gt;</a></code> (см. <a href="https://mdn.github.io/web-components-examples/popup-info-box-web-component/">работающий пример</a>). Он содержит изображение и строку, и встраивает изображение в страницу. Когда на изображение наводят фокус, компонент показывает всплывающую подсказку с текстом.</p>

<p>Прежде всего файл JavaScript определяет класс <code>PopUpInfo</code>, наследующий от {{domxref("HTMLElement")}}. Автономные пользовательские элементы почти всегда наследуют <code>HTMLElement</code>.</p>

<pre class="brush: js">class PopUpInfo extends HTMLElement {
  constructor() {
    // Всегда первым делом вызывайте super() в конструкторе
    super();

    // далее следует функциональность элемента

    ...
  }
}</pre>

<p>В этом фрагменте кода содержится определение конструктора {{jsxref("Classes.constructor","constructor")}} класса, которое всегда начинается с вызова <code><a href="/en-US/docs/Web/JavaScript/Reference/Operators/super">super()</a></code> чтобы отработала цепочка прототипного наследования.</p>

<p>Внутри конструктора мы определяем всю функциональность, которую получит элемент при создании его объекта. В данном случае мы добавляем shadow root к пользовательскому элементу, производим манипуляции с DOM, чтобы создать определенную структуру shadow DOM внутри элемента — которая затем присоединяется к shadow root — и наконец добавляем CSS к shadow root, чтобы задать его стиль.</p>

<pre class="brush: js">// Создание shadow root
var shadow = this.attachShadow({mode: 'open'});

// Создание spans
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');

// Берем содержимое атрибута и добавляем его в span
var text = this.getAttribute('text');
info.textContent = text;

// Вставляем иконку
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

// Создаем CSS для shadow dom
var style = document.createElement('style');

style.textContent = '.wrapper {' +
// CSS truncated for brevity

// добавляем созданные элементы к shadow dom

shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);</pre>

<p>Наконец, регистрируем пользовательский элемент в <code>CustomElementRegistry</code> с помощью метода <code>define()</code>, который упоминался ранее — в качестве параметров мы передаем ему имя элемента и имя класса, который содержит его функциональность:</p>

<pre class="brush: js">customElements.define('popup-info', PopUpInfo);</pre>

<p>Теперь он доступен для использования на нашей странице. В HTML мы используем его так:</p>

<pre class="brush: html">&lt;popup-info img="img/alt.png" text="Код валидации вашей карты (CVC)
  это дополнительная мера безопасности — это последние 3 или 4 цифры
  на обороте вашей карты."&gt;</pre>

<div class="note">
<p><strong>Замечение</strong>: Вы можете прочитать <a href="https://github.com/mdn/web-components-examples/blob/master/popup-info-box-web-component/main.js">полный исходный код на JavaScript</a> здесь.</p>
</div>

<h3 id="Модифицированные_встроенные_элементы">Модифицированные встроенные элементы</h3>

<p>Тепреь давайте взглянем на другой пример модифицированного пользовательского элемента — <a href="https://github.com/mdn/web-components-examples/tree/master/expanding-list-web-component">раскрывающийся список</a> (<a href="https://mdn.github.io/web-components-examples/expanding-list-web-component/">см. действующий пример</a>). Он превращает любой ненумерованный список в раскрывающееся/складывающееся меню.</p>

<p>Первым делом определим класс элемента наподобие того, как это делалось выше:</p>

<pre class="brush: js">class ExpandingList extends HTMLUListElement {
  constructor() {
    // Всегда первым делом вызываем super() в конструкторе
    super();

    // ниже следует функциональность элемента

    ...
  }
}</pre>

<p>Здесь мы не будем во всех подробностях описывать функциональность элемента, вы можете понять как он работает, посмотрев исходный код. Единственное принципиальное различие с предыдующим примером состоит в том, что мы используем интерфейс {{domxref("HTMLUListElement")}}, а не {{domxref("HTMLElement")}}. Так что у него есть все характеристики элемента {{htmlelement("ul")}}, плюс дополнительная функциональность, которую определили мы. Это и отличает модифицированный встроенный элемент от автономного пользовательского элемента.</p>

<p>Далее мы регистрируем этот элемент с помощью метода <code>define()</code> как в прошлом примере, только на сей раз мы добавляем объект options, который определяет, какому встроенному элементу наследует данный:</p>

<pre class="brush: js">customElements.define('expanding-list', ExpandingList, { extends: "ul" });</pre>

<p>Встроенный элемент используется на веб-странице немного по-другому:</p>

<pre class="brush: html">&lt;ul is="expanding-list"&gt;

  ...

&lt;/ul&gt;</pre>

<p>Вы задаете элемент <code>&lt;ul&gt;</code> как обычно, но указываете имя модифицированного элемента в атрибуте <code>is</code>.</p>

<div class="note">
<p><strong>Замечание</strong>: Полный <a href="https://github.com/mdn/web-components-examples/blob/master/expanding-list-web-component/main.js">исходный код на JavaScript</a> доступен здесь.</p>
</div>

<h2 id="Использование_lifecycle_callbacks">Использование lifecycle callbacks</h2>

<p>Вы можете определить несколько разных коллбеков в конструкторе пользовательских элементов, которые сработают на разных этапах жизненного цикла элемента:</p>

<ul>
 <li><code>connectedCallback</code>: Срабатывает, когда пользовательский элемент впервые добавляется в DOM.</li>
 <li><code>disconnectedCallback</code>: Срабатывает, когда пользовательский элемент удаляется из DOM.</li>
 <li><code>adoptedCallback</code>: Срабатывает, когда пользовательский элемент перемещен в новый документ.</li>
 <li><code>attributeChangedCallback</code>: Срабатывает, когда пользовательскому элементу добавляют, удаляют или изменяют атрибут.</li>
</ul>

<p>Посмотрим на них в действии. Код ниже взят из примера <a href="https://github.com/mdn/web-components-examples/tree/master/life-cycle-callbacks">life-cycle-callbacks</a> (<a href="https://mdn.github.io/web-components-examples/life-cycle-callbacks/">см. его в действии</a>). Это тривиальный пример, создающий на странице цветной квадрат. Вот как выглядит код пользовательского элемента:</p>

<pre class="brush: html">&lt;custom-square l="100" c="red"&gt;&lt;/custom-square&gt;</pre>

<p>Конструктор класса очень простой — мы просто добавляем shadow DOM к элементу, а затем добавляем пустые элементы {{htmlelement("div")}} и {{htmlelement("style")}} к shadow root:</p>

<pre class="brush: js">var shadow = this.attachShadow({mode: 'open'});

var div = document.createElement('div');
var style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);</pre>

<p>Наиболее важная функция в этом примере <code>updateStyle()</code> — она принимает элемент, находит его shadow root, находит его элемент <code>&lt;style&gt;</code>, и добавляет {{cssxref("width")}}, {{cssxref("height")}}, и {{cssxref("background-color")}} к стилям.</p>

<pre class="brush: js">function updateStyle(elem) {
  var shadow = elem.shadowRoot;
  var childNodes = shadow.childNodes;
  for(var i = 0; i &lt; childNodes.length; i++) {
    if(childNodes[i].nodeName === 'STYLE') {
      childNodes[i].textContent = 'div {' +
                          ' width: ' + elem.getAttribute('l') + 'px;' +
                          ' height: ' + elem.getAttribute('l') + 'px;' +
                          ' background-color: ' + elem.getAttribute('c');
    }
  }
}</pre>

<p>Сами изменения стилей обрабатываются коллбеками жизненного цикла, находящимися внутри конструктора. <code>connectedCallback()</code> срабатывает, когда элемент встраивается в DOM — здесь мы запускаем функцию <code>updateStyle()</code> которая обсеспечивает, чтобы квадрат имел стиль, описанный в его атрибутах:</p>

<pre class="brush: js">connectedCallback() {
  console.log('Пользовательский элемент квадрат добавлен на страницу.');
  updateStyle(this);
}</pre>

<p>коллбеки <code>disconnectedCallback()</code> и <code>adoptedCallback()</code> логируют простые сообщения на консоль, которые уведомляют нас, что элемент удален из DOM или перемещен на другую страницу:</p>

<pre class="brush: js">disconnectedCallback() {
  console.log('Пользователский элемент квадрат удален.');
}

adoptedCallback() {
  console.log('Пользовательский элемент квадарат перемещен на другую страницу.');
}</pre>

<p>Коллбек <code>attributeChangedCallback()</code> запускается когда один из аттрибутов элемента меняется. Как видно из его свойств, можно воздействовать на индивидуальные аттрибуты, глядя на их имена, и новые и старые значения аттрибутов. В данном случае, однако, мы просто снова запускаем функцию <code>updateStyle()</code> чтобы убедиться, что атрибуты квадрата получили новые значения:</p>

<pre class="brush: js">attributeChangedCallback(name, oldValue, newValue) {
  console.log('Атрибуты пользовательского элемента квадрат изменились.');
  updateStyle(this);
}</pre>

<p>Обратите внимание, что нужно наблюдать за атрибутами, чтобы запустить коллбек <code>attributeChangedCallback()</code> когда они изменятся. Это делается через вызов геттера <code>observedAttributes()</code> в конструкторе, который содержит оператор <code>return</code> возвращающий массив с именами атрибутов, которые вы хотите наблюдать:</p>

<pre class="brush: js">static get observedAttributes() {return ['w', 'l']; }</pre>

<p>В нашем случае он расположен в начале конструктора.</p>

<div class="note">
<p><strong>Замечение</strong>: Смотрите <a href="https://github.com/mdn/web-components-examples/blob/master/life-cycle-callbacks/main.js">полный исходный код на JavaScript</a> здесь.</p>
</div>