This commit is contained in:
Ilya Kantor 2017-02-28 12:54:48 +03:00
parent 4272b7bb13
commit 508969c13f
168 changed files with 340 additions and 10 deletions

View file

@ -0,0 +1,349 @@
# Searching with getElement*, querySelector* and others
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 the name has dash inside, so the "simple" variable access won't work
alert(window['elem-content']); // but still possible to access with [...]
</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, because relies on global variables. The browser tries to help us by mixing namespaces of JS and DOM, but there may be conflicts.
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>
```
```smart header="There can be only one"
By the specification the value of `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 is unpredictable. The browser may return any of them at random. So please stick to the rule and keep `id` unique.
```
I will often use `id` to directly reference an element in examples, but that's only to keep things short. In real life `document.getElementById` is definitely the preferred method.
## getElementsBy*
There are also other methods of this kind:
`elem.getElementsByTagName(tag)` looks for elements with the given tag and returns the collection of them. The `tag` can also be `*` for any.
For instance:
```js
// get all divs in the document
let divs = document.getElementsByTagName('div');
```
Unlike `getElementById` that can be called only on `document`, this method can look inside any 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 only absent in `getElementById`, because we look for a single element. But `getElementsByTagName` returns a collection.
```
````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:
- `document.getElementsByName(name)` returns elements with the given `name` attribute. Rarely used.
- `elem.getElementsByClassName(className)` returns elements that have the given CSS class. Elements may have other classes too.
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];
let articles = form.getElementsByClassName('article');
alert( articles.length ); // 2, will find both elements with class "article"
</script>
```
## querySelectorAll [#querySelectorAll]
Now 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>
```
```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.
## 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` fits the CSS-selector. It returns `true` or `false`.
The method comes handy when we are walking 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>
for (let elem of document.body.children) {
*!*
if (elem.matches('a[href$="zip"]')) {
*/!*
alert("The archive reference: " + elem.href );
}
}
</script>
```
## closest
The method `elem.closest(css)` looks the nearest ancestor (parent or his parent and so on) 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 them. If it matches, then stops the search and returns it.
For instance:
```html run
<h1>Contents</h1>
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 1</li>
</ul>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL above our this LI
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.
For instance, here in the first script the length is `1`, because the browser only processed the first div.
Then later after the second `div` it's 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. Soon we'll see more ways to alter DOM.
## 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`. Or when it's the same element.