This commit is contained in:
Ilya Kantor 2017-03-21 17:14:05 +03:00
parent ab9ab64bd5
commit 97c8f22bbb
289 changed files with 195 additions and 172 deletions

View file

@ -1,36 +0,0 @@
There are many ways to do it.
Here are some of them:
```js
// 1. The table with `id="age-table"`.
let table = document.getElementById('age-table')
// 2. All label elements inside that table
table.getElementsByTagName('label')
// or
document.querySelectorAll('#age-table label')
// 3. The first td in that table (with the word "Age").
table.rows[0].cells[0]
// or
table.getElementsByTagName('td')[0]
// or
table.querySelector('td')
// 4. The form with the name "search".
// assuming there's only one element with name="search"
let form = document.getElementsByName('search')[0]
// or, form specifically
document.querySelector('form[name="search"]')
// 5. The first input in that form.
form.getElementsByTagName('input')
// or
form.querySelector('input')
// 6. The last input in that form.
// there's no direct query for that
let inputs = form.querySelectorAll('input') // search all
inputs[inputs.length-1] // take last
```

View file

@ -1,42 +0,0 @@
<!DOCTYPE HTML>
<html>
<body>
<form name="search">
<label>Search the site:
<input type="text" name="search">
</label>
<input type="submit" value="Search!">
</form>
<hr>
<form name="search-person">
Search the visitors:
<table id="age-table">
<tr>
<td>Age:</td>
<td id="age-list">
<label>
<input type="radio" name="age" value="young">less than 18</label>
<label>
<input type="radio" name="age" value="mature">18-50</label>
<label>
<input type="radio" name="age" value="senior">more than 50</label>
</td>
</tr>
<tr>
<td>Additionally:</td>
<td>
<input type="text" name="info[0]">
<input type="text" name="info[1]">
<input type="text" name="info[2]">
</td>
</tr>
</table>
<input type="submit" value="Search!">
</form>
</body>
</html>

View file

@ -1,18 +0,0 @@
importance: 4
---
# Search for elements
Here's the document with the table and form.
How to find?
1. The table with `id="age-table"`.
2. All `label` elements inside that table (there should be 3 of them).
3. The first `td` in that table (with the word "Age").
4. The `form` with the name `search`.
5. The first `input` in that form.
6. The last `input` in that form.
Open the page [table.html](table.html) in a separate window and make use of browser tools for that.

View file

@ -1,19 +0,0 @@
Let's make a loop over `<li>`:
```js
for (let li of document.querySelector('li')) {
...
}
```
In the loop we need to get the text inside every `li`. We can read it directly from the first child node, that is the text node:
```js
for (let li of document.querySelector('li')) {
let title = li.firstChild.data;
// title is the text in <li> before any other nodes
}
```
Then we can get the number of descendants `li.getElementsByTagName('li')`.

View file

@ -1,57 +0,0 @@
<!DOCTYPE HTML>
<html>
<body>
<ul>
<li>Animals
<ul>
<li>Mammals
<ul>
<li>Cows</li>
<li>Donkeys</li>
<li>Dogs</li>
<li>Tigers</li>
</ul>
</li>
<li>Other
<ul>
<li>Snakes</li>
<li>Birds</li>
<li>Lizards</li>
</ul>
</li>
</ul>
</li>
<li>Fishes
<ul>
<li>Aquarium
<ul>
<li>Guppy</li>
<li>Angelfish</li>
</ul>
</li>
<li>Sea
<ul>
<li>Sea trout</li>
</ul>
</li>
</ul>
</li>
</ul>
<script>
for (let li of document.querySelector('li')) {
// get the title from the text node
let title = li.firstChild.data;
title = title.trim(); // remove extra spaces from ends
// get the descendants count
let count = li.getElementsByTagName('li').length;
alert(title + ': ' + count);
}
</script>
</body>
</html>

View file

@ -1,47 +0,0 @@
<!DOCTYPE HTML>
<html>
<body>
<ul>
<li>Animals
<ul>
<li>Mammals
<ul>
<li>Cows</li>
<li>Donkeys</li>
<li>Dogs</li>
<li>Tigers</li>
</ul>
</li>
<li>Other
<ul>
<li>Snakes</li>
<li>Birds</li>
<li>Lizards</li>
</ul>
</li>
</ul>
</li>
<li>Fishes
<ul>
<li>Aquarium
<ul>
<li>Guppy</li>
<li>Angelfish</li>
</ul>
</li>
<li>Sea
<ul>
<li>Sea trout</li>
</ul>
</li>
</ul>
</li>
</ul>
<script>
// ... your code...
</script>
</body>
</html>

View file

