This commit is contained in:
Ilya Kantor 2019-03-31 01:06:10 +03:00
parent 8de6fa6127
commit 79324d0adf
17 changed files with 809 additions and 502 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 324 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 735 KiB

Before After
Before After

View file

@ -67,7 +67,7 @@ For example, there already exists `<time>` element in HTML, for date/time. But i
Let's create `<time-formatted>` element that does the formatting:
```html run height=50 autorun
```html run height=50 autorun="no-epub"
<script>
class TimeFormatted extends HTMLElement {
@ -133,7 +133,7 @@ Please note that in the current implementation, after the element is rendered, f
We can observe attributes by providing their list in `observedAttributes()` static getter, and then update the element in `attributeChangedCallback`. It's called for changes only in the listed attributes for performance reasons.
```html run autorun height=50
```html run autorun="no-epub" height=50
<script>
class TimeFormatted extends HTMLElement {
@ -286,7 +286,7 @@ We could use special [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessi
Built-in elements can be customized by inheriting from their classes. HTML buttons are instances of `HTMLButtonElement`, so let's extend it:
```html run autorun
```html run autorun="no-epub"
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
@ -324,4 +324,7 @@ customElements.define('hello-button', HelloButton, {extends: 'button'}); // 2
Мы рассмотрели, как создавать свои DOM-элементы при помощи стандарта [Custom Elements](http://www.w3.org/TR/custom-elements/).
POLYFILL
Edge is a bit lagging behind, but there's a polyfill <https://github.com/webcomponents/webcomponentsjs> that covers
Далее мы перейдём к изучению дополнительных возможностей по работе с DOM.

View file

@ -75,16 +75,15 @@ 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.
1. Shadow DOM elements are not visible to `querySelector` from the light DOM. In particular, Shadow DOM elements may have ids that conflict with those in the light DOM. They be unique only within the shadow tree.
2. Shadow DOM has own stylesheets. Style rules from the outer DOM don't get applied.
For example:
```html run untrusted height=40
<style>
*!*
/* document style not applied to shadow tree (2) */
/* document style doesn't apply to shadow tree (2) */
*/!*
p { color: red; }
</style>
@ -94,7 +93,7 @@ For example:
<script>
elem.attachShadow({mode: 'open'});
*!*
// inner style is applied to shadow tree (1)
// shadow tree has its own style (1)
*/!*
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
@ -102,7 +101,7 @@ For example:
`;
*!*
// <p> is only visible from inside the shadow tree (3)
// <p> is only visible from queries inside the shadow tree (3)
*/!*
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
@ -120,9 +119,55 @@ The element with a shadow root is called a "shadow tree host", and is available
alert(elem.shadowRoot.host === elem); // true
```
## Composition
We can create "slotted" components, e.g. articles, tabs, menus, that can be filled by
## Slots, composition
Quite often, our components should be generic. Like tabs, menus, etc -- we should be able to fill them with content.
Something like this:
<custom-menu>
<title>Please choose:</title>
<item>Candy</item>
<item>
<div slot="birthday">01.01.2001</div>
<p>I am John</p>
</user-card>
Let's say we need to create a `<user-card>` custom element, for showing user cards.
```html run
<user-card>
<p>Hello</p>
<div slot="username">John Smith</div>
<div slot="birthday">01.01.2001</div>
<p>I am John</p>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot = `
<slot name="username"></slot>
<slot name="birthday"></slot>
<slot></slot>
`;
}
});
</script>
```
Let's say we'd like to create a generic `<user-card>` element for showing the information about the user. The element itself should be generic and contain "bio", "birthday" and "avatar" image.
Another developer should be able to add `<user-card>` on the page
Shadow DOM with "slots", e.g. articles, tabs, menus, that can be filled by
polyfill https://github.com/webcomponents/webcomponentsjs

View file

@ -1,53 +1,92 @@
# Шаблоны <template>
# Template tags
Элемент `<template>` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
A built-in `<template>` element serves as a storage for markup. The browser ignores it contents, only checks for syntax validity.
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность `<template>`?
Of course, there are many ways to create an invisible element somewhere in HTML for markup purposes. What's special about `<template>`?
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только скрыто, но и считается находящимся вообще "вне документа". А при вставке автоматически "оживает", выполняются из него скрипты, начинает проигрываться видео и т.п.
First, as the content is ignored, it can be any valid HTML.
Содержимое тега `<template>`, в отличие, к примеру, от шаблонов или `<script type="неизвестный тип">`, обрабатывается браузером. А значит, должно быть корректным HTML.
For example, we can put there a table row `<tr>`:
```html
<template>
<tr>
<td>Contents</td>
</tr>
</template>
```
Оно доступно как `DocumentFragment` в свойстве тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
Usually, if we try to put `<tr>` inside, say, a `<div>`, the browser detects the invalid DOM structure and "fixes" it, adds `<table>` around. That's not what we want. On the other hand, `<template>` keeps exactly what we place there.
## Вставка шаблона
We can put there styles:
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
```html
<template>
<style>
p { font-weight: bold; }
</style>
</template>
```
```html run autorun="no-epub"
<p id="elem">
Доброе утро, страна!</p>
The browser considers `<template>` content "out of the document", so the style is not applied.
More than that, we can also have `<video>`, `<audio>` and even `<script>` in the template. It becomes live (the script executes) when we insert it.
## Inserting template
The template content is available in its `content` property as a `DocumentFragment` -- a special type of DOM node.
We can treat it as any other DOM node, except one special property: when we insert it somewhere, its children are inserted instead.
For example, let's rewrite a Shadow DOM example from the previous chapter using `<template>`:
```html run untrusted autorun="no-epub" height=60
<template id="tmpl">
<h3><content></content></h3>
<p>Привет из подполья!</p>
<script>
document.write('...document.write:Новость!');
</script>
<style> p { font-weight: bold; } </style>
<script> alert("I am alive!"); </script>
<p id="message"></p>
</template>
<div id="elem">Click me</div>
<script>
var root = elem.createShadowRoot();
root.appendChild(tmpl.content.cloneNode(true));
elem.onclick = function() {
elem.attachShadow({mode: 'open'});
*!*
elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*)
*/!*
elem.shadowRoot.getElementById('message').innerHTML = "Hello from the shadows!";
};
</script>
```
У нас получилось, что:
In the line `(*)` when we clone and insert `tmpl.content`, its children (`<style>`, `<p>`) are inserted instead, they form the shadow DOM:
1. В элементе `#elem` содержатся данные в некоторой оговорённой разметке.
2. Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.
3. Здесь шаблон показывается в Shadow DOM тега. Технически, это не обязательно, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег `<content>`.
```html
<div id="elem">
#shadow-root
<style> p { font-weight: bold; } </style>
<script> alert("I am alive!"); </script>
<p id="message"></p>
</div>
```
Можно также заметить, что скрипт из шаблона выполнился. Это важнейшее отличие вставки шаблона от вставки HTML через `innerHTML` и от обычного `DocumentFragment`.
Please note that the template `<script>` runs exactly when it's added into the document. If we clone `template.content` multiple times, it executes each time.
Также мы вставили не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.
## Summary
## Итого
To summarize:
Тег `<template>` не призван заменить системы шаблонизации. В нём нет хитрых операторов итерации, привязок к данным.
- `<template>` content can be any syntactically correct HTML.
- `<template>` content is considered "out of the document", so it doesn't affect anything.
- We can access `template.content` from JavaScript, clone it to render new component or for other purposes.
Его основная особенность -- это возможность вставки "живого" содержимого, вместе со скриптами.
The `<template>` tag is quite unique, because:
И, конечно, мелочь, но удобно, что он не требует никаких библиотек.
- The browser checks the syntax inside it (as opposed to using a template string inside a script).
- ...But still allows to use any top-level HTML tags, even those that don't make sense without proper wrappers (e.g. `<tr>`).
- The content becomes interactive: scripts run, `<video autoplay>` plays etc, when the insert it into the document (as opposed to assigning it with `elem.innerHTML=` that doesn't do that).
The `<template>` tag does not feature any sophisticated iteration mechanisms or variable substitutions, making it less powerful than frameworks. But it's built-in and ready to serve.

View file

@ -0,0 +1,297 @@
# Shadow DOM slots, composition
Our components should be generic. For instance, when we create a `<custom-tabs>` or `<custom-menu>`, we need to let others will it with data.
The code that makes use of `<custom-menu>` can look like this:
```html
<custom-menu>
<title>Candy menu</title>
<item>Lollipop</item>
<item>Fruit Toast</item>
<item>Cup Cake</item>
</custom-menu>
```
...Then our component should render it properly, as a nice menu with event handlers etc.
How to implement it?
One way is to analyze the element content and dynamically copy-rearrange DOM nodes. That's posssible, but requires some coding. Also, if we're moving things to shadow DOM, then CSS styles from the document do not apply any more, so the visual styling may be lost.
Another way is to use `<slot>` elements in shadow DOM. They allow to specify content in light DOM that the browser renders in component slots.
It's easy to grasp by example.
## Named slots
Here, `<user-card>` shadow DOM provides two slots, and the component user should fill them:
```html run autorun="no-epub" untrusted height=80
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
*!*
<slot name="username"></slot>
*/!*
</div>
<div>Birthday:
*!*
<slot name="birthday"></slot>
*/!*
</div>
`;
}
});
</script>
<user-card>
<span *!*slot="username"*/!*>John Smith</span>
<span *!*slot="birthday"*/!*>01.01.2001</span>
</user-card>
```
In shadow DOM `<slot name="X">` defines an "insertion point", a place where the elements with `slot="X"` are rendered.
The process of re-rendering light DOM inside shadow DOM is rather untypical, so let's go into details.
First, after the script has finished we created the shadow DOM, leading to this DOM structure:
```html
<user-card>
#shadow-root
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
```
Now the element has both light and shadow DOM. In such case only shadow DOM is rendered.
After that, for each `<slot name="...">` in shadow DOM, the browser looks for `slot="..."` with the same name in the light DOM.
These elements are rendered -- as a whole, inside the slots:
![](shadow-dom-user-card.png)
At the end, the so-called "flattened" DOM looks like this:
```html
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
</user-card>
```
So, slotted content fills its containers.
...But the nodes are actually not moved around! That can be easily checked if we run `querySelector`: nodes are still at their places.
```js
// light DOM nodes <span> are physically at place
alert( document.querySelector('user-card span').length ); // 2
```
It looks bizarre indeed, from the first sight, but so it is. The browser renders nodes from light DOM inside the shadow DOM.
````smart header="Slot default content"
If we put something inside a `<slot>`, it becomes the default content. The browser shows it if there's no corresponding filler in light DOM.
For example, in this piece of shadow DOM, `Anonymous` renders if there's no `slot="username"` in corresponding light DOM.
```html
<div>Name:
<slot name="username">Anonymous</slot>
</div>
```
````
````warn header="Only top-level children may have slot=\"...\" attribute"
The `slot="..."` attribute is only valid for direct children of the shadow host (in our example, `<user-card>` element). For nested elements it's ignored.
For example, the second `<span>` here is ignored (as it's not a top-level child of `<user-card>`):
```
<user-card>
<span slot="username">John Smith</span>
<div><span slot="birthday">01.01.2001</span></div>
</user-card>
```
In practice, there's no sense in slotting a deeply nested element, so this limitation just ensures the correct DOM structure.
````
## Default slot
The first `<slot>` in shadow DOM that doesn't have a name is a "default" slot.
It gets any data from the light DOM that isn't slotted elsewhere.
For example, let's add a "bio" field to our `<user-card>` that collects any unslotted information about the user:
```html run autorun="no-epub" untrusted height=140
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<fieldset>
<legend>About me</legend>
*!*
<slot></slot>
*/!*
</fieldset>
`;
}
});
</script>
<user-card>
*!*
<div>Hello</div>
*/!*
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
*!*
<div>I am John!</div>
*/!*
</user-card>
```
All the unslotted light DOM content gets into the `<fieldset>`.
Elements are appended to the slot one after another, so the flattened DOM looks like this:
```html
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
*!*
<fieldset>
<legend>About me</legend>
<slot>
<div>Hello</div>
<div>I am John!</div>
</slot>
</fieldset>
*/!*
</user-card>
```
## Menu example
Now let's back to `<custom-menu>`, mentioned at the beginning of the chapter.
We can use slots to distribute elements: title and items:
```html
<custom-menu>
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</custom-menu>
```
The shadow DOM template:
```html
<template id="tmpl">
<style> /* menu styles */ </style>
<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>
</template>
```
As you can see, for `<slot name="item">` there are multiple `<li>` elements in light DOM, with the same `slot="item"`. In that case they all get appended to the slot, one after another.
So the flattened DOM becomes:
```html
<div class="menu">
<slot name="title">
<span slot="title">Candy menu</span>
</slot>
<ul>
<slot name="item">
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</slot>
</ul>
</div>
```
One might notice that, in a valid DOM, `<li>` must be a direct child of `<ul>`, so we have an oddity here. But that's flattened DOM, it describes how the component is rendered. Physically nodes are still at their places.
We just need to add an `onclick` handler, and the custom element is ready:
```js
customElements.define('custom-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
// tmpl is the shadow DOM template (above)
this.shadowRoot.append( tmpl.content.cloneNode(true) );
// we can't select light DOM nodes, so let's handle clicks on the slot
this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
// open/close the menu
this.shadowRoot.querySelector('.menu').classList.toggle('closed');
};
}
});
```
Here's the full demo:
[iframe src="menu" height=140 edit]
Now, as we have elements from light DOM in the shadow DOM, styles from the document and shadow DOM can mix. There are few simple rules for that. We'll see the details in the next chapter.
## Итого
Shadow DOM -- это средство для создания отдельного DOM-дерева внутри элемента, которое не видно снаружи без применения специальных методов.
- Ряд браузерных элементов со сложной структурой уже имеют Shadow DOM.
- Можно создать Shadow DOM внутри любого элемента вызовом `elem.createShadowRoot()`. В дальнейшем его корень будет доступен как `elem.shadowRoot`. У встроенных элементов он недоступен.
- Как только у элемента появляется Shadow DOM, его изначальное содержимое скрывается. Теперь показывается только Shadow DOM, который может указать, какое содержимое хозяина куда вставлять, при помощи элемента `<content>`. Можно указать селектор `<content select="селектор">` и размещать разное содержимое в разных местах Shadow DOM.
- Элемент `<content>` перемещает содержимое исходного элемента в Shadow DOM только визуально, в структуре DOM оно остаётся на тех же местах.
Подробнее спецификация описана по адресу <http://w3c.github.io/webcomponents/spec/shadow/>.
Далее мы рассмотрим работу с шаблонами, которые также являются частью платформы Web Components и не заменяют существующие шаблонные системы, но дополняют их важными встроенными в браузер возможностями.

