draft
|
@ -57,7 +57,7 @@ Even if we press `key:Shift+Enter` to input multiple lines, and put `use strict`
|
|||
|
||||
The reliable way to ensure `use strict` would be to input the code into console like this:
|
||||
|
||||
```
|
||||
```js
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ let i = 0;
|
|||
|
||||
let start = Date.now();
|
||||
|
||||
let timer = setInterval(count, 0);
|
||||
let timer = setInterval(count);
|
||||
|
||||
function count() {
|
||||
|
||||
|
@ -20,4 +20,3 @@ function count() {
|
|||
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function count() {
|
|||
if (i == 1000000000) {
|
||||
alert("Done in " + (Date.now() - start) + 'ms');
|
||||
} else {
|
||||
setTimeout(count, 0);
|
||||
setTimeout(count);
|
||||
}
|
||||
|
||||
// a piece of heavy job
|
||||
|
|
|
@ -15,7 +15,7 @@ These methods are not a part of JavaScript specification. But most environments
|
|||
The syntax:
|
||||
|
||||
```js
|
||||
let timerId = setTimeout(func|code, delay[, arg1, arg2...])
|
||||
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
@ -25,7 +25,7 @@ Parameters:
|
|||
Usually, that's a function. For historical reasons, a string of code can be passed, but that's not recommended.
|
||||
|
||||
`delay`
|
||||
: The delay before run, in milliseconds (1000 ms = 1 second).
|
||||
: The delay before run, in milliseconds (1000 ms = 1 second), by default 0.
|
||||
|
||||
`arg1`, `arg2`...
|
||||
: Arguments for the function (not supported in IE9-)
|
||||
|
@ -110,7 +110,7 @@ For browsers, timers are described in the [timers section](https://www.w3.org/TR
|
|||
The `setInterval` method has the same syntax as `setTimeout`:
|
||||
|
||||
```js
|
||||
let timerId = setInterval(func|code, delay[, arg1, arg2...])
|
||||
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
|
||||
```
|
||||
|
||||
All arguments have the same meaning. But unlike `setTimeout` it runs the function not only once, but regularly after the given interval of time.
|
||||
|
@ -238,7 +238,7 @@ There's a side-effect. A function references the outer lexical environment, so,
|
|||
|
||||
## setTimeout(...,0)
|
||||
|
||||
There's a special use case: `setTimeout(func, 0)`.
|
||||
There's a special use case: `setTimeout(func, 0)`, or just `setTimeout(func)`.
|
||||
|
||||
This schedules the execution of `func` as soon as possible. But scheduler will invoke it only after the current code is complete.
|
||||
|
||||
|
@ -247,7 +247,7 @@ So the function is scheduled to run "right after" the current code. In other wor
|
|||
For instance, this outputs "Hello", then immediately "World":
|
||||
|
||||
```js run
|
||||
setTimeout(() => alert("World"), 0);
|
||||
setTimeout(() => alert("World"));
|
||||
|
||||
alert("Hello");
|
||||
```
|
||||
|
@ -260,7 +260,7 @@ There's a trick to split CPU-hungry tasks using `setTimeout`.
|
|||
|
||||
For instance, a syntax-highlighting script (used to colorize code examples on this page) is quite CPU-heavy. To highlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a big text that takes a lot. It may even cause the browser to "hang", which is unacceptable.
|
||||
|
||||
So we can split the long text into pieces. First 100 lines, then plan another 100 lines using `setTimeout(...,0)`, and so on.
|
||||
So we can split the long text into pieces. First 100 lines, then plan another 100 lines using `setTimeout(..., 0)`, and so on.
|
||||
|
||||
For clarity, let's take a simpler example for consideration. We have a function to count from `1` to `1000000000`.
|
||||
|
||||
|
@ -303,7 +303,7 @@ function count() {
|
|||
if (i == 1e9) {
|
||||
alert("Done in " + (Date.now() - start) + 'ms');
|
||||
} else {
|
||||
setTimeout(count, 0); // schedule the new call (**)
|
||||
setTimeout(count); // schedule the new call (**)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -338,7 +338,7 @@ function count() {
|
|||
|
||||
// move the scheduling at the beginning
|
||||
if (i < 1e9 - 1e6) {
|
||||
setTimeout(count, 0); // schedule the new call
|
||||
setTimeout(count); // schedule the new call
|
||||
}
|
||||
|
||||
do {
|
||||
|
@ -371,8 +371,8 @@ setTimeout(function run() {
|
|||
times.push(Date.now() - start); // remember delay from the previous call
|
||||
|
||||
if (start + 100 < Date.now()) alert(times); // show the delays after 100ms
|
||||
else setTimeout(run, 0); // else re-schedule
|
||||
}, 0);
|
||||
else setTimeout(run); // else re-schedule
|
||||
});
|
||||
|
||||
// an example of the output:
|
||||
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
|
||||
|
@ -430,7 +430,7 @@ And if we use `setTimeout` to split it into pieces then changes are applied in-b
|
|||
} while (i % 1e3 != 0);
|
||||
|
||||
if (i < 1e9) {
|
||||
setTimeout(count, 0);
|
||||
setTimeout(count);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ So, what comprises a component?
|
|||
|
||||
- [Custom elements](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) -- to define custom HTML elements.
|
||||
- [Shadow DOM](https://dom.spec.whatwg.org/#shadow-trees) -- to create an internal DOM for the component, hidden from the others.
|
||||
- [CSS Scoping](https://drafts.csswg.org/css-scoping/) -- to declare styles that only apply inside the component.
|
||||
- [CSS Scoping](https://drafts.csswg.org/css-scoping/) -- to declare styles that only apply inside the Shadow DOM of the component.
|
||||
|
||||
There exist many frameworks and development methodologies that aim to do the similar thing, each one with its own bells and whistles. Usually, special CSS classes and conventions are used to provide "component feel" -- CSS scoping and DOM encapsulation.
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
Please note:
|
||||
1. We clear `setInterval` timer when the element is removed from the document. That's important, otherwise it continues ticking even if not needed any more. And the browser can't clear the memory from this element and referenced by it.
|
||||
2. We can access current date as `elem.date` property. All class methods and properties are naturally element methods and properties.
|
|
@ -0,0 +1,9 @@
|
|||
<!doctype html>
|
||||
<script src="time-formatted.js"></script>
|
||||
<script src="live-timer.js"></script>
|
||||
|
||||
<live-timer id="elem"></live-timer>
|
||||
|
||||
<script>
|
||||
elem.addEventListener('tick', event => console.log(event.detail));
|
||||
</script>
|
|
@ -14,18 +14,18 @@ class TimeFormatted extends HTMLElement {
|
|||
}).format(date);
|
||||
}
|
||||
|
||||
connectedCallback() { // (2)
|
||||
connectedCallback() {
|
||||
if (!this.rendered) {
|
||||
this.render();
|
||||
this.rendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes() { // (3)
|
||||
static get observedAttributes() {
|
||||
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) { // (4)
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<!-- don't modify this -->
|
||||
<script src="time-formatted.js"></script>
|
||||
|
||||
<!-- your code here: -->
|
||||
<script src="live-timer.js"></script>
|
||||
|
||||
<live-timer id="elem"></live-timer>
|
||||
|
||||
<script>
|
||||
elem.addEventListener('tick', event => console.log(event.detail));
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
class LiveTimer extends HTMLElement {
|
||||
|
||||
/* your code here */
|
||||
|
||||
}
|
||||
|
||||
customElements.define("live-timer", LiveTimer);
|
|
@ -0,0 +1,34 @@
|
|||
class TimeFormatted extends HTMLElement {
|
||||
|
||||
render() {
|
||||
let date = new Date(this.getAttribute('datetime') || Date.now());
|
||||
|
||||
this.innerHTML = new Intl.DateTimeFormat("default", {
|
||||
year: this.getAttribute('year') || undefined,
|
||||
month: this.getAttribute('month') || undefined,
|
||||
day: this.getAttribute('day') || undefined,
|
||||
hour: this.getAttribute('hour') || undefined,
|
||||
minute: this.getAttribute('minute') || undefined,
|
||||
second: this.getAttribute('second') || undefined,
|
||||
timeZoneName: this.getAttribute('time-zone-name') || undefined,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.rendered) {
|
||||
this.render();
|
||||
this.rendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define("time-formatted", TimeFormatted);
|
23
8-web-components/2-custom-elements/1-live-timer/task.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
# Live timer element
|
||||
|
||||
We already have `<time-formatted>` element to show a nicely formatted time.
|
||||
|
||||
Create `<live-timer>` element to show the current time:
|
||||
1. It should use `<time-formatted>` internally, not duplicate its functionality.
|
||||
2. Ticks (updates) every second.
|
||||
3. For every tick, a custom event named `tick` should be generated, with the current date in `event.detail` (see chapter <info:dispatch-events>).
|
||||
|
||||
Usage:
|
||||
|
||||
```html
|
||||
<live-timer id="elem"></live-timer>
|
||||
|
||||
<script>
|
||||
elem.addEventListener('tick', event => console.log(event.detail));
|
||||
</script>
|
||||
```
|
||||
|
||||
Demo:
|
||||
|
||||
[iframe src="solution" height=40]
|
|
@ -40,7 +40,7 @@ class MyElement extends HTMLElement {
|
|||
|
||||
adoptedCallback() {
|
||||
// called when the element is moved to a new document
|
||||
// (document.adoptNode call, very rarely used)
|
||||
// (happens in document.adoptNode, very rarely used)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -113,22 +113,20 @@ To track custom elements from JavaScript, there are methods:
|
|||
- `customElements.whenDefined(name)` -- returns a promise that resolves (without value) when a custom element with the given `name` is defined.
|
||||
```
|
||||
|
||||
```smart header="Rendering in `connectedCallback` instead of `constructor`"
|
||||
|
||||
|
||||
```smart header="Rendering in `connectedCallback`, not in `constructor`"
|
||||
In the example above, element content is rendered (created) in `connectedCallback`.
|
||||
|
||||
Why not in the `constructor`?
|
||||
|
||||
First, that might be better performance-wise -- to delay the work until its really needed.
|
||||
The reason is simple: when `constructor` is called, it's yet too early. The element instance is created, but not populated yet. We don't have attributes at this stage: calls to `getAttribute` always return `null`. So we can't really render there.
|
||||
|
||||
Besides, if you think about it, that's be tter performance-wise -- to delay the work until it's really needed.
|
||||
|
||||
The `connectedCallback` triggers when the element is in the document, not just appended to another element as a child. So we can build detached DOM, create elements and prepare them for later use. They will only be actually rendered when they make it into the page.
|
||||
|
||||
Second, when the element is on the page, we can get geometry information about it, e.g. sizes (`elem.offsetWidth/offsetHeight`), so rendering at this stage is more powerful.
|
||||
|
||||
On the other hand, `constructor` is the right place to initialize internal data structures, do lightweight jobs.
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Observing attributes
|
||||
|
||||
Please note that in the current implementation, after the element is rendered, further attribute changes don't have any effect. That's strange for an HTML element. Usually, when we change an attribute, like `a.href`, the change is immediately visible. So let's fix this.
|
||||
|
@ -196,6 +194,89 @@ setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
|
|||
4. ...and re-renders the element.
|
||||
5. At the end, we can easily make a live timer.
|
||||
|
||||
## Rendering order
|
||||
|
||||
When HTML parser builds the DOM, elements are processed one after another, parents before children. E.g. is we have `<outer><inner></inner></outer>`, then `<outer>` is created and connected to DOM first, and then `<inner>`.
|
||||
|
||||
That leads to important side effects for custom elements.
|
||||
|
||||
For example, if a custom element tries to access `innerHTML` in `connectedCallback`, it gets nothing:
|
||||
|
||||
```html run height=40
|
||||
<script>
|
||||
customElements.define('user-info', class extends HTMLElement {
|
||||
|
||||
connectedCallback() {
|
||||
alert(this.innerHTML);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
*!*
|
||||
<user-info>John</user-info>
|
||||
*/!*
|
||||
```
|
||||
|
||||
If you run it, the `alert` is empty.
|
||||
|
||||
That's exactly because there are no children on that stage, the DOM is unfinished yet. So, if we'd like to pass information to custom element, we can use attributes. They are available immediately.
|
||||
|
||||
Or, if we really need the children, we can defer access to them with zero-delay `setTimeout`.
|
||||
|
||||
This works:
|
||||
|
||||
```html run height=40
|
||||
<script>
|
||||
customElements.define('user-info', class extends HTMLElement {
|
||||
|
||||
connectedCallback() {
|
||||
*!*
|
||||
setTimeout(() => alert(this.innerHTML));
|
||||
*/!*
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
*!*
|
||||
<user-info>John</user-info>
|
||||
*/!*
|
||||
```
|
||||
|
||||
Now in `setTimeout` we can get the contents of the element and finish the initialization.
|
||||
|
||||
On the other hand, this solution is also not perfect. If nested custom elements also use `setTimeout` to initialize themselves, then they queue up: the outer `setTimeout` triggers first, and then the inner one.
|
||||
|
||||
So the outer element finishes the initialization before the inner one.
|
||||
|
||||
For example:
|
||||
|
||||
```html run height=0
|
||||
<script>
|
||||
customElements.define('user-info', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
alert(`${this.id} connected.`);
|
||||
setTimeout(() => alert(`${this.id} initialized.`));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
*!*
|
||||
<user-info id="outer">
|
||||
<user-info id="inner"></user-info>
|
||||
</user-info>
|
||||
*/!*
|
||||
```
|
||||
|
||||
Output order:
|
||||
|
||||
1. outer connected.
|
||||
2. inner connected.
|
||||
2. outer initialized.
|
||||
4. inner initialized.
|
||||
|
||||
If we'd like the outer element to wait for inner ones, then there's no built-in reliable solution. But we can invent one. For instance, inner elements can dispatch events like `initialized`, and outer ones can listen and react on them.
|
||||
|
||||
## Customized built-in elements
|
||||
|
||||
|
@ -239,249 +320,6 @@ customElements.define('hello-button', HelloButton, {extends: 'button'}); // 2
|
|||
4. Our buttons extend built-in ones, so they retain the standard features like `disabled` attribute.
|
||||
|
||||
|
||||
|
||||
For example, we can create `<custom-dropdown>` -- a nice-looking dropdown select, `<phone-input>` -- an input element for a phone number, and other graphical components.
|
||||
|
||||
There are two kinds of custom elements:
|
||||
|
||||
|
||||
|
||||
|
||||
An autonomous custom element, which is defined with no extends option. These types of custom elements have a local name equal to their defined name.
|
||||
|
||||
A customized built-in element, which is defined with an extends option. These types of custom elements have a local name equal to the value passed in their extends option, and their defined name is used as the value of the is attribute, which therefore must be a valid custom element name.
|
||||
|
||||
|
||||
|
||||
```js run
|
||||
class DateLocal extends HTMLElement {
|
||||
constructor() {
|
||||
// element is created/upgraded
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
## "tel" example
|
||||
|
||||
|
||||
|
||||
For ins, we'd like to show a date in visitor's time zone.
|
||||
|
||||
The time zone is
|
||||
|
||||
A critical reader may wonder: "Do we need custom HTML elements? We can create interfaces from existing ones".
|
||||
|
||||
|
||||
|
||||
Критично настроенный читатель скажет: "Зачем ещё стандарт для своих типов элементов? Я могу создать любой элемент и прямо сейчас! В любом из современных браузеров можно писать любой HTML, используя свои теги: `<mytag>`. Или создавать элементы из JavaScript при помощи `document.createElement('mytag')`."
|
||||
|
||||
Однако, по умолчанию элемент с нестандартным названием (например `<mytag>`) воспринимается браузером, как нечто неопределённо-непонятное. Ему соответствует класс [HTMLUnknownElement](http://www.w3.org/TR/html5/dom.html#htmlunknownelement), и у него нет каких-либо особых методов.
|
||||
|
||||
**Стандарт Custom Elements позволяет описывать для новых элементов свои свойства, методы, объявлять свой DOM, подобие конструктора и многое другое.**
|
||||
|
||||
Давайте посмотрим это на примерах.
|
||||
|
||||
```warn header="Для примеров рекомендуется Chrome"
|
||||
Так как спецификация не окончательна, то для запуска примеров рекомендуется использовать Google Chrome, лучше -- последнюю сборку [Chrome Canary](https://www.google.ru/chrome/browser/canary.html), в которой, как правило, отражены последние изменения.
|
||||
```
|
||||
|
||||
## Новый элемент
|
||||
|
||||
Для описания нового элемента используется вызов `document.registerElement(имя, { prototype: прототип })`.
|
||||
|
||||
Здесь:
|
||||
|
||||
- `имя` -- имя нового тега, например `"mega-select"`. Оно обязано содержать дефис `"-"`. Спецификация требует дефис, чтобы избежать в будущем конфликтов со стандартными элементами HTML. Нельзя создать элемент `timer` или `myTimer` -- будет ошибка.
|
||||
- `прототип` -- объект-прототип для нового элемента, он должен наследовать от `HTMLElement`, чтобы у элемента были стандартные свойства и методы.
|
||||
|
||||
Вот, к примеру, новый элемент `<my-timer>`:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
*!*
|
||||
// прототип с методами для нового элемента
|
||||
*/!*
|
||||
var MyTimerProto = Object.create(HTMLElement.prototype);
|
||||
MyTimerProto.tick = function() { // *!*свой метод tick*/!*
|
||||
this.innerHTML++;
|
||||
};
|
||||
|
||||
*!*
|
||||
// регистрируем новый элемент в браузере
|
||||
*/!*
|
||||
document.registerElement("my-timer", {
|
||||
prototype: MyTimerProto
|
||||
});
|
||||
</script>
|
||||
|
||||
*!*
|
||||
<!-- теперь используем новый элемент -->
|
||||
*/!*
|
||||
<my-timer id="timer">0</my-timer>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// вызовем метод tick() на элементе
|
||||
*/!*
|
||||
setInterval(function() {
|
||||
timer.tick();
|
||||
}, 1000);
|
||||
</script>
|
||||
```
|
||||
|
||||
Использовать новый элемент в HTML можно и до его объявления через `registerElement`.
|
||||
|
||||
Для этого в браузере предусмотрен специальный режим "обновления" существующих элементов.
|
||||
|
||||
Если браузер видит элемент с неизвестным именем, в котором есть дефис `-` (такие элементы называются "unresolved"), то:
|
||||
|
||||
- Он ставит такому элементу специальный CSS-псевдокласс `:unresolved`, для того, чтобы через CSS можно было показать, что он ещё "не подгрузился".
|
||||
- При вызове `registerElement` такие элементы автоматически обновятся до нужного класса.
|
||||
|
||||
В примере ниже регистрация элемента происходит через 2 секунды после его появления в разметке:
|
||||
|
||||
```html run no-beautify
|
||||
<style>
|
||||
*!*
|
||||
/* стиль для :unresolved элемента (до регистрации) */
|
||||
*/!*
|
||||
hello-world:unresolved {
|
||||
color: white;
|
||||
}
|
||||
|
||||
hello-world {
|
||||
transition: color 3s;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hello-world id="hello">Hello, world!</hello-world>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// регистрация произойдёт через 2 сек
|
||||
*/!*
|
||||
setTimeout(function() {
|
||||
document.registerElement("hello-world", {
|
||||
prototype: {
|
||||
__proto__: HTMLElement.prototype,
|
||||
sayHi: function() { alert('Привет!'); }
|
||||
}
|
||||
});
|
||||
|
||||
*!*
|
||||
// у нового типа элементов есть метод sayHi
|
||||
*/!*
|
||||
hello.sayHi();
|
||||
}, 2000);
|
||||
</script>
|
||||
```
|
||||
|
||||
Можно создавать такие элементы и в JavaScript -- обычным вызовом `createElement`:
|
||||
|
||||
```js
|
||||
var timer = document.createElement('my-timer');
|
||||
```
|
||||
|
||||
## Расширение встроенных элементов
|
||||
|
||||
Выше мы видели пример создания элемента на основе базового `HTMLElement`. Но можно расширить и другие, более конкретные HTML-элементы.
|
||||
|
||||
Для расширения встроенных элементов у `registerElement` предусмотрен параметр `extends`, в котором можно задать, какой тег мы расширяем.
|
||||
|
||||
Например, кнопку:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
var MyTimerProto = Object.create(*!*HTMLButtonElement.prototype*/!*);
|
||||
MyTimerProto.tick = function() {
|
||||
this.innerHTML++;
|
||||
};
|
||||
|
||||
document.registerElement("my-timer", {
|
||||
prototype: MyTimerProto,
|
||||
*!*
|
||||
extends: 'button'
|
||||
*/!*
|
||||
});
|
||||
</script>
|
||||
|
||||
<button *!*is="my-timer"*/!* id="timer">0</button>
|
||||
|
||||
<script>
|
||||
setInterval(function() {
|
||||
timer.tick();
|
||||
}, 1000);
|
||||
|
||||
timer.onclick = function() {
|
||||
alert("Текущее значение: " + this.innerHTML);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Важные детали:
|
||||
|
||||
Прототип теперь наследует не от `HTMLElement`, а от `HTMLButtonElement`
|
||||
: Чтобы расширить элемент, нужно унаследовать прототип от его класса.
|
||||
|
||||
В HTML указывается при помощи атрибута `is="..."`
|
||||
: Это принципиальное отличие разметки от обычного объявления без `extends`. Теперь `<my-timer>` работать не будет, нужно использовать исходный тег и `is`.
|
||||
|
||||
Работают методы, стили и события кнопки.
|
||||
: При клике на кнопку её не отличишь от встроенной. И всё же, это новый элемент, со своими методами, в данном случае `tick`.
|
||||
|
||||
При создании нового элемента в JS, если используется `extends`, необходимо указать и исходный тег в том числе:
|
||||
|
||||
```js
|
||||
var timer = document.createElement("button", "my-timer");
|
||||
```
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
В прототипе своего элемента мы можем задать специальные методы, которые будут вызываться при создании, добавлении и удалении элемента из DOM:
|
||||
|
||||
<table>
|
||||
<tr><td><code>createdCallback</code></td><td>Элемент создан</td></tr>
|
||||
<tr><td><code>attachedCallback</code></td><td>Элемент добавлен в документ</td></tr>
|
||||
<tr><td><code>detachedCallback</code></td><td>Элемент удалён из документа</td></tr>
|
||||
<tr><td><code>attributeChangedCallback(name, prevValue, newValue)</code></td><td>Атрибут добавлен, изменён или удалён</td></tr>
|
||||
</table>
|
||||
|
||||
Как вы, наверняка, заметили, `createdCallback` является подобием конструктора. Он вызывается только при создании элемента, поэтому всю дополнительную инициализацию имеет смысл описывать в нём.
|
||||
|
||||
Давайте используем `createdCallback`, чтобы инициализировать таймер, а `attachedCallback` -- чтобы автоматически запускать таймер при вставке в документ:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
var MyTimerProto = Object.create(HTMLElement.prototype);
|
||||
|
||||
MyTimerProto.tick = function() {
|
||||
this.timer++;
|
||||
this.innerHTML = this.timer;
|
||||
};
|
||||
|
||||
*!*
|
||||
MyTimerProto.createdCallback = function() {
|
||||
this.timer = 0;
|
||||
};
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
MyTimerProto.attachedCallback = function() {
|
||||
setInterval(this.tick.bind(this), 1000);
|
||||
};
|
||||
*/!*
|
||||
|
||||
document.registerElement("my-timer", {
|
||||
prototype: MyTimerProto
|
||||
});
|
||||
</script>
|
||||
|
||||
<my-timer id="timer">0</my-timer>
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Мы рассмотрели, как создавать свои DOM-элементы при помощи стандарта [Custom Elements](http://www.w3.org/TR/custom-elements/).
|
||||
|
|
|
@ -1,6 +1,138 @@
|
|||
# Shadow DOM
|
||||
|
||||
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) является отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
|
||||
Did you ever think how complex browser controls like `<input type="range">` are created and styled?
|
||||
|
||||
To draw this kind of element, the browser uses DOM/CSS internally:
|
||||
|
||||
<input type="range">
|
||||
|
||||
The associated DOM structure is normally hidden from us, but we can see it in developer tools. E.g. in Chrome, we need to enable in Dev Tools "Show user agent shadow DOM" option.
|
||||
|
||||
Then `<input type="range">` looks like this:
|
||||
|
||||

|
||||
|
||||
What you see under `#shadow-root` is called "Shadow DOM".
|
||||
|
||||
We can't get built-in Shadow DOM elements by regular JavaScript calls or selectors. These are not regular children, but a powerful encapsulation technique.
|
||||
|
||||
In the example above, we can see a useful attribute `pseudo`. It's non-standard, exists for historical reasons. We can use it style subelements with CSS, like this:
|
||||
|
||||
```html run autorun
|
||||
<style>
|
||||
/* make the slider track red */
|
||||
input::-webkit-slider-runnable-track {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<input type="range">
|
||||
```
|
||||
|
||||
Once again, `pseudo` is a non-standard attribute. Chronologically, browsers first started to experiment with internal DOM structures, and then, after time, Shadow DOM was standartized to allow us, developers, to do the similar thing.
|
||||
|
||||
Furhter on, we'll use the modern Shadow DOM standard.
|
||||
|
||||
## Shadow tree
|
||||
|
||||
A DOM element can have two types of DOM subtrees:
|
||||
|
||||
1. Light tree -- a regular DOM subtree, made of HTML children.
|
||||
2. Shadow tree -- a hidden DOM subtree, not reflected in HTML, hidden from prying eyes.
|
||||
|
||||
Technically, it's possible for an element to have both at the same time. Then the browser renders only the shadow tree. But usually the element content is either "light" (included into the main DOM/HTML) or "shadowed" (encapsulated from it).
|
||||
|
||||
Shadow tree can be used in Custom Elements to hide internal implementation details.
|
||||
|
||||
For example, this `<show-hello>` element hides its internal DOM in shadow tree:
|
||||
|
||||
```html run autorun height=40
|
||||
<script>
|
||||
customElements.define('show-hello', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({mode: 'open'});
|
||||
shadow.innerHTML = `<p>Hello, ${this.getAttribute('name')}!</p>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<show-hello name="John"></show-hello>
|
||||
```
|
||||
|
||||
The call to `elem.attachShadow({mode: …})` creates a shadow tree root for the element. There are two limitations:
|
||||
1. We can create only one shadow root per element.
|
||||
2. The `elem` must be either a custom element, or one of: "article", "aside", "blockquote", "body", "div", "footer", "h1..h6", "header", "main" "nav", "p", "section", "span".
|
||||
|
||||
The `mode` option sets the encapsulation level. It must have any of two values:
|
||||
- **`"open"`** -- then the shadow root is available as DOM property `elem.shadowRoot`, so any code is able to access it.
|
||||
- **`"closed"`** -- `elem.shadowRoot` is always `null`. Browser-native shadow trees, such as `<input type="range">`, are closed.
|
||||
|
||||
The [shadow root object](https://dom.spec.whatwg.org/#shadowroot) is like an element (or, more precisely, `DocumentFragment`): we can use `innerHTML` or DOM methods to populate it.
|
||||
|
||||
That's how it looks in Chrome dev tools:
|
||||
|
||||

|
||||
|
||||
**Shadow DOM is strongly delimited from the main document:**
|
||||
|
||||
1. Shadow tree has own stylesheets. Style rules from the outer DOM don't get applied.
|
||||
2. Shadow tree is not visible by `document.querySelector`.
|
||||
3. Shadow tree element ids may match those in the outer document. They be unique only within the shadow tree.
|
||||
|
||||
For example:
|
||||
|
||||
```html run untrusted height=40
|
||||
<div id="elem"></div>
|
||||
<script>
|
||||
|
||||
customElements.define('show-hello', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.attachShadow({mode: 'open'});
|
||||
*!*
|
||||
// inner style is applied to shadow tree (1)
|
||||
*/!*
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style> p { font-weight: bold; } </style>
|
||||
<p>Hello, ${this.getAttribute('name')}!</p>
|
||||
`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<show-hello id="elem" name="John"></show-hello>
|
||||
|
||||
<style>
|
||||
*!*
|
||||
/* document style not applied to shadow tree (2) */
|
||||
*/!*
|
||||
p { color: red; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// <p> is only visible from inside the shadow tree (3)
|
||||
*/!*
|
||||
alert(document.querySelectorAll('p').length); // 0
|
||||
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
|
||||
</script>
|
||||
```
|
||||
|
||||
1. The style from the shadow tree itself applies.
|
||||
2. ...But the style from the outside doesn't.
|
||||
3. To get elements in shadow tree, we must query from inside the tree.
|
||||
|
||||
The element with a shadow root is called a "shadow tree host", and is available as the shadow root `host` property:
|
||||
|
||||
```js
|
||||
// assuming {mode: "open"}
|
||||
alert(elem.shadowRoot.host === elem); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
[Shadow tree](https://dom.spec.whatwg.org/#concept-shadow-tree) is a DOM tree that отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
|
||||
|
||||
*Shadow DOM* -- это внутренний DOM элемента, который существует отдельно от внешнего документа. В нём могут быть свои ID, свои стили и так далее. Причём снаружи его, без применения специальных техник, не видно, поэтому не возникает конфликтов.
|
||||
|
||||
|
@ -160,4 +292,3 @@ Shadow DOM -- это средство для создания отдельног
|
|||
Подробнее спецификация описана по адресу <http://w3c.github.io/webcomponents/spec/shadow/>.
|
||||
|
||||
Далее мы рассмотрим работу с шаблонами, которые также являются частью платформы Web Components и не заменяют существующие шаблонные системы, но дополняют их важными встроенными в браузер возможностями.
|
||||
|
||||
|
|
Before Width: | Height: | Size: 6 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 48 KiB |
BIN
8-web-components/3-shadow-dom/shadow-dom-range.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
8-web-components/3-shadow-dom/shadow-dom-range@2x.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
8-web-components/3-shadow-dom/shadow-dom-say-hello.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
8-web-components/3-shadow-dom/shadow-dom-say-hello@2x.png
Normal file
After Width: | Height: | Size: 25 KiB |