@ -1,14 +0,0 @@
importance: 5
---
# Count descendants
There's a tree structured as nested `ul/li`.
Write the code that for each `<li>` shows:
1. What's the text inside it (without the subtree)
2. The number of nested `<li>` -- all descendants, including the deeply nested ones.
[demo src="solution"]

View file

@ -1,380 +0,0 @@
# Searching: getElement* and querySelector*
DOM navigation properties are great when elements are close to each other. What if they are not? How to get an arbitrary element of the page?
There are additional searching methods for that.
[cut]
## document.getElementById or just id
If an element has the `id` attribute, then there's a global variable by the name from that `id`.
We can use it to access the element, like this:
```html run
<div id="*!*elem*/!*">
<div id="*!*elem-content*/!*">Element</div>
</div>
<script>
alert(elem); // DOM-element with id="elem"
alert(window.elem); // accessing global variable like this also works
// for elem-content things are a bit more complex
// that has a dash inside, so it can't be a variable name
alert(window['elem-content']); // ...but accessable using square brackets [...]
</script>
```
That's unless we declare the same-named variable by our own:
```html run untrusted height=0
<div id="elem"></div>
<script>
let elem = 5;
alert(elem); // the variable overrides the element
</script>
```
The behavior is described [in the specification](http://www.whatwg.org/specs/web-apps/current-work/#dom-window-nameditem), but it is supported mainly for compatibility. The browser tries to help us by mixing namespaces of JS and DOM. Good for very simple scripts, but there may be name conflicts. Also, when we look in JS and don't have HTML in view, it's not obvious where the variable comes from.
The better alternative is to use a special method `document.getElementById(id)`.
For instance:
```html run
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
*!*
let elem = document.getElementById('elem');
*/!*
elem.style.background = 'red';
</script>
```
Here in the tutorial we'll often use `id` to directly reference an element, but that's only to keep things short. In real life `document.getElementById` is the preferred method.
```smart header="There can be only one"
The `id` must be unique. There can be only one element in the document with the given `id`.
If there are multiple elements with the same `id`, then the behavior of corresponding methods is unpredictable. The browser may return any of them at random. So please stick to the rule and keep `id` unique.
```
```warn header="Only `document.getElementById`, not `anyNode.getElementById`"
The method `getElementById` that can be called only on `document` object. It looks for the given `id` in the whole document.
```
## getElementsBy*
There are also other methods to look for nodes:
- `elem.getElementsByTagName(tag)` looks for elements with the given tag and returns the collection of them. The `tag` parameter can also be a star `"*"` for "any tags".
For instance:
```js
// get all divs in the document
let divs = document.getElementsByTagName('div');
```
This method is callable in the context of any DOM element.
Let's find all `input` inside the table:
```html run height=50
<table id="table">
<tr>
<td>Your age:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked> less than 18
</label>
<label>
<input type="radio" name="age" value="mature"> from 18 to 50
</label>
<label>
<input type="radio" name="age" value="senior"> more than 60
</label>
</td>
</tr>
</table>
<script>
*!*
let inputs = table.getElementsByTagName('input');
*/!*
for (let input of inputs) {
alert( input.value + ': ' + input.checked );
}
</script>
```
```warn header="Don't forget the `\"s\"` letter!"
Novice developers sometimes forget the letter `"s"`. That is, they try to call `getElementByTagName` instead of <code>getElement<b>s</b>ByTagName</code>.
The `"s"` letter is absent in `getElementById`, because it returns a single element. But `getElementsByTagName` returns a collection of elements, so there's `"s"` inside.
```
````warn header="It returns a collection, not an element!"
Another widespread novice mistake is to write like:
```js
// doesn't work
document.getElementsByTagName('input').value = 5;
```
That won't work, because it takes a *collection* of inputs and assigns the value to it, rather to elements inside it.
We should either iterate over the collection or get an element by the number, and then assign, like this:
```js
// should work (if there's an input)
document.getElementsByTagName('input')[0].value = 5;
```
````
There are also other rarely used methods of this kind:
- `elem.getElementsByClassName(className)` returns elements that have the given CSS class. Elements may have other classes too.
- `document.getElementsByName(name)` returns elements with the given `name` attribute, document-wide. Exists for historical reasons, very rarely used, we mention it here only for completeness.
For instance:
```html run height=50
<form name="my-form">
<div class="article">Article</div>
<div class="long article">Long article</div>
</form>
<script>
// find by name attribute
let form = document.getElementsByName('my-form')[0];
// find by class inside the form
let articles = form.getElementsByClassName('article');
alert(articles.length); // 2, found two elements with class "article"
</script>
```
## querySelectorAll [#querySelectorAll]
Now goes the heavy artillery.
The call to `elem.querySelectorAll(css)` returns all elements inside `elem` matching the given CSS selector. That's the most often used and powerful method.
Here we look for all `<li>` elements that are last children:
```html run
<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
<script>
*!*
let elements = document.querySelectorAll('ul > li:last-child');
*/!*
for (let elem of elements) {
alert(elem.innerHTML); // "test", "passed"
}
</script>
```
This method is indeed powerful, because any CSS selector can be used.
```smart header="Can use pseudo-classes as well"
Pseudo-classes in the CSS selector like `:hover` and `:active` are also supported. For instance, `document.querySelectorAll(':hover')` will return the collection with elements that the pointer is over now (in nesting order: from the outmost `<html>` to the most nested one).
```
## querySelector [#querySelector]
The call to `elem.querySelector(css)` returns the first element for the given CSS selector.
In other words, the result is the same as `elem.querySelectorAll(css)[0]`, but the latter is looking for *all* elements and picking one, while `elem.querySelector` just looks for one. So it's faster and shorter to write.
## matches
Previous methods were searching the DOM.
The [elem.matches(css)](http://dom.spec.whatwg.org/#dom-element-matches) does not look for anything, it merely checks if `elem` matches the given CSS-selector. It returns `true` or `false`.
The method comes handy when we are iterating over elements (like in array or something) and trying to filter those that interest us.
For instance:
```html run
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
// can be any collection instead of document.body.children
for (let elem of document.body.children) {
*!*
if (elem.matches('a[href$="zip"]')) {
*/!*
alert("The archive reference: " + elem.href );
}
}
</script>
```
## closest
All elements that are directly above the given one are called its *ancestors*.
In other words, ancestors are: parent, the parent of parent, its parent and so on. The ancestors together form the chain of parents from the element to the top.
The method `elem.closest(css)` looks the nearest ancestor that matches the CSS-selector. The `elem` itself is also included in the search.
In other words, the method `closest` goes up from the element and checks each of parents. If it matches the selector, then the search stops, and the ancestor is returned.
For instance:
```html run
<h1>Contents</h1>
<div class="contents">
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 1</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
alert(chapter.closest('h1')); // null (because h1 is not an ancestor)
</script>
```
## Live collections
All methods `"getElementsBy*"` return a *live* collection. Such collections always reflect the current state of the document and "auto-update" when it changes.
In the example below, there are two scripts.
1. The first one creates a reference to the collection of `<div>`. As of now, it's length is `1`.
2. The second scripts runs after the browser meets one more `<div>`, so it's length is `2`.
```html run
<div>First div</div>
<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
*!*
alert(divs.length); // 2
*/!*
</script>
```
In contrast, `querySelectorAll` returns a *static* collection. It's like a fixed array of elements.
If we use it instead, then both scripts output `1`:
```html run
<div>First div</div>
<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
*!*
alert(divs.length); // 1
*/!*
</script>
```
Now we can easily see the difference. The static collection did not increase after the appearance of a new `div` in the document.
Here we used separate scripts to illustrate how the element addition affects the collection, but any DOM manipulations affect them. Soon we'll see more of them.
## Summary
There are 6 main methods to search for nodes in DOM:
<table>
<thead>
<tr>
<td>Method</td>
<td>Searches by...</td>
<td>Can call on an element?</td>
<td>Live?</td>
</tr>
</thead>
<tbody>
<tr>
<td><code>getElementById</code></td>
<td><code>id</code></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td><code>getElementsByName</code></td>
<td><code>name</code></td>
<td>-</td>
<td></td>
</tr>
<tr>
<td><code>getElementsByTagName</code></td>
<td>tag or <code>'*'</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>getElementsByClassName</code></td>
<td>class</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>querySelector</code></td>
<td>CSS-selector</td>
<td></td>
<td>-</td>
</tr>
<tr>
<td><code>querySelectorAll</code></td>
<td>CSS-selector</td>
<td></td>
<td>-</td>
</tr>
</tbody>
</table>
Please note that methods `getElementById` and `getElementsByName` can only be called in the context of the document: `document.getElementById(...)`. But not on an element: `elem.getElementById(...)` would cause an error.
Other methods can be called on elements too. For instance `elem.querySelectorAll(...)` will search inside `elem` (in the DOM subtree).
Besides that:
- There is `elem.matches(css)` to check if `elem` matches the given CSS selector.
- There is `elem.closest(css)` to look for a nearest ancestor that matches the given CSS-selector. The `elem` itself is also checked.
And let's mention one more method here to check for the child-parent relationship:
- `elemA.contains(elemB)` returns true if `elemB` is inside `elemA` (a descendant of `elemA`) or when `elemA==elemB`.