View file

@ -0,0 +1,54 @@
<!doctype html>
<template id="tmpl">
<style>
ul {
margin: 0;
list-style: none;
padding-left: 20px;
}
::slotted([slot="title"]) {
font-size: 18px;
font-weight: bold;
cursor: pointer;
}
::slotted([slot="title"])::before {
content: '📂';
font-size: 14px;
}
.closed ::slotted([slot="title"])::before {
content: '📁';
}
.closed ul {
display: none;
}
</style>
<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>
</template>
<script>
customElements.define('custom-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.append( tmpl.content.cloneNode(true) );
this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
this.shadowRoot.querySelector('.menu').classList.toggle('closed');
};
}
});
</script>
<custom-menu>
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</custom-menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -0,0 +1,332 @@
# Shadow DOM styling
Shadow DOM may include both `<style>` and `<link rel="stylesheeet" href="…">` tags. In the latter case, stylesheets are HTTP-cached, so they are not redownloaded.
As a general rule, CSS selectors in local styles work only inside the shadow tree. But there are few exceptions.
## :host
The `:host` selector allows to select the shadow host: the element containing the shadow tree.
For instance, we're making `<custom-dialog>` element that should be centered. For that we need to style not something inside `<custom-dialog>`, but the element itself.
That's exactly what `:host` selects:
```html run autorun="no-epub" untrusted height=80
<template id="tmpl">
<style>
:host {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog>
Hello!
</custom-dialog>
```
## Cascading
The shadow host (`<custom-dialog>` itself) is a member of the outer document, so it's affected by the main CSS cascade.
If there's a property styled both in `:host` locally, and in the document, then the document style takes precedence.
For instance, if in the outer document we had:
```html
<style>
custom-dialog {
padding: 0;
}
</style>
```
...Then the dialog would be without padding.
It's very convenient, as we can setup "default" styles in the component `:host` rule, and then easily override them in the document.
The exception is when a local property is labelled `!important`. For important properties, local styles take precedence.
## :host(selector)
Same as `:host`, but the shadow host also must the selector.
For example, we'd like to center the custom dialog only if it has `centered` attribute:
```html run autorun="no-epub" untrusted height=80
<template id="tmpl">
<style>
*!*
:host([centered]) {
*/!*
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
:host {
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog centered>
Centered!
</custom-dialog>
<custom-dialog>
Not centered.
</custom-dialog>
```
Now the additional centering styles are only applied to the first dialog `<custom-dialog centered>`.
## :host-context(selector)
Same as `:host`, the shadow host or any of its ancestors in the outer document must match the selector.
E.g. if we add a rule `:host-context(.top)` to the example above, it matches for following cases if an ancestor matches `.top`:
```html
<header class="top">
<div>
<custom-dialog>...</custom-dialog>
<div>
</header>
```
...Or when the dialog matches `.top`:
```html
<custom-dialog class="top">...</custom-dialog>
```
```smart
The `:host-context(selector)` is the only CSS rule that can somewhat test the outer document context, outside the shadow host.
```
## Styling slotted content
Now let's consider the situation with slots.
Elements, that come from light DOM, keep their document styles. Local styles do not affect them, as they are physically not in the shadow DOM.
For example, here `<span>` takes the document style, but not the local one:
```html run autorun="no-epub" untrusted height=80
<style>
*!*
span { font-weight: bold }
*/!*
</style>
<user-card>
<div slot="username">*!*<span>John Smith</span>*/!*</div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
*!*
span { background: red; }
*/!*
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
```
The result is bold, but not red.
If we'd like to style slotted elements, there are two choices.
First, we can style the `<slot>` itself and rely on style inheritance:
```html run autorun="no-epub" untrusted height=80
<user-card>
<div slot="username">*!*<span>John Smith</span>*/!*</div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
*!*
slot[name="username"] { font-weight: bold; }
*/!*
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
```
Here `<p>John Smith</p>` becomes bold, because of CSS inheritance from it's "flattened parent" slot. But not all CSS properties are inherited.
Another option is to use `::slotted(selector)` pseudo-class. It allows to select elements that are inserted into slots and match the selector.
In our example, `::slotted(div)` selects exactly `<div slot="username">`:
```html run autorun="no-epub" untrusted height=80
<user-card>
<div slot="username">*!*<span>John Smith</span>*/!*</div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
*!*
::slotted(div) { display: inline; border: 1px solid red; }
*/!*
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
```
Please note, we can't descend any further. These selectors are invalid:
```css
::slotted(div span) {
/* our slotted <div> does not match this */
}
::slotted(div) p {
/* can't go inside light DOM */
}
```
Also, `::slotted` can't be used in JavaScript `querySelector`. That's CSS only pseudo-class.
## Using CSS properties
How do we style a component in-depth?
Naturally, we can style its main element, `<custom-dialog>` or `<user-card>`, etc.
But how can we affect its internals? For instance, in `<user-card>` we'd like to allow the outer document change how user fields look.
Just as we expose methods to interact with our component, we can expose CSS variables (custom CSS properties) to style it.
**Custom CSS properties exist on all levels, both in light and shadow.**
For example, in shadow DOM we can use `--user-card-field-color` CSS variable to style fields:
```html
<style>
.field {
/* if --user-card-field-color is not defined, use black */
color: var(--user-card-field-color, black);
}
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>
</style>
```
Then, we can declare this property in the outer document for `<user-card>`:
```css
user-card {
--user-card-field-color: green;
}
```
Custom CSS properties pierce through shadow DOM, they are visible everywhere, so the inner `.field` rule will make use of it.
Here's the full example:
```html run autorun="no-epub" untrusted height=80
<template id="tmpl">
<style>
.field {
color: var(--user-card-field-color, black);
}
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>
</template>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true));
}
});
</script>
<style>
user-card {
--user-card-field-color: green;
}
</style>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
```
## Summary
Shadow DOM can include styles, such as `<style>` or `<link rel="stylesheet">`.
Local styles can affect:
- shadow tree,
- shadow host (from the outer document),
- slotted elements (from the outer document).
Document styles can affect:
- shadow host (as it's in the outer document)
- slotted elements and their contents (as it's physically in the outer document)
When CSS properties intersect, normally document styles have precedence, unless the property is labelled as `!important`. Then local styles have precedence.
CSS custom properties pierce through shadow DOM. They are used as "hooks" to style the component:
1. The component uses CSS properties to style key elements, such as `var(--component-name-title, <default value>)` for a title.
2. Component author publishes these properties for developers, they are same important as other public component methods.
3. When a developer wants to style a title, they assign `--component-name-title` CSS property for the shadow host (as in the example above) or one of its parents.
4. Profit!

View file

@ -1,294 +0,0 @@
# Веб-компонент в сборе
В этой главе мы посмотрим на итоговый пример веб-компонента, включающий в себя описанные ранее технологии: Custom Elements, Shadow DOM, CSS Scoping и, конечно же, Imports.
## Компонент ui-message
Компонент `ui-message` будет описан в отдельном файле `ui-message.html`.
Его использование будет выглядеть следующим образом:
```html
<link rel="import" id="link" href="ui-message.html">
<style>
ui-message {
width: 80%;
margin: auto;
}
</style>
*!*
<ui-message class="info">Доброе утро, страна!</ui-message>
*/!*
*!*
<ui-message class="warning">Внимание-внимание! Говорит информбюро!</ui-message>
*/!*
```
Этот код ничем не отличается от использования обычного элемента, поэтому перейдём дальше, к содержимому `ui-message.html`
## Шаблон для ui-message
Файл `ui-message.html` можно начать с шаблона:
```html
<template id="tmpl">
<style>
.content {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
:host {
display: block;
}
:host(.info) .content {
color: green;
}
:host(.warning) .content {
color: red;
}
</style>
<div class="content">
<content></content>
</div>
</template>
```
Этот шаблон рисует `<div class="content">` и заполняет его содержимым элемента-хозяина.
Важные детали:
- Самое важное правило здесь `:host { display:block }`.
Оно обязательно! . Это правило задаёт, что корень DOM-дерева будет иметь `display:block`. По умолчанию `:host` не создаёт CSS-блок, а это значит, что ни ширину ни отступы указать не получится.
- Последующие правила `:host(.info) .content` и `:host(.warning) .content` стилизуют содержимое в зависимости от того, какой на хозяине класс.
## Скрипт для ui-message
В файле `ui-message.html` мы создадим новый элемент `<ui-message>`:
```js
// (1) получить шаблон
var localDocument = document.currentScript.ownerDocument;
var tmpl = localDocument.getElementById('tmpl');
// (2) создать элемент
var MessageProto = Object.create(HTMLElement.prototype);
MessageProto.createdCallback = function() {
var root = this.createShadowRoot();
root.appendChild(tmpl.content.cloneNode(true));
};
// (3) зарегистрировать в DOM
document.registerElement('ui-message', {
prototype: MessageProto
});
```
Все компоненты этого кода мы подробно разбирали ранее:
1. Получаем шаблон из текущего документа, то есть из самого импорта.
2. Описываем элемент. Он довольно прост -- при создании записывает в свой `Shadow DOM` шаблон. При этом содержимое исходного элемента будет показано в `<content>`, но делать правила на сам `content` бессмысленно -- они не сработают. Нужно либо перейти внутрь `<content>` при помощи `::content`-селектора, либо указать для внешнего элемента `.content`, что в данном случае и сделано.
3. С момента регистрации все уже существующие элементы `<ui-message>` будут превращены в описанные здесь. И будущие, конечно, тоже.
Компонент в действии:
[codetabs src="message" height=200]
## Компонент ui-slider с jQuery
Компонент может использовать и внешние библиотеки.
Для примера создадим слайдер с использованием библиотеки [jQuery UI](http://jqueryui.com).
Компонент `ui-slider` будет показывать слайдер с минимальным и максимальным значением из атрибутов `min/max` и генерировать событие `slide` при его перемещении.
Использование:
```html
<link rel="import" id="link" href="ui-slider.html">
<ui-slider min="0" max="1000" id="elem"></ui-slider>
<script>
elem.addEventListener("slide", function(e) {
value.innerHTML = e.detail.value;
});
</script>
<div id="value">0</div>
```
## Файл компонента ui-slider
Файл `ui-slider.html`, задающий компонент, мы разберём по частям.
### Заголовок
В начале подключим jQuery и jQuery UI.
Мы импортируем в слайдер `jquery.html`, который содержит теги `<script>` вместо того, чтобы явным образом прописывать загрузку скриптов:
```html
<head>
<link rel="import" href="jquery.html">
</head>
```
Это сделано для того, чтобы другие компоненты, которым тоже могут понадобится эти библиотеки, также могли импортировать `jquery.html`. При повторном импорте ничего не произойдёт, скрипты не будут подгружены и исполнены два раза.
То есть, это средство оптимизации.
Содержимое `jquery.html`:
[html src="ui-slider/jquery.html"]
### Шаблон
Шаблон будет помещён в Shadow DOM. В нём должны быть стили и элементы, необходимые слайдеру.
Конкретно для слайдера из разметки достаточно одного элемента `<div id="slider"></div>`, который затем будет обработан jQuery UI.
Кроме того, в шаблоне должны быть стили:
```html
<template id="tmpl">
<style>
@import url(https://code.jquery.com/ui/1.11.4/themes/ui-lightness/jquery-ui.css);
:host {
display: block;
}
</style>
<div id="slider"></div>
</template>
```
### Скрипт
Скрипт для нового элемента похож на тот, что делали раньше, но теперь он использует jQuery UI для создания слайдера внутри своего Shadow DOM.
Для его понимания желательно знать jQuery, хотя в коде ниже я намеренно свёл использование этой библиотеки к минимуму.
```js
var localDocument = document.currentScript.ownerDocument;
var tmpl = localDocument.getElementById('tmpl');
var SliderProto = Object.create(HTMLElement.prototype);
SliderProto.createdCallback = function() {
// (1) инициализировать Shadow DOM, получить из него #slider
var root = this.createShadowRoot();
root.appendChild(tmpl.content.cloneNode(true));
this.$slider = $(root.getElementById('slider'));
var self = this;
// (2) инициализировать слайдер, пробросить параметры
this.$slider.slider({
min: this.getAttribute('min') || 0,
max: this.getAttribute('max') || 100,
value: this.getAttribute('value') || 0,
slide: function() {
// (3) пробросить событие
var event = new CustomEvent("slide", {
detail: {
value: self.$slider.slider("option", "value")
},
bubbles: true
});
self.dispatchEvent(event);
}
});
};
document.registerElement('ui-slider', {
prototype: SliderProto
});
```
Функция `createdCallback` по шагам:
1. Создаём Shadow DOM, элемент `#slider` получаем из него, он не в основном документе.
2. Используя jQuery UI, слайдер создаётся вызовом [jQuery UI методом slider](http://jqueryui.com/slider/), который имеет вид `$elem.slider({...параметры...});`. Параметры получаем из атрибутов `<ui-slider>` (он же `this`) и отдаём библиотеке. Она делает всю работу.
3. Параметр `slide` задаёт функцию-коллбэк, которая вызывается при передвижении слайдера и будет генерировать DOM-событие на элементе, на которое можно будет поставить обработчик при помощи `addEventListener`. В его деталях мы указываем новое значение слайдера.
Полный код с примером:
[codetabs src="ui-slider" height=300]
Его можно далее улучшать, например добавить геттер и сеттер для значения `value`:
```js
Object.defineProperty(SliderProto, 'value', {
get: function() {
return this.$slider.slider("option", "value");
},
set: function(value) {
this.$slider.slider('option', 'value', value);
}
});
```
Если добавить этот код, то к значению `<ui-slider>` можно будет обращаться как `elem.value`, аналогично всяким встроенным `<input>`.
## Проблема с jQuery
Попробуйте пример выше. Он не совсем работает. Слайдер прокручивается первый раз, но второй раз он как-то странно "прыгает".
Чтобы понять, почему это происходит, я заглянул в исходники jQuery UI и, после отладки происходящего, натолкнулся на проблемный код.
Он был в методе [offset](http://api.jquery.com/offset/), который предназначен для того, чтобы определять координаты элемента. Этот метод не срабатывал, поскольку в нём есть проверка, которая выглядит примерно так:
```js
var box = {
top: 0,
left: 0
};
...
// Make sure it's not a disconnected DOM node
if(!jQuery.contains(elem.ownerDocument, elem)) {
return box;
}
```
То есть, jQuery проверяет, находится ли элемент `elem` внутри своего документа `elem.ownerDocument`. Если нет -- то считается, что элемент вне DOM, и его размеры равны нулю.
Если копнуть чуть глубже, то `jQuery.contains` в современных браузерах сводится к обычному вызову [contains](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains).
Парадокс с Shadow DOM заключается в том, что вызов `elem.ownerDocument.contains(elem)` вернёт `false`!
Получилось, что элемент не в документе и одновременно он имеет размеры. Такого разработчики jQuery не предусмотрели.
Можно, конечно, побежать исправлять jQuery, но давайте подумаем, может быть так оно и должно быть?
С точки зрения здравого смысла, Shadow DOM является частью текущего документа. Это соответствует и духу [текущей спецификации](http://w3c.github.io/webcomponents/spec/shadow/), где shadow tree рассматривается в контексте document tree.
Поэтому на самом деле `document.contains(elem)` следовало бы возвращать `true`.
Почему же `false`? Причина проста -- описанный в [другом стандарте](http://www.w3.org/TR/dom/#dom-node-contains) механизм работы `contains` по сути состоит в проходе вверх от `elem` по цепочке `parentNode`, пока либо встретим искомый элемент, тогда ответ `true`, а иначе `false`. В случае с Shadow DOM этот путь закончится на корне Shadow DOM-дерева, оно ведь не является потомком хозяина.
**Метод `contains` описан стандартом без учёта Shadow DOM, поэтому возвратил неверный результат `false`.**
Это один из тех небольших, но важных нюансов, которые показывают, что стандарты всё ещё в разработке.
## Итого
- С использованием современных технологий можно делать компоненты. Но это, всё же, дело будущего. Все стандарты находятся в процессе доработки, готовятся новые.
- Можно использовать произвольную библиотеку, такую как jQuery, и работать с Shadow DOM с её использованием. Но возможны проблемки. Выше была продемонстрирована одна из них, могут быть и другие.
Пока веб-компоненты ещё не являются законченными стандартами, можно попробовать [Polymer](http://www.polymer-project.org) -- это самый известный из полифиллов на тему веб-компонент.
Он старается их эмулировать по возможности кросс-браузерно, но пока что это довольно-таки сложно, в частности, необходима дополнительная разметка.

View file

@ -1,23 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="import" id="link" href="ui-message.html">
<style>
ui-message {
width: 80%;
margin: auto;
}
</style>
</head>
<body>
<ui-message class="info">Доброе утро, страна!</ui-message>
<ui-message class="warning">Внимание-внимание! Говорит информбюро!</ui-message>
</body>
</html>

View file

@ -1,55 +0,0 @@
<!DOCTYPE HTML>
<html>
<body>
<template id="tmpl">
<style>
.content {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
:host {
display: block;
}
:host(.info) .content {
color: green;
}
:host(.warning) .content {
color: red;
}
</style>
<div class="content">
<content></content>
</div>
</template>
<script>
! function() {
var localDocument = document.currentScript.ownerDocument;
var tmpl = localDocument.getElementById('tmpl');
var MessageProto = Object.create(HTMLElement.prototype);
MessageProto.createdCallback = function() {
var root = this.createShadowRoot();
root.appendChild(tmpl.content.cloneNode(true));
};
document.registerElement('ui-message', {
prototype: MessageProto
});
}();
</script>
</body>
</html>

View file

@ -1,29 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="import" id="link" href="ui-slider.html">
<style>
ui-slider {
width: 300px;
margin: 10px;
}
</style>
</head>
<body>
<ui-slider min="0" max="1000" id="elem"></ui-slider>
<script>
elem.addEventListener("slide", function(e) {
value.innerHTML = e.detail.value; // = this.value
});
</script>
<div id="value">0</div>
</body>
</html>

View file

@ -1,2 +0,0 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>

View file

@ -1,60 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="import" href="jquery.html">
</head>
<body>
<template id="tmpl">
<style>
@import url(https://code.jquery.com/ui/1.11.4/themes/ui-lightness/jquery-ui.css);
:host {
display: block;
}
</style>
<div id="slider"></div>
</template>
<script>
! function() {
var localDocument = document.currentScript.ownerDocument;
var tmpl = localDocument.getElementById('tmpl');
var SliderProto = Object.create(HTMLElement.prototype);
SliderProto.createdCallback = function() {
var root = this.createShadowRoot();
root.appendChild(tmpl.content.cloneNode(true));
this.$slider = $(root.getElementById('slider'));
var self = this;
this.$slider.slider({
min: +this.getAttribute('min') || 0,
max: +this.getAttribute('max') || 100,
value: this.getAttribute('value') || 0,
slide: function() {
var event = new CustomEvent("slide", {
detail: {
value: self.$slider.slider("option", "value")
},
bubbles: true
});
self.dispatchEvent(event);
}
});
};
document.registerElement('ui-slider', {
prototype: SliderProto
});
}();
</script>
</body>
</html>

Binary file not shown.