up
|
@ -226,7 +226,7 @@ You may want skip those for now if you're reading for the first time, or if you
|
||||||
|
|
||||||
### Module scripts are deferred
|
### Module scripts are deferred
|
||||||
|
|
||||||
Module scripts are *always* deferred, same effect as `defer` attribute (described in the chapter [](info:onload-ondomcontentloaded)), for both external and inline scripts.
|
Module scripts are *always* deferred, same effect as `defer` attribute (described in the chapter [](info:script-async-defer)), for both external and inline scripts.
|
||||||
|
|
||||||
In other words:
|
In other words:
|
||||||
- external module scripts `<script type="module" src="...">` don't block HTML processing.
|
- external module scripts `<script type="module" src="...">` don't block HTML processing.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Page lifecycle: DOMContentLoaded, load, beforeunload, unload
|
# Page: DOMContentLoaded, load, beforeunload, unload
|
||||||
|
|
||||||
The lifecycle of an HTML page has three important events:
|
The lifecycle of an HTML page has three important events:
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ Each event may be useful:
|
||||||
|
|
||||||
- `DOMContentLoaded` event -- DOM is ready, so the handler can lookup DOM nodes, initialize the interface.
|
- `DOMContentLoaded` event -- DOM is ready, so the handler can lookup DOM nodes, initialize the interface.
|
||||||
- `load` event -- additional resources are loaded, we can get image sizes (if not specified in HTML/CSS) etc.
|
- `load` event -- additional resources are loaded, we can get image sizes (if not specified in HTML/CSS) etc.
|
||||||
- `beforeunload/unload` event -- the user is leaving: we can check if the user saved the changes and ask them whether they really want to leave.
|
- `beforeunload` event -- the user is leaving: we can check if the user saved the changes and ask them whether they really want to leave.
|
||||||
|
- `unload` -- the user almost left, but we still can initiate some operations, such as sending out statistics.
|
||||||
|
|
||||||
Let's explore the details of these events.
|
Let's explore the details of these events.
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ We must use `addEventListener` to catch it:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
document.addEventListener("DOMContentLoaded", ready);
|
document.addEventListener("DOMContentLoaded", ready);
|
||||||
|
// not "document.onDOMContentLoaded = ..."
|
||||||
```
|
```
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
@ -43,40 +45,46 @@ For instance:
|
||||||
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
|
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
|
||||||
```
|
```
|
||||||
|
|
||||||
In the example the `DOMContentLoaded` handler runs when the document is loaded and does not wait for the image to load. So `alert` shows zero sizes.
|
In the example the `DOMContentLoaded` handler runs when the document is loaded, so it can see all the elements, including `<img>` below.
|
||||||
|
|
||||||
At the first sight `DOMContentLoaded` event is very simple. The DOM tree is ready -- here's the event. But there are few peculiarities.
|
But it doesn't wait for the image to load. So `alert` shows zero sizes.
|
||||||
|
|
||||||
|
At the first sight `DOMContentLoaded` event is very simple. The DOM tree is ready -- here's the event. There are few peculiarities though.
|
||||||
|
|
||||||
### DOMContentLoaded and scripts
|
### DOMContentLoaded and scripts
|
||||||
|
|
||||||
When the browser initially loads HTML and comes across a `<script>...</script>` in the text, it can't continue building DOM. It must execute the script right now. So `DOMContentLoaded` may only happen after all such scripts are executed.
|
When the browser processes an HTML-document and comes across a `<script>` tag, it needs to execute before continuing building the DOM. That's a precaution, as scripts may want to modify DOM, and even `document.write` into it, so `DOMContentLoaded` has to wait.
|
||||||
|
|
||||||
External scripts (with `src`) also put DOM building to pause while the script is loading and executing. So `DOMContentLoaded` waits for external scripts as well.
|
So DOMContentLoaded definitely happens after such scripts:
|
||||||
|
|
||||||
The only exception are external scripts with `async` and `defer` attributes. They tell the browser to continue processing without waiting for the scripts. This lets the user see the page before scripts finish loading, which is good for performance.
|
```html run
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
console.log("DOM ready!");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
### Scripts with `async` and `defer`
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
|
||||||
|
|
||||||
Attributes `async` and `defer` work only for external scripts. They are ignored if there's no `src`.
|
<script>
|
||||||
|
alert("Library loaded, inline script executed");
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
Both of them tell the browser that it may go on working with the page, and load the script "in background", then run the script when it loads. So the script doesn't block DOM building and page rendering.
|
In the example above, we first see "Library loaded...", and then "DOM ready!" (all scripts are executed).
|
||||||
|
|
||||||
There are two differences between them.
|
```warn header="Scripts with `async`, `defer` or `type=\"module\"` don't block DOMContentLoaded"
|
||||||
|
|
||||||
| | `async` | `defer` |
|
Script attributes `async` and `defer`, that we'll cover [a bit later](info:script-async-defer), don't block DOMContentLoaded. [Javascript modules](info:modules) behave like `defer`, they don't block it too.
|
||||||
|---------|---------|---------|
|
|
||||||
| Order | Scripts with `async` execute *in the load-first order*. Their document order doesn't matter -- which loads first runs first. | Scripts with `defer` always execute *in the document order* (as they go in the document). |
|
|
||||||
| `DOMContentLoaded` | Scripts with `async` may load and execute while the document has not yet been fully downloaded. That happens if scripts are small or cached, and the document is long enough. | Scripts with `defer` execute after the document is loaded and parsed (they wait if needed), right before `DOMContentLoaded`. |
|
|
||||||
|
|
||||||
So `async` is used for independent scripts, like counters or ads, that don't need to access page content. And their relative execution order does not matter.
|
So here we're talking about "regular" scripts, like `<script>...</script>`, or `<script src="..."></script>`.
|
||||||
|
```
|
||||||
While `defer` is used for scripts that need DOM and/or their relative execution order is important.
|
|
||||||
|
|
||||||
### DOMContentLoaded and styles
|
### DOMContentLoaded and styles
|
||||||
|
|
||||||
External style sheets don't affect DOM, and so `DOMContentLoaded` does not wait for them.
|
External style sheets don't affect DOM, so `DOMContentLoaded` does not wait for them.
|
||||||
|
|
||||||
But there's a pitfall: if we have a script after the style, then that script must wait for the stylesheet to execute:
|
But there's a pitfall. Isf we have a script after the style, then that script must wait until the stylesheet loads:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link type="text/css" rel="stylesheet" href="style.css">
|
<link type="text/css" rel="stylesheet" href="style.css">
|
||||||
|
@ -98,7 +106,6 @@ For instance, if the page has a form with login and password, and the browser re
|
||||||
|
|
||||||
So if `DOMContentLoaded` is postponed by long-loading scripts, then autofill also awaits. You probably saw that on some sites (if you use browser autofill) -- the login/password fields don't get autofilled immediately, but there's a delay till the page fully loads. That's actually the delay until the `DOMContentLoaded` event.
|
So if `DOMContentLoaded` is postponed by long-loading scripts, then autofill also awaits. You probably saw that on some sites (if you use browser autofill) -- the login/password fields don't get autofilled immediately, but there's a delay till the page fully loads. That's actually the delay until the `DOMContentLoaded` event.
|
||||||
|
|
||||||
One of minor benefits in using `async` and `defer` for external scripts -- they don't block `DOMContentLoaded` and don't delay browser autofill.
|
|
||||||
|
|
||||||
## window.onload [#window-onload]
|
## window.onload [#window-onload]
|
||||||
|
|
||||||
|
@ -123,15 +130,15 @@ The example below correctly shows image sizes, because `window.onload` waits for
|
||||||
|
|
||||||
When a visitor leaves the page, the `unload` event triggers on `window`. We can do something there that doesn't involve a delay, like closing related popup windows.
|
When a visitor leaves the page, the `unload` event triggers on `window`. We can do something there that doesn't involve a delay, like closing related popup windows.
|
||||||
|
|
||||||
This event is a good place to send out analytics.
|
The notable exception is sending analytics.
|
||||||
|
|
||||||
E.g. we have a script that gathers some data about mouse clicks, scrolls, viewed page areas -- the statistics that can help us to see what users want.
|
Let's say we gather data about how the page is used: mouse clicks, scrolls, viewed page areas, and so on.
|
||||||
|
|
||||||
Then `onunload` is the best time to send it out. Regular networking methods such as [fetch](info:fetch-basics) or [XMLHttpRequest](info:xmlhttprequest) don't work well, because we're in the process of leaving the page.
|
Naturally, `unload` event is when the user leaves us, and we'd like to save the data on our server.
|
||||||
|
|
||||||
So, there exist `navigator.sendBeacon(url, data)` method for such needs, described in the specification <https://w3c.github.io/beacon/>.
|
There exists a special `navigator.sendBeacon(url, data)` method for such needs, described in the specification <https://w3c.github.io/beacon/>.
|
||||||
|
|
||||||
It sends the data in background. The transition to another page is not delayed: the browser leaves the page and performs `sendBeacon` in background.
|
It sends the data in background. The transition to another page is not delayed: the browser leaves the page, but still performs `sendBeacon`.
|
||||||
|
|
||||||
Here's how to use it:
|
Here's how to use it:
|
||||||
```js
|
```js
|
||||||
|
@ -148,15 +155,28 @@ window.addEventListener("unload", function() {
|
||||||
|
|
||||||
When the `sendBeacon` request is finished, the browser probably has already left the document, so there's no way to get server response (which is usually empty for analytics).
|
When the `sendBeacon` request is finished, the browser probably has already left the document, so there's no way to get server response (which is usually empty for analytics).
|
||||||
|
|
||||||
|
There's also a `keepalive` flag for doing such "after-page-left" requests in [fetch](info:fetch-basics) method for generic network requests. You can find more information in the chapter <info:fetch-api>.
|
||||||
|
|
||||||
|
|
||||||
If we want to cancel the transition to another page, we can't do it here. But we can use another event -- `onbeforeunload`.
|
If we want to cancel the transition to another page, we can't do it here. But we can use another event -- `onbeforeunload`.
|
||||||
|
|
||||||
## window.onbeforeunload [#window.onbeforeunload]
|
## window.onbeforeunload [#window.onbeforeunload]
|
||||||
|
|
||||||
If a visitor initiated navigation away from the page or tries to close the window, the `beforeunload` handler asks for additional confirmation.
|
If a visitor initiated navigation away from the page or tries to close the window, the `beforeunload` handler asks for additional confirmation.
|
||||||
|
|
||||||
It may return a string with the question. Historically browsers used to show it, but as of now only some of them do. That's because certain webmasters abused this event handler by showing misleading and hackish messages.
|
If we cancel the event, the browser may ask the visitor if they are sure.
|
||||||
|
|
||||||
You can try it by running this code and then reloading the page.
|
You can try it by running this code and then reloading the page:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
For historical reasons, returning a non-empty string also counts as canceling the event. Some time ago browsers used show it as a message, but as the [modern specification](https://html.spec.whatwg.org/#unloading-documents) says, they shouldn't.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
window.onbeforeunload = function() {
|
window.onbeforeunload = function() {
|
||||||
|
@ -164,11 +184,7 @@ window.onbeforeunload = function() {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
```online
|
The behavior was changed, because some webmasters abused this event handler by showing misleading and annoying messages. So right now old browsers still may show it as a message, but aside of that -- there's no way to customize the message shown to the user.
|
||||||
Or you can click on the button in `<iframe>` below to set the handler, and then click the link:
|
|
||||||
|
|
||||||
[iframe src="window-onbeforeunload" border="1" height="80" link edit]
|
|
||||||
```
|
|
||||||
|
|
||||||
## readyState
|
## readyState
|
||||||
|
|
||||||
|
@ -176,9 +192,11 @@ What happens if we set the `DOMContentLoaded` handler after the document is load
|
||||||
|
|
||||||
Naturally, it never runs.
|
Naturally, it never runs.
|
||||||
|
|
||||||
There are cases when we are not sure whether the document is ready or not, for instance an external script with `async` attribute loads and runs asynchronously. Depending on the network, it may load and execute before the document is complete or after that, we can't be sure. So we should be able to know the current state of the document.
|
There are cases when we are not sure whether the document is ready or not. We'd like our function to execute when the DOM is loaded, be it now or later.
|
||||||
|
|
||||||
The `document.readyState` property gives us information about it. There are 3 possible values:
|
The `document.readyState` property tells us about the current loading state.
|
||||||
|
|
||||||
|
There are 3 possible values:
|
||||||
|
|
||||||
- `"loading"` -- the document is loading.
|
- `"loading"` -- the document is loading.
|
||||||
- `"interactive"` -- the document was fully read.
|
- `"interactive"` -- the document was fully read.
|
||||||
|
@ -192,13 +210,15 @@ Like this:
|
||||||
function work() { /*...*/ }
|
function work() { /*...*/ }
|
||||||
|
|
||||||
if (document.readyState == 'loading') {
|
if (document.readyState == 'loading') {
|
||||||
|
// loading yet, wait for the event
|
||||||
document.addEventListener('DOMContentLoaded', work);
|
document.addEventListener('DOMContentLoaded', work);
|
||||||
} else {
|
} else {
|
||||||
|
// DOM is ready!
|
||||||
work();
|
work();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
There's a `readystatechange` event that triggers when the state changes, so we can print all these states like this:
|
There's also `readystatechange` event that triggers when the state changes, so we can print all these states like this:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
// current state
|
// current state
|
||||||
|
@ -208,15 +228,14 @@ console.log(document.readyState);
|
||||||
document.addEventListener('readystatechange', () => console.log(document.readyState));
|
document.addEventListener('readystatechange', () => console.log(document.readyState));
|
||||||
```
|
```
|
||||||
|
|
||||||
The `readystatechange` event is an alternative mechanics of tracking the document loading state, it appeared long ago. Nowadays, it is rarely used, but let's cover it for completeness.
|
The `readystatechange` event is an alternative mechanics of tracking the document loading state, it appeared long ago. Nowadays, it is rarely used.
|
||||||
|
|
||||||
What is the place of `readystatechange` among other events?
|
Let's see the full events flow for the completeness.
|
||||||
|
|
||||||
To see the timing, here's a document with `<iframe>`, `<img>` and handlers that log events:
|
Here's a document with `<iframe>`, `<img>` and handlers that log events:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script>
|
<script>
|
||||||
function log(text) { /* output the time and message */ }
|
|
||||||
log('initial readyState:' + document.readyState);
|
log('initial readyState:' + document.readyState);
|
||||||
|
|
||||||
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
|
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
|
||||||
|
@ -244,23 +263,22 @@ The typical output:
|
||||||
6. [4] readyState:complete
|
6. [4] readyState:complete
|
||||||
7. [4] window onload
|
7. [4] window onload
|
||||||
|
|
||||||
The numbers in square brackets denote the approximate time of when it happens. The real time is a bit greater, but events labeled with the same digit happen approximately at the same time (+- a few ms).
|
The numbers in square brackets denote the approximate time of when it happens. Events labeled with the same digit happen approximately at the same time (+- a few ms).
|
||||||
|
|
||||||
- `document.readyState` becomes `interactive` right before `DOMContentLoaded`. These two events actually mean the same.
|
- `document.readyState` becomes `interactive` right before `DOMContentLoaded`. These two things actually mean the same.
|
||||||
- `document.readyState` becomes `complete` when all resources (`iframe` and `img`) are loaded. Here we can see that it happens in about the same time as `img.onload` (`img` is the last resource) and `window.onload`. Switching to `complete` state means the same as `window.onload`. The difference is that `window.onload` always works after all other `load` handlers.
|
- `document.readyState` becomes `complete` when all resources (`iframe` and `img`) are loaded. Here we can see that it happens in about the same time as `img.onload` (`img` is the last resource) and `window.onload`. Switching to `complete` state means the same as `window.onload`. The difference is that `window.onload` always works after all other `load` handlers.
|
||||||
|
|
||||||
|
|
||||||
## Lifecycle events summary
|
## Summary
|
||||||
|
|
||||||
Page lifecycle events:
|
Page load events:
|
||||||
|
|
||||||
- `DOMContentLoaded` event triggers on `document` when DOM is ready. We can apply JavaScript to elements at this stage.
|
- `DOMContentLoaded` event triggers on `document` when DOM is ready. We can apply JavaScript to elements at this stage.
|
||||||
- All inline scripts and scripts with `defer` are already executed.
|
- Script such as `<script>...</script>` or `<script src="..."></script>` block DOMContentLoaded, the browser waits for them to execute.
|
||||||
- Async scripts may execute both before and after the event, depends on when they load.
|
|
||||||
- Images and other resources may also still continue loading.
|
- Images and other resources may also still continue loading.
|
||||||
- `load` event on `window` triggers when the page and all resources are loaded. We rarely use it, because there's usually no need to wait for so long.
|
- `load` event on `window` triggers when the page and all resources are loaded. We rarely use it, because there's usually no need to wait for so long.
|
||||||
- `beforeunload` event on `window` triggers when the user wants to leave the page. If it returns a string, the browser shows a question whether the user really wants to leave or not.
|
- `beforeunload` event on `window` triggers when the user wants to leave the page. If we cancel the event, browser asks whether the user really wants to leave (e.g we have unsaved changes).
|
||||||
- `unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used.
|
- `unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used. We can send out a network request with `navigator.sendBeacon`.
|
||||||
- `document.readyState` is the current state of the document, changes can be tracked in the `readystatechange` event:
|
- `document.readyState` is the current state of the document, changes can be tracked in the `readystatechange` event:
|
||||||
- `loading` -- the document is loading.
|
- `loading` -- the document is loading.
|
||||||
- `interactive` -- the document is parsed, happens at about the same time as `DOMContentLoaded`, but before it.
|
- `interactive` -- the document is parsed, happens at about the same time as `DOMContentLoaded`, but before it.
|
197
2-ui/10-loading/02-script-async-defer/article.md
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
|
||||||
|
# Scripts: async, defer
|
||||||
|
|
||||||
|
In modern websites, scripts are often "heavier" than HTML: their download size is larger, and processing time is also longer.
|
||||||
|
|
||||||
|
When the browser loads HTML and comes across a `<script>...</script>` tag, it can't continue building DOM. It must execute the script right now. The same happens for external scripts `<script src="..."></script>`: the browser must wait until the script downloads, execute it, and only after process the rest of the page.
|
||||||
|
|
||||||
|
That leads to two important issues:
|
||||||
|
|
||||||
|
1. Scripts can't see DOM elements below them, so can't add handlers etc.
|
||||||
|
2. If there's a bulky script at the top of the page, it "blocks the page". Users can't see the page content till it downloads and runs:
|
||||||
|
|
||||||
|
```html run height=100
|
||||||
|
<p>...content before script...</p>
|
||||||
|
|
||||||
|
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
|
||||||
|
|
||||||
|
<!-- This isn't visible until the script loads -->
|
||||||
|
<p>...content after script...</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
There are some workarounds to that. For instance, we can put a script at the bottom of the page. Then it can see elements above it, and it doesn't block the page content from showing:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body>
|
||||||
|
...all content is above the script...
|
||||||
|
|
||||||
|
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
But this solution is far from perfect. For example, the browser actually notices the script (and can start downloading it) only after it downloaded the full HTML document. For long HTML documents, that may be a noticeable delay.
|
||||||
|
|
||||||
|
Such things are invisible for people using very fast connections, but many people in the world still have slower internet speeds and far-from-perfect mobile connectivity.
|
||||||
|
|
||||||
|
Luckily, there are two `<script>` attributes that solve the problem for us: `defer` and `async`
|
||||||
|
|
||||||
|
## defer
|
||||||
|
|
||||||
|
The `defer` attribute tells the browser that it should go on working with the page, and load the script "in background", then run the script when it loads.
|
||||||
|
|
||||||
|
Here's the same example as above, but with `defer`:
|
||||||
|
|
||||||
|
```html run height=100
|
||||||
|
<p>...content before script...</p>
|
||||||
|
|
||||||
|
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
|
||||||
|
|
||||||
|
<!-- visible immediately -->
|
||||||
|
<p>...content after script...</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Scripts with `defer` never block the page.
|
||||||
|
- Scripts with `defer` always execute when the DOM is ready, but before `DOMContentLoaded` event.
|
||||||
|
|
||||||
|
The following example demonstrates that:
|
||||||
|
|
||||||
|
```html run height=100
|
||||||
|
<p>...content before scripts...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!")); // (2)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
|
||||||
|
|
||||||
|
<p>...content after scripts...</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The page content shows up immediately.
|
||||||
|
2. `DOMContentLoaded` waits for the deferred script. It only triggers when the script `(2)` is downloaded is executed.
|
||||||
|
|
||||||
|
Deferred scripts keep their relative order, just like regular scripts.
|
||||||
|
|
||||||
|
So, if we have a long script first, and then a smaller one, then the latter one waits.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
|
||||||
|
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```smart header="The small script downloads first, runs second"
|
||||||
|
Browsers scan the page for scripts and download them in parallel, to improve performance. So in the example above both scripts download in parallel. The `small.js` probably makes it first.
|
||||||
|
|
||||||
|
But the specification requres scripts to execute in the document order, so it waits for `long.js` to execute.
|
||||||
|
```
|
||||||
|
|
||||||
|
```smart header="The `defer` attribute is only for external scripts"
|
||||||
|
The `defer` attribute is ignored if the script has no `src`.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## async
|
||||||
|
|
||||||
|
The `async` attribute means that a script is completely independant:
|
||||||
|
|
||||||
|
- The page doesn't wait for async scripts, the contents is processed and displayed.
|
||||||
|
- `DOMContentLoaded` and async scripts don't wait each other:
|
||||||
|
- `DOMContentLoaded` may happen both before an async script (if an async script finishes loading after the page is complete)
|
||||||
|
- ...or after an async script (if an async script is short or was in HTTP-cache)
|
||||||
|
- Other scripts don't wait for `async` scripts, and `async` scripts don't wait for them.
|
||||||
|
|
||||||
|
|
||||||
|
So, if we have several `async` scripts, they may execute in any order. Whatever loads first -- runs first:
|
||||||
|
|
||||||
|
```html run height=100
|
||||||
|
<p>...content before scripts...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
|
||||||
|
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
|
||||||
|
|
||||||
|
<p>...content after scripts...</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The page content shows up immediately: `async` doesn't block it.
|
||||||
|
2. `DOMContentLoaded` may happen both before and after `async`, no guarantees here.
|
||||||
|
3. Async scripts don't wait for each other. A smaller script `small.js` goes second, but probably loads before `long.js`, so runs first. That's called a "load-first" order.
|
||||||
|
|
||||||
|
Async scripts are great when we integrate an independant third-party script into the page: counters, ads and so on.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script async src="https://google-analytics.com/analytics.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Dynamic scripts
|
||||||
|
|
||||||
|
We can also create a script dynamically using Javascript:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = "/article/script-async-defer/long.js";
|
||||||
|
document.body.append(script); // (*)
|
||||||
|
```
|
||||||
|
|
||||||
|
The script starts loading as soon as it's appended to the document `(*)`.
|
||||||
|
|
||||||
|
**Dynamic scripts behave as "async" by default.**
|
||||||
|
|
||||||
|
That is:
|
||||||
|
- They don't wait for anything, nothing waits for them.
|
||||||
|
- The script that loads first -- runs first ("load-first" order).
|
||||||
|
|
||||||
|
We can change the load-first order into the document order by explicitly setting `async` to `false`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = "/article/script-async-defer/long.js";
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.async = false;
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
document.body.append(script);
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, here we add two scripts. Without `script.async=false` they would execute in load-first order (the `small.js` probably first). But with that flag the order is "as in the document":
|
||||||
|
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function loadScript(src) {
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
script.async = false;
|
||||||
|
document.body.append(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// long.js runs first because of async=false
|
||||||
|
loadScript("/article/script-async-defer/long.js");
|
||||||
|
loadScript("/article/script-async-defer/small.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Both `async` and `defer` have one common thing: they don't block page rendering. So the user can read page content and get acquanted with the page immediately.
|
||||||
|
|
||||||
|
But there are also essential differences between them:
|
||||||
|
|
||||||
|
| | Order | `DOMContentLoaded` |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `async` | *Load-first order*. Their document order doesn't matter -- which loads first | Irrelevant. May load and execute while the document has not yet been fully downloaded. That happens if scripts are small or cached, and the document is long enough. |
|
||||||
|
| `defer` | *Document order* (as they go in the document). | Execute after the document is loaded and parsed (they wait if needed), right before `DOMContentLoaded`. |
|
||||||
|
|
||||||
|
```warn header="Page without scripts should be usable"
|
||||||
|
Please note that if you're using `defer`, then the page is visible before the script loads and enables all the graphical components.
|
||||||
|
|
||||||
|
So, buttons should be disabled by CSS or by other means, to let the user
|
||||||
|
|
||||||
|
In practice, `defer` is used for scripts that need DOM and/or their relative execution order is important.
|
||||||
|
|
||||||
|
|
||||||
|
So `async` is used for independent scripts, like counters or ads, that don't need to access page content. And their relative execution order does not matter.
|
32
2-ui/10-loading/02-script-async-defer/long.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
// ...long js... ...long js... ...long js... ...long js... ...long js... ...long js...
|
||||||
|
|
||||||
|
alert("Long script loaded");
|
3
2-ui/10-loading/02-script-async-defer/small.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// ...small js...
|
||||||
|
|
||||||
|
alert("Small script loaded");
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
function setHandler() {
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
return "There are unsaved changes. Leave now?";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick="setHandler()">Set window.onbeforeunload</button>
|
||||||
|
|
||||||
|
<a href="http://example.com">Leave for EXAMPLE.COM</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
206
2-ui/10-loading/03-onload-onerror/article.md
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
# Resource loading: onload and onerror
|
||||||
|
|
||||||
|
The browser allows to track the loading of external resources -- scripts, iframes, pictures and so on.
|
||||||
|
|
||||||
|
There are two events for it:
|
||||||
|
|
||||||
|
- `onload` -- successful load,
|
||||||
|
- `onerror` -- an error occurred.
|
||||||
|
|
||||||
|
## Loading a script
|
||||||
|
|
||||||
|
Let's say we need to load a third-party script and call a function that resides there.
|
||||||
|
|
||||||
|
We can load it dynamically, like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = "my.js";
|
||||||
|
|
||||||
|
document.head.append(script);
|
||||||
|
```
|
||||||
|
|
||||||
|
...But how to run the function that is declared inside that script? We need to wait until the script loads, and only then we can call it.
|
||||||
|
|
||||||
|
```smart
|
||||||
|
For our own scripts we could use [Javascript modules](info:modules) here, but they are not widely adopted by third-party libraries.
|
||||||
|
```
|
||||||
|
|
||||||
|
### script.onload
|
||||||
|
|
||||||
|
The main helper is the `load` event. It triggers after the script was loaded and executed.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run untrusted
|
||||||
|
let script = document.createElement('script');
|
||||||
|
|
||||||
|
// can load any script, from any domain
|
||||||
|
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
|
||||||
|
document.head.append(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onload = function() {
|
||||||
|
// the script creates a helper function "_"
|
||||||
|
alert(_); // the function is available
|
||||||
|
};
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
So in `onload` we can use script variables, run functions etc.
|
||||||
|
|
||||||
|
...And what if the loading failed? For instance, there's no such script (error 404) or the server or the server is down (unavailable).
|
||||||
|
|
||||||
|
### script.onerror
|
||||||
|
|
||||||
|
Errors that occur during the loading of the script can be tracked on `error` event.
|
||||||
|
|
||||||
|
For instance, let's request a script that doesn't exist:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = "https://example.com/404.js"; // no such script
|
||||||
|
document.head.append(script);
|
||||||
|
|
||||||
|
*!*
|
||||||
|
script.onerror = function() {
|
||||||
|
alert("Error loading " + this.src); // Error loading https://example.com/404.js
|
||||||
|
};
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that we can't get HTTP error details here. We don't know was it error 404 or 500 or something else. Just that the loading failed.
|
||||||
|
|
||||||
|
```warn
|
||||||
|
Events `onload`/`onerror` track only the loading itself.
|
||||||
|
|
||||||
|
Errors during script processing and execution are out of the scope of these events. To track script errors, one can use `window.onerror` global handler.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other resources
|
||||||
|
|
||||||
|
The `load` and `error` events also work for other resources, basically for any resource that has an external `src`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = "https://js.cx/clipart/train.gif"; // (*)
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
alert(`Image loaded, size ${img.width}x${img.height}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = function() {
|
||||||
|
alert("Error occured while loading image");
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
There are some notes though:
|
||||||
|
|
||||||
|
- Most resources start loading when they are added to the document. But `<img>` is an exception. It starts loading when it gets an src `(*)`.
|
||||||
|
- For `<iframe>`, the `iframe.onload` event triggers when the iframe loading finished, both for successful load and in case of an error.
|
||||||
|
|
||||||
|
That's for historical reasons.
|
||||||
|
|
||||||
|
## Crossorigin policy
|
||||||
|
|
||||||
|
There's a rule: scripts from one site can't access contents of the other site. So, e.g. a script at `https://facebook.com` can't read the user's mailbox at `https://gmail.com`.
|
||||||
|
|
||||||
|
Or, to be more precise, one origin (domain/port/protocol triplet) can't access the content from another one. So even if we have a subdomain, or just another port, these are different origins, no access to each other.
|
||||||
|
|
||||||
|
This rule also affects resources from other domains.
|
||||||
|
|
||||||
|
If we're using a script from another domain, and there's an error in it, we can't get error details.
|
||||||
|
|
||||||
|
For example, let's take a script with a single (bad) function call:
|
||||||
|
```js
|
||||||
|
// 📁 error.js
|
||||||
|
noSuchFunction();
|
||||||
|
```
|
||||||
|
|
||||||
|
Now load it from our domain:
|
||||||
|
|
||||||
|
```html run height=0
|
||||||
|
<script>
|
||||||
|
window.onerror = function(message, url, line, col, errorObj) {
|
||||||
|
alert(`${message}\n${url}, ${line}:${col}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="/article/onload-onerror/crossorigin/error.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
We can see a good error report, like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Uncaught ReferenceError: noSuchFunction is not defined
|
||||||
|
https://javascript.info/article/onload-onerror/crossorigin/error.js, 1:1
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's load the same script from another domain:
|
||||||
|
|
||||||
|
```html run height=0
|
||||||
|
<script>
|
||||||
|
window.onerror = function(message, url, line, col, errorObj) {
|
||||||
|
alert(`${message}\n${url}, ${line}:${col}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The report is different, like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Script error.
|
||||||
|
, 0:0
|
||||||
|
```
|
||||||
|
|
||||||
|
Details may vary depeding on the browser, but the idea is same: any information about the internals of a script is hidden. Exactly because it's from another domain.
|
||||||
|
|
||||||
|
Why do we need the details?
|
||||||
|
|
||||||
|
There are many services (and we can build our own) that listen to `window.onerror`, save errors at the server and provide an interface to access and analyze them. That's great, as we can see real errors, triggered by our users. But we can't see any error information for scripts from other domains.
|
||||||
|
|
||||||
|
Similar cross-origin policy (CORS) is enforced for other types of resources as well.
|
||||||
|
|
||||||
|
**To allow cross-origin access, we need `crossorigin` attribute, plus the remote server must provide special headers.**
|
||||||
|
|
||||||
|
There are three levels of cross-origin access:
|
||||||
|
|
||||||
|
1. **No `crossorigin` attribute** -- access prohibited.
|
||||||
|
2. **`crossorigin="anonymous"`** -- access allowed if the server responds with the header `Access-Control-Allow-Origin` with `*` or our origin. Browser does not send authorization information and cookies to remote server.
|
||||||
|
3. **`crossorigin="use-credentials"`** -- access allowed if the server sends back the header `Access-Control-Allow-Origin` with our origin and `Access-Control-Allow-Credentials: true`. Browser sends authorization information and cookies to remote server.
|
||||||
|
|
||||||
|
```smart
|
||||||
|
You can read more about cross-origin access in the chapter <info:fetch-crossorigin>. It describes `fetch` method for network requests, but the policy is exactly the same.
|
||||||
|
|
||||||
|
Such thing as "cookies" is out of our current scope, but you can read about them in the chapter <info:cookie>.
|
||||||
|
```
|
||||||
|
|
||||||
|
In our case, we didn't have any crossorigin attribute. So the cross-origin access was prohibited. Let's add it.
|
||||||
|
|
||||||
|
We can choose between `"anonymous"` (no cookies sent, one server-side header needed) and `"use-credentials"` (sends cookies too, two server-side headers needed).
|
||||||
|
|
||||||
|
If we don't care about cookies, then `"anonymous"` is a way to go:
|
||||||
|
|
||||||
|
```html run height=0
|
||||||
|
<script>
|
||||||
|
window.onerror = function(message, url, line, col, errorObj) {
|
||||||
|
alert(`${message}\n${url}, ${line}:${col}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script *!*crossorigin="anonymous"*/!* src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, assuming that the server provides `Access-Control-Allow-Origin` header, everything's fine. We have the full error report.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Images `<img>`, external styles, scripts and other resources provide `load` and `error` events to track their loading:
|
||||||
|
|
||||||
|
- `load` triggers on a successful load,
|
||||||
|
- `error` triggers on a failed load.
|
||||||
|
|
||||||
|
The only exception is `<iframe>`: for historical reasons it always triggers `load`, for any load completion, even if the page is not found.
|
||||||
|
|
||||||
|
The `readystatechange` event also works for resources, but is rarely used, because `load/error` events are simpler.
|
|
@ -0,0 +1 @@
|
||||||
|
noSuchFunction();
|
2
2-ui/10-loading/index.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
# Document and resource loading
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
@ -1,91 +0,0 @@
|
||||||
# Resource loading: onload and onerror
|
|
||||||
|
|
||||||
The browser allows to track the loading of external resources -- scripts, iframes, pictures and so on.
|
|
||||||
|
|
||||||
There are two events for it:
|
|
||||||
|
|
||||||
- `onload` -- successful load,
|
|
||||||
- `onerror` -- an error occurred.
|
|
||||||
|
|
||||||
## Loading a script
|
|
||||||
|
|
||||||
Let's say we need to call a function that resides in an external script.
|
|
||||||
|
|
||||||
We can load it dynamically, like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let script = document.createElement('script');
|
|
||||||
script.src = "my.js";
|
|
||||||
|
|
||||||
document.head.append(script);
|
|
||||||
```
|
|
||||||
|
|
||||||
...But how to run the function that is declared inside that script? We need to wait until the script loads, and only then we can call it.
|
|
||||||
|
|
||||||
### script.onload
|
|
||||||
|
|
||||||
The main helper is the `load` event. It triggers after the script was loaded and executed.
|
|
||||||
|
|
||||||
For instance:
|
|
||||||
|
|
||||||
```js run untrusted
|
|
||||||
let script = document.createElement('script');
|
|
||||||
|
|
||||||
// can load any script, from any domain
|
|
||||||
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
|
|
||||||
document.head.append(script);
|
|
||||||
|
|
||||||
*!*
|
|
||||||
script.onload = function() {
|
|
||||||
// the script creates a helper function "_"
|
|
||||||
alert(_); // the function is available
|
|
||||||
};
|
|
||||||
*/!*
|
|
||||||
```
|
|
||||||
|
|
||||||
So in `onload` we can use script variables, run functions etc.
|
|
||||||
|
|
||||||
...And what if the loading failed? For instance, there's no such script (error 404) or the server or the server is down (unavailable).
|
|
||||||
|
|
||||||
### script.onerror
|
|
||||||
|
|
||||||
Errors that occur during the loading (but not execution) of the script can be tracked on `error` event.
|
|
||||||
|
|
||||||
For instance, let's request a script that doesn't exist:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
let script = document.createElement('script');
|
|
||||||
script.src = "https://example.com/404.js"; // no such script
|
|
||||||
document.head.append(script);
|
|
||||||
|
|
||||||
*!*
|
|
||||||
script.onerror = function() {
|
|
||||||
alert("Error loading " + this.src); // Error loading https://example.com/404.js
|
|
||||||
};
|
|
||||||
*/!*
|
|
||||||
```
|
|
||||||
|
|
||||||
Please note that we can't get error details here. We don't know was it error 404 or 500 or something else. Just that the loading failed.
|
|
||||||
|
|
||||||
## Other resources
|
|
||||||
|
|
||||||
The `load` and `error` events also work for other resources. There may be minor differences though.
|
|
||||||
|
|
||||||
For instance:
|
|
||||||
|
|
||||||
`<img>`, `<link>` (external stylesheets)
|
|
||||||
: Both `load` and `error` events work as expected.
|
|
||||||
|
|
||||||
`<iframe>`
|
|
||||||
: Only `load` event when the iframe loading finished. It triggers both for successful load and in case of an error. That's for historical reasons.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Pictures `<img>`, external styles, scripts and other resources provide `load` and `error` events to track their loading:
|
|
||||||
|
|
||||||
- `load` triggers on a successful load,
|
|
||||||
- `error` triggers on a failed load.
|
|
||||||
|
|
||||||
The only exception is `<iframe>`: for historical reasons it always triggers `load`, for any load completion, even if the page is not found.
|
|
||||||
|
|
||||||
The `readystatechange` event also works for resources, but is rarely used, because `load/error` events are simpler.
|
|
|
@ -1,3 +1,3 @@
|
||||||
# Events in details
|
# UI Events
|
||||||
|
|
||||||
Here we cover most important events and details of working with them.
|
Here we cover most important user interface events and how to work with them.
|
||||||
|
|
249
2-ui/99-misc/12-mutation-observer/article.md
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
|
||||||
|
# Mutation observer
|
||||||
|
|
||||||
|
`MutationObserver` is a built-in object that observes a DOM element and fires a callback in case of changes.
|
||||||
|
|
||||||
|
We'll first see syntax, and then explore a real-world use case.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
`MutationObserver` is easy to use.
|
||||||
|
|
||||||
|
First, we create an observer with a callback-function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let observer = new MutationObserver(callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
And then attach it to a DOM node:
|
||||||
|
|
||||||
|
```js
|
||||||
|
observer.observe(node, config);
|
||||||
|
```
|
||||||
|
|
||||||
|
`config` is an object with boolean options "what kind of changes to react on":
|
||||||
|
- `childList` -- changes in the direct children of `node`,
|
||||||
|
- `subtree` -- in all descendants of `node`,
|
||||||
|
- `attributes` -- attributes of `node`,
|
||||||
|
- `attributeOldValue` -- record the old value of attribute (infers `attributes`),
|
||||||
|
- `characterData` -- whether to observe `node.data` (text content),
|
||||||
|
- `characterDataOldValue` -- record the old value of `node.data` (infers `characterData`),
|
||||||
|
- `attributeFilter` -- an array of attribute names, to observe only selected ones.
|
||||||
|
|
||||||
|
Then after any changes, the `callback` is executed, with a list of [MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects as the first argument, and the observer itself as the second argument.
|
||||||
|
|
||||||
|
[MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects have properties:
|
||||||
|
|
||||||
|
- `type` -- mutation type, one of
|
||||||
|
- `"attributes"` (attribute modified)
|
||||||
|
- `"characterData"` (data modified)
|
||||||
|
- `"childList"` (elements added/removed),
|
||||||
|
- `target` -- where the change occured: an element for "attributes", or text node for "characterData", or an element for a "childList" mutation,
|
||||||
|
- `addedNodes/removedNodes` -- nodes that were added/removed,
|
||||||
|
- `previousSibling/nextSibling` -- the previous and next sibling to added/removed nodes,
|
||||||
|
- `attributeName/attributeNamespace` -- the name/namespace (for XML) of the changed attribute,
|
||||||
|
- `oldValue` -- the previous value, only for attribute or text changes.
|
||||||
|
|
||||||
|
|
||||||
|
For example, here's a `<div>` with `contentEditable` attribute. That attribute allows us to focus on it and edit.
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<div contentEditable id="elem">Edit <b>me</b>, please</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let observer = new MutationObserver(mutationRecords => {
|
||||||
|
console.log(mutationRecords); // console.log(the changes)
|
||||||
|
});
|
||||||
|
observer.observe(elem, {
|
||||||
|
// observe everything except attributes
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterDataOldValue: true
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
If we change the text inside `<b>me</b>`, we'll get a single mutation:
|
||||||
|
|
||||||
|
```js
|
||||||
|
mutationRecords = [{
|
||||||
|
type: "characterData",
|
||||||
|
oldValue: "me",
|
||||||
|
target: <text node>,
|
||||||
|
// other properties empty
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
If we select and remove the `<b>me</b>` altogether, we'll get multiple mutations:
|
||||||
|
|
||||||
|
```js
|
||||||
|
mutationRecords = [{
|
||||||
|
type: "childList",
|
||||||
|
target: <div#elem>,
|
||||||
|
removedNodes: [<b>],
|
||||||
|
nextSibling: <text node>,
|
||||||
|
previousSibling: <text node>
|
||||||
|
// other properties empty
|
||||||
|
}, {
|
||||||
|
type: "characterData"
|
||||||
|
target: <text node>
|
||||||
|
// ...details depend on how the browser handles the change
|
||||||
|
// it may coalesce two adjacent text nodes "Edit " and ", please" into one node
|
||||||
|
// or it can just delete the extra space after "Edit".
|
||||||
|
// may be one mutation or a few
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observer use case
|
||||||
|
|
||||||
|
When `MutationObserver` is needed? Is there a scenario when such thing can be useful?
|
||||||
|
|
||||||
|
Sure, we can track something like `contentEditable` and create "undo/redo" stack, but here's an example where `MutationObserver` is good from architectural standpoint.
|
||||||
|
|
||||||
|
Let's say we're making a website about programming, like this one. Naturally, articles and other materials may contain source code snippets.
|
||||||
|
|
||||||
|
An HTML code snippet looks like this:
|
||||||
|
```html
|
||||||
|
...
|
||||||
|
<pre class="language-javascript"><code>
|
||||||
|
// here's the code
|
||||||
|
let hello = "world";
|
||||||
|
</code></pre>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
There's also a JavaScript highlighting library, e.g. [Prism.js](https://prismjs.com/). A call to `Prism.highlightElem(pre)` examines the contents of such `pre` elements and adds colored syntax highlighting, similar to what you in examples here, this page.
|
||||||
|
|
||||||
|
Generally, when a page loads, e.g. at the bottom of the page, we can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// highlight all code snippets on the page
|
||||||
|
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the `<pre>` snippet looks like this (without line numbers by default):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// here's the code
|
||||||
|
let hello = "world";
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything's simple so far, right? There are `<pre>` code snippets in HTML, we highlight them.
|
||||||
|
|
||||||
|
Now let's go on. Let's say we're going to dynamically fetch materials from a server. We'll study methods for that [later in the tutorial](info:fetch-basics). For now it only matters that we fetch an HTML article from a webserver and display it on demand:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let article = /* fetch new content from server */
|
||||||
|
articleElem.innerHTML = article;
|
||||||
|
```
|
||||||
|
|
||||||
|
The new `article` HTML may contain code snippets. We need to call `Prism.highlightElem` on them, otherwise they won't get highlighted.
|
||||||
|
|
||||||
|
**Who's responsibility is to call `Prism.highlightElem` for a dynamically loaded article?**
|
||||||
|
|
||||||
|
We could append that call to the code that loads an article, like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let article = /* fetch new content from server */
|
||||||
|
articleElem.innerHTML = article;
|
||||||
|
|
||||||
|
*!*
|
||||||
|
let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
|
||||||
|
snippets.forEach(Prism.highlightElem);
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
...But imagine, we have many places where we load contents with code: articles, quizzes, forum posts. Do we need to put the highlighting call everywhere? Then we need to be careful, not to forget about it.
|
||||||
|
|
||||||
|
And what if we load the content into a third-party engine? E.g. we have a forum written by someone else, that loads contents dynamically, and we'd like to add syntax highlighting to it. No one likes to patch third-party scripts.
|
||||||
|
|
||||||
|
Luckily, there's another option.
|
||||||
|
|
||||||
|
We can use `MutationObserver` to automatically detect code snippets inserted in the page and highlight them.
|
||||||
|
|
||||||
|
So we'll handle the highlighting functionality in one place, relieving us from the need to integrate it.
|
||||||
|
|
||||||
|
## Dynamic highlight demo
|
||||||
|
|
||||||
|
Here's the working example.
|
||||||
|
|
||||||
|
If you run this code, it starts observing the element below and highlighting any code snippets that appear there:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let observer = new MutationObserver(mutations => {
|
||||||
|
|
||||||
|
for(let mutation of mutations) {
|
||||||
|
// examine new nodes
|
||||||
|
|
||||||
|
for(let node of mutation.addedNodes) {
|
||||||
|
// skip newly added text nodes
|
||||||
|
if (!(node instanceof HTMLElement)) continue;
|
||||||
|
|
||||||
|
// check the inserted element for being a code snippet
|
||||||
|
if (node.matches('pre[class*="language-"]')) {
|
||||||
|
Prism.highlightElement(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// search its subtree for code snippets
|
||||||
|
for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
|
||||||
|
Prism.highlightElement(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
let demoElem = document.getElementById('highlight-demo');
|
||||||
|
|
||||||
|
observer.observe(demoElem, {childList: true, subtree: true});
|
||||||
|
```
|
||||||
|
|
||||||
|
<p id="highlight-demo" style="border: 1px solid #ddd">Demo element with <code>id="highlight-demo"</code>, obverved by the example above.</p>
|
||||||
|
|
||||||
|
The code below populates `innerHTML`. If you've run the code above, snippets will get highlighted:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let demoElem = document.getElementById('highlight-demo');
|
||||||
|
|
||||||
|
// dynamically insert content with code snippets
|
||||||
|
demoElem.innerHTML = `A code snippet is below:
|
||||||
|
<pre class="language-javascript"><code> let hello = "world!"; </code></pre>
|
||||||
|
<div>Another one:</div>
|
||||||
|
<div>
|
||||||
|
<pre class="language-css"><code>.class { margin: 5px; } </code></pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we have `MutationObserver` that can track all highlighting in observed elements or the whole `document`. We can add/remove code snippets in HTML without thinking about it.
|
||||||
|
|
||||||
|
|
||||||
|
## Garbage collection
|
||||||
|
|
||||||
|
Observers use weak references to nodes internally. That is: if a node is removed from DOM, and becomes unreachable, then it becomes garbage collected, an observer doesn't prevent that.
|
||||||
|
|
||||||
|
Still, we can release observers any time:
|
||||||
|
|
||||||
|
- `observer.disconnect()` -- stops the observation.
|
||||||
|
|
||||||
|
Additionally:
|
||||||
|
|
||||||
|
- `mutationRecords = observer.takeRecords()` -- gets a list of unprocessed mutation records, those that happened, but the callback did not handle them.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// we're going to disconnect the observer
|
||||||
|
// it might have not yet handled some mutations
|
||||||
|
let mutationRecords = observer.takeRecords();
|
||||||
|
// process mutationRecords
|
||||||
|
|
||||||
|
// now all handled, disconnect
|
||||||
|
observer.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MutationObserver` can react on changes in DOM: attributes, added/removed elements, text content.
|
||||||
|
|
||||||
|
We can use it to track changes introduced by other parts of our own or 3rd-party code.
|
||||||
|
|
||||||
|
For example, to post-process dynamically inserted content, as demo `innerHTML`, like highlighting in the example above.
|
4
2-ui/99-misc/index.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
|
||||||
|
Not yet categorized articles.
|
231
8-custom-elements/1-custom-elements-intro/article.md
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
# Custom Elements
|
||||||
|
|
||||||
|
"Custom Elements" is a way to create new HTML elements and describe their properties, methods and constructor with JavaScript.
|
||||||
|
|
||||||
|
After we define a custom element, we can use it freely both in scripts or in generated HTML, on par with built-in HTML elements. The browser becomes responsible for calling proper methods when the element is created, added or removed from DOM, and that's very convenient.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## "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/).
|
||||||
|
|
||||||
|
Далее мы перейдём к изучению дополнительных возможностей по работе с DOM.
|
163
8-custom-elements/3-shadow-dom/article.md
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
# Shadow DOM
|
||||||
|
|
||||||
|
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) является отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
|
||||||
|
|
||||||
|
*Shadow DOM* -- это внутренний DOM элемента, который существует отдельно от внешнего документа. В нём могут быть свои ID, свои стили и так далее. Причём снаружи его, без применения специальных техник, не видно, поэтому не возникает конфликтов.
|
||||||
|
|
||||||
|
## Внутри браузера
|
||||||
|
|
||||||
|
Концепция Shadow DOM начала применяться довольно давно внутри самих браузеров. Когда браузер показывает сложные элементы управления, наподобие слайдера `<input type="range">` или календаря `<input type="date">` -- внутри себя он конструирует их из самых обычных стилизованных `<div>`, `<span>` и так далее.
|
||||||
|
|
||||||
|
С первого взгляда они незаметны, но если в настройках Chrome Development Tools выбрать показ Shadow DOM, то их можно легко увидеть.
|
||||||
|
|
||||||
|
Например, вот такое содержимое будет у `<input type="date">`:
|
||||||
|

|
||||||
|
|
||||||
|
То, что находится под `#shadow-root` -- это и есть Shadow DOM.
|
||||||
|
|
||||||
|
**Получить элементы из Shadow DOM можно только при помощи специальных JavaScript-вызовов или селекторов. Это не обычные дети, а намного более мощное средство отделения содержимого.**
|
||||||
|
|
||||||
|
В Shadow DOM выше можно увидеть полезный атрибут `pseudo`. Он нестандартный, существует по историческим причинам. С его помощью можно стилизовать подэлементы через CSS, например, сделаем поле редактирования даты красным:
|
||||||
|
|
||||||
|
```html run no-beautify
|
||||||
|
<style>
|
||||||
|
*!*
|
||||||
|
input::-webkit-datetime-edit {
|
||||||
|
*/!*
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<input type="date">
|
||||||
|
```
|
||||||
|
|
||||||
|
Ещё раз заметим, что `pseudo` -- нестандартный атрибут. Если говорить хронологически, то сначала браузеры начали экспериментировать внутри себя с инкапсуляцией внутренних DOM-структур, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое разработчикам.
|
||||||
|
|
||||||
|
Далее мы рассмотрим работу с Shadow DOM из JavaScript, по стандарту [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/).
|
||||||
|
|
||||||
|
## Создание Shadow DOM
|
||||||
|
|
||||||
|
Shadow DOM можно создать внутри любого элемента вызовом `elem.createShadowRoot()`.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```html run autorun="no-epub"
|
||||||
|
<p id="elem">Доброе утро, страна!</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var root = elem.createShadowRoot();
|
||||||
|
root.innerHTML = "<p>Привет из подполья!</p>";
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вы запустите этот пример, то увидите, что изначальное содержимое элемента куда-то исчезло и показывается только "Привет из подполья!". Это потому, что у элемента есть Shadow DOM.
|
||||||
|
|
||||||
|
**С момента создания Shadow DOM обычное содержимое (дети) элемента не отображается, а показывается только Shadow DOM.**
|
||||||
|
|
||||||
|
Внутрь этого Shadow DOM, при желании, можно поместить обычное содержимое. Для этого нужно указать, куда. В Shadow DOM это делается через "точку вставки" (insertion point). Она объявляется при помощи тега `<content>`, например:
|
||||||
|
|
||||||
|
```html run autorun="no-epub"
|
||||||
|
<p id="elem">Доброе утро, страна!</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var root = elem.createShadowRoot();
|
||||||
|
root.innerHTML = "<h3>*!*<content></content>*/!*</h3> <p>Привет из подполья!</p>";
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь вы увидите две строчки: "Доброе утро, страна!" в заголовке, а затем "Привет из подполья".
|
||||||
|
|
||||||
|
Shadow DOM примера выше в инструментах разработки:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Важные детали:
|
||||||
|
|
||||||
|
- Тег `<content>` влияет только на отображение, он не перемещает узлы физически. Как видно из картинки выше, текстовый узел "Доброе утро, страна!" остался внутри `p#elem`. Его можно даже получить при помощи `elem.firstElementChild`.
|
||||||
|
- Внутри `<content>` показывается не элемент целиком `<p id="elem">`, а его содержимое, то есть в данном случае текст "Доброе утро, страна!".
|
||||||
|
|
||||||
|
**В `<content>` атрибутом `select` можно указать конкретный селектор содержимого, которое нужно переносить. Например, `<content select="h3"></content>` перенесёт только заголовки.**
|
||||||
|
|
||||||
|
Внутри Shadow DOM можно использовать `<content>` много раз с разными значениями `select`, указывая таким образом, где конкретно какие части исходного содержимого разместить. Но при этом дублирование узлов невозможно. Если узел показан в одном `<content>`, то в следующем он будет пропущен.
|
||||||
|
|
||||||
|
Например, если сначала идёт `<content select="h3.title">`, а затем `<content select="h3">`, то в первом `<content>` будут показаны заголовки `<h3>` с классом `title`, а во втором -- все остальные, кроме уже показанных.</li>
|
||||||
|
|
||||||
|
В примере выше тег `<content></content>` внутри пуст. Если в нём указать содержимое, то оно будет показано только в том случае, если узлов для вставки нет. Например потому что ни один узел не подпал под указанный `select`, или все они уже отображены другими, более ранними `<content>`.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```html run autorun="no-epub" no-beautify
|
||||||
|
<section id="elem">
|
||||||
|
<h1>Новости</h1>
|
||||||
|
<article>Жили-были <i>старик со старухой</i>, но недавно...</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var root = elem.createShadowRoot();
|
||||||
|
|
||||||
|
root.innerHTML = "<content select='h1'></content> \
|
||||||
|
<content select='.author'>Без автора.</content> \
|
||||||
|
<content></content>";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick="alert(root.innerHTML)">root.innerHTML</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
При запуске мы увидим, что:
|
||||||
|
|
||||||
|
- Первый `<content select='h1'>` выведет заголовок.
|
||||||
|
- Второй `<content select=".author">` вывел бы автора, но так как такого элемента нет -- выводится содержимое самого `<content select=".author">`, то есть "Без автора".
|
||||||
|
- Третий `<content>` выведет остальное содержимое исходного элемента -- уже без заголовка `<h1>`, он выведен ранее!
|
||||||
|
|
||||||
|
Ещё раз обратим внимание, что `<content>` физически не перемещает узлы по DOM. Он только показывает, где их отображать, а также, как мы увидим далее, влияет на применение стилей.
|
||||||
|
|
||||||
|
## Корень shadowRoot
|
||||||
|
|
||||||
|
После создания корень внутреннего DOM-дерева доступен как `elem.shadowRoot`.
|
||||||
|
|
||||||
|
Он представляет собой специальный объект, поддерживающий основные методы CSS-запросов и подробно описанный в стандарте как [ShadowRoot](http://w3c.github.io/webcomponents/spec/shadow/#shadowroot-object).
|
||||||
|
|
||||||
|
Если нужно работать с содержимым в Shadow DOM, то нужно перейти к нему через `elem.shadowRoot`. Можно и создать новое Shadow DOM-дерево из JavaScript, например:
|
||||||
|
|
||||||
|
```html run autorun="no-epub"
|
||||||
|
<p id="elem">Доброе утро, страна!</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
*!*
|
||||||
|
// создать новое дерево Shadow DOM для elem
|
||||||
|
*/!*
|
||||||
|
var root = elem.createShadowRoot();
|
||||||
|
|
||||||
|
root.innerHTML = "<h3><content></content></h3> <p>Привет из подполья!</p> <hr>";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
*!*
|
||||||
|
// прочитать данные из Shadow DOM для elem
|
||||||
|
*/!*
|
||||||
|
var root = elem.shadowRoot;
|
||||||
|
// Привет из подполья!
|
||||||
|
document.write("<p>p:" + root.querySelector('p').innerHTML);
|
||||||
|
// пусто, так как физически узлы - вне content
|
||||||
|
document.write("<p>content:" + root.querySelector('content').innerHTML);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="Внутрь встроенных элементов так \"залезть\" нельзя"
|
||||||
|
На момент написания статьи `shadowRoot` можно получить только для Shadow DOM, созданного описанным выше способом, но не встроенного, как в элементах типа `<input type="date">`.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
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 и не заменяют существующие шаблонные системы, но дополняют их важными встроенными в браузер возможностями.
|
||||||
|
|
BIN
8-custom-elements/3-shadow-dom/shadow-content.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
8-custom-elements/3-shadow-dom/shadow-content@2x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
8-custom-elements/3-shadow-dom/shadow-dom-chrome.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
8-custom-elements/3-shadow-dom/shadow-dom-chrome@2x.png
Normal file
After Width: | Height: | Size: 48 KiB |
53
8-custom-elements/4-template-tag/article.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
|
||||||
|
# Шаблоны <template>
|
||||||
|
|
||||||
|
Элемент `<template>` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
|
||||||
|
|
||||||
|
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность `<template>`?
|
||||||
|
|
||||||
|
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только скрыто, но и считается находящимся вообще "вне документа". А при вставке автоматически "оживает", выполняются из него скрипты, начинает проигрываться видео и т.п.
|
||||||
|
|
||||||
|
Содержимое тега `<template>`, в отличие, к примеру, от шаблонов или `<script type="неизвестный тип">`, обрабатывается браузером. А значит, должно быть корректным HTML.
|
||||||
|
|
||||||
|
Оно доступно как `DocumentFragment` в свойстве тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
|
||||||
|
|
||||||
|
## Вставка шаблона
|
||||||
|
|
||||||
|
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
|
||||||
|
|
||||||
|
```html run autorun="no-epub"
|
||||||
|
<p id="elem">
|
||||||
|
Доброе утро, страна!</p>
|
||||||
|
|
||||||
|
<template id="tmpl">
|
||||||
|
<h3><content></content></h3>
|
||||||
|
<p>Привет из подполья!</p>
|
||||||
|
<script>
|
||||||
|
document.write('...document.write:Новость!');
|
||||||
|
</script>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var root = elem.createShadowRoot();
|
||||||
|
root.appendChild(tmpl.content.cloneNode(true));
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
У нас получилось, что:
|
||||||
|
|
||||||
|
1. В элементе `#elem` содержатся данные в некоторой оговорённой разметке.
|
||||||
|
2. Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.
|
||||||
|
3. Здесь шаблон показывается в Shadow DOM тега. Технически, это не обязательно, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег `<content>`.
|
||||||
|
|
||||||
|
Можно также заметить, что скрипт из шаблона выполнился. Это важнейшее отличие вставки шаблона от вставки HTML через `innerHTML` и от обычного `DocumentFragment`.
|
||||||
|
|
||||||
|
Также мы вставили не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
Тег `<template>` не призван заменить системы шаблонизации. В нём нет хитрых операторов итерации, привязок к данным.
|
||||||
|
|
||||||
|
Его основная особенность -- это возможность вставки "живого" содержимого, вместе со скриптами.
|
||||||
|
|
||||||
|
И, конечно, мелочь, но удобно, что он не требует никаких библиотек.
|
||||||
|
|
294
8-custom-elements/7-webcomponent-build/article.md
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
# Веб-компонент в сборе
|
||||||
|
|
||||||
|
В этой главе мы посмотрим на итоговый пример веб-компонента, включающий в себя описанные ранее технологии: 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) -- это самый известный из полифиллов на тему веб-компонент.
|
||||||
|
|
||||||
|
Он старается их эмулировать по возможности кросс-браузерно, но пока что это довольно-таки сложно, в частности, необходима дополнительная разметка.
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,2 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<!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>
|
3
8-custom-elements/index.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Web components
|
||||||
|
|
||||||
|
Web components is a set of standards to make self-contained components: custom HTML-elements with their own properties and methods, incapsulated DOM and styles.
|