|
| 1 | +--- |
| 2 | +chapter: 28 |
| 3 | +description: Understanding DOM Manipulation in JavaScript. |
| 4 | +--- |
| 5 | + |
| 6 | +## DOM Manipulation |
| 7 | + |
| 8 | +While `window` represents the Browser, the **DOM** itself is represented by the [**`document`**](https://developer.mozilla.org/en-US/docs/Web/API/Document) global object—`document` _is_ the DOM (the _current_ HTML rendered in the browser). You access properties and call methods of this object in order to manipulate the content displayed in the browser! |
| 9 | + |
| 10 | +### Referencing HTML Elements {-} |
| 11 | + |
| 12 | +In order to manipulate the DOM elements in a page, you first need to get a _reference_ to the element you want to change—that is, you need a variable that refers to that element. You can get these variable references by using one of the `document` "selector" functions: |
| 13 | + |
| 14 | +```js |
| 15 | +//element with id="foo" |
| 16 | +let fooElem = document.getElementById("foo"); |
| 17 | + |
| 18 | +//elements with class="row" |
| 19 | +let rowElems = document.getElementsByClassName("row"); //note the plural! |
| 20 | + |
| 21 | +//<li> elements |
| 22 | +let liElems = document.getElementsByTagName("li"); //note the plural! |
| 23 | + |
| 24 | +/*easiest to select by reusing CSS selectors! */ |
| 25 | +let cssSelector = "header p, .title > p"; //a string of a CSS selector |
| 26 | + |
| 27 | +//selects FIRST element that matches css selector |
| 28 | +let elem = document.querySelector(cssSelector); |
| 29 | + |
| 30 | +//matches ALL elements that match css selector |
| 31 | +let elems = document.querySelectorAll(cssSelector); |
| 32 | +``` |
| 33 | + |
| 34 | +- The `document.querySelector()` is _by far_ the most flexible and easy to use of these methods: it can easily do the same as all the other methods (just put in an id, class, or element selector). **You should always use `querySelector()`**. |
| 35 | + |
| 36 | +- Note that the methods that return multiple nodes (e.g., `querySelectorAll`) return a [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) object. While this is like an array (you can access elements via index through bracket notation and it has a `.length` property), it is **not** an array: meaning it doesn't support methods like `forEach()` and `map()` across all browsers. If you need to iterate through a `NodeList`, you should use a regular `for` loop. But in practice, you're much more likely to only work with single elements at a time. |
| 37 | + |
| 38 | +### Modifying HTML Elements {-} |
| 39 | + |
| 40 | +Once you have a reference to an element, you access properties and call methods on that object in order to modify its state in the DOM—which will in turn modify how it _currently_ is displayed on the page. Thus by modifying these objects, you are dynamically changing the web page's content! |
| 41 | + |
| 42 | +<p class="alert alert-warning">**Important**: setting these properties do not change the `.html` source code file! Instead, they just change the _rendered DOM elements_ (think: the content stored in the computer's memory rather than in a file). If you refresh the page, the content will go back to how the `.html` source code file specifies it should appear—unless that also loads the script that modifies the DOM. What is shown on the page is the HTML with the JavaScript modifications added in.</p> |
| 43 | + |
| 44 | +#### Changing Content {-} |
| 45 | + |
| 46 | +You can use JavaScript to access and modify the **content** of a DOM element (e.g., the stuff between the start and close tags): |
| 47 | + |
| 48 | +```js |
| 49 | +//get a reference to the FIRST <p> element |
| 50 | +let elem = document.querySelector("p"); |
| 51 | + |
| 52 | +console.log(elem); //to demonstrate |
| 53 | + |
| 54 | +let text = elem.textContent; //the text content of the elem |
| 55 | +elem.textContent = "This is different content!"; //change the content |
| 56 | + |
| 57 | +let html = elem.innerHTML; //content including HTML |
| 58 | +elem.innerHTML = "This is <em>different</em> content!"; //interpreted as HTML |
| 59 | +``` |
| 60 | + |
| 61 | +The `textContent` property of the element refers to _all_ of the content, but considered as "plain text" this means that it is considered a "safe" property: you can assign strings that contain's HTML code (e.g., `<em>Hello</em>`), but that code will be escaped and not interpreted as HTML (instead the `<` and `>` will be written out as if you had used [HTML entities](https://www.w3schools.com/html/html_entities.asp)). The `.innerHTML` property, on the other hand, is "not safe": any HTML included in the String you assign to it will be converted into DOM elements. This makes it not a great property to use unless unless you are absolutely certain the content came from a trusted source. |
| 62 | + |
| 63 | +- The `innerHTML` property should be used primarily for including _inline_ elements such as `<em>` or `<strong>`. For more complex HTML content, it is cleaner code (separation of concerns!) to explicitly create new elements—see below for details. |
| 64 | + |
| 65 | +- You can "clear" the content of an element by setting it's content to be an empty string (`''`): |
| 66 | + |
| 67 | + ```js |
| 68 | + let alertElem = document.querySelector(".alert"); |
| 69 | + alertElem.textContent = ""; //no more alert! |
| 70 | + ``` |
| 71 | + |
| 72 | +#### Changing Attributes {-} |
| 73 | + |
| 74 | +You can also change the **attributes** of individual elements. Each attribute defined in the HTML specification is typically exposed as a _property_ of the element object: |
| 75 | + |
| 76 | +```js |
| 77 | +//get a reference to the `#picture` element |
| 78 | +let imgElem = document.querySelector("#picture"); |
| 79 | + |
| 80 | +//access the attribute |
| 81 | +console.log(imgElem.src); //logs the source of the image |
| 82 | + |
| 83 | +//modify the attribute |
| 84 | +imgElem.src = "my-picture.png"; |
| 85 | +``` |
| 86 | + |
| 87 | +<p class="alert alert-warning">You **cannot** access `element.class` or `element.style` attributes directly in this way; see below for specifics on changing the CSS of an element.</p> |
| 88 | + |
| 89 | +You can alternatively modify element attributes by using the methods `getAttribute()` (passing it which attribute to access) and `setAttribute()` (passing it which attribute to modify and what value to assign to that attribute): |
| 90 | + |
| 91 | +```js |
| 92 | +let imgElem = document.querySelector("#picture"); |
| 93 | +imgElement.setAttribute("src", "my-other-picture.png"); //set the src |
| 94 | + |
| 95 | +console.log(imgElem.getAttribute("src")); //=> 'my-other-picture.png' |
| 96 | + |
| 97 | +//the `hasAttribute()` method returns a boolean. |
| 98 | +let isThick = document.querySelector("svg rect").hasAttribute("stroke-width"); //chained anonymous variables |
| 99 | +``` |
| 100 | + |
| 101 | +These methods will let you interact with attributes that are _not_ defined by the HTML spec, such as `data-` attribute. However, they _don't_ work with certain element attributes (such as the `value` attribute of an `<input>` element). Other elements may have their own special DOM properties: see the [DOM Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) for a list of [HTML interfaces](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model#HTML_interfaces). |
| 102 | + |
| 103 | +#### Changing Element CSS {-} |
| 104 | + |
| 105 | +It is possible to modify the **CSS classes** (and even inline styling) of an element. But rather than using the `class` property like with other attributes, you instead access the **`classList`** property. On modern browsers (IE 10 or later), this property supports methods `.add()` and `.remove()` for adding and removing classes from the list: |
| 106 | + |
| 107 | +```js |
| 108 | +//access list of classes |
| 109 | +let classList = elem.classList; |
| 110 | + |
| 111 | +//add a class |
| 112 | +elem.classList.add("small"); //add a single class |
| 113 | +elem.classList.add("alert", "alert-warning"); //add multiples classes (not on IE) |
| 114 | + |
| 115 | +//remove a class |
| 116 | +elem.classList.remove("small"); |
| 117 | + |
| 118 | +//"toggle" (add if missing, remove if present) |
| 119 | +elem.classList.toggle("small"); |
| 120 | +``` |
| 121 | + |
| 122 | +- While IE 10+ does support these methods, it doesn't support _multiple arguments_ for them (so you can't add multiple classes in a single method call). If you need to support older browsers (including any version of IE), you can instead modify the `.className` property as if it were a String: |
| 123 | + |
| 124 | + ```js |
| 125 | + //fallback for IE (all) |
| 126 | + var classes = elem.className; |
| 127 | + classes += " " + "sweet sour"; //modify the string (append!) |
| 128 | + elem.className = classes; //reassign |
| 129 | + ``` |
| 130 | + |
| 131 | + The `classList` methods work perfectly on Microsoft Edge. |
| 132 | + |
| 133 | +It is also possible to access and modify individual CSS properties of elements through the DOM element's `style` property. `.style` references an Object whose keys are the CSS property names (but written in _camelCase_ instead of _kabob-case_) |
| 134 | + |
| 135 | +```js |
| 136 | +let h1 = document.querySelector("h1"); |
| 137 | +h1.style.color = "green"; |
| 138 | +h1.style.backgroundColor = "black"; //not `.background-color` |
| 139 | +``` |
| 140 | + |
| 141 | +<p class="alert alert-info">In general, you should modify element CSS by changing the class of the element, rather than specific style properties.</p> |
| 142 | + |
| 143 | +### Modifying the DOM Tree {-} |
| 144 | + |
| 145 | +In addition to modifying the individual DOM elements, it is also possible to access and modify the _DOM tree itself!_ That is, you can create new elements and add them to the tree (read: webpage), remove elements from the tree, or pluck them out of the tree and insert them somewhere else! |
| 146 | + |
| 147 | +First, note that each JavaScript DOM element has [ _read-only_ properties](https://www.w3schools.com/js/js_htmldom_navigation.asp) referring to its parent, children, and sibling elements: |
| 148 | + |
| 149 | +```html |
| 150 | +<main> |
| 151 | + <section id="first-section"> |
| 152 | + <p>First paragraph</p> |
| 153 | + <p>Second paragraph</p> |
| 154 | + </section> |
| 155 | + <section id="second-section"></section> |
| 156 | + <main></main> |
| 157 | +</main> |
| 158 | +``` |
| 159 | + |
| 160 | +```js |
| 161 | +//get reference to the first section |
| 162 | +let firstSection = document.querySelector("#first-section"); |
| 163 | + |
| 164 | +//get reference to the "parent" node |
| 165 | +let main = firstSection.parentElement; |
| 166 | +console.log(main); //<main>...</main> |
| 167 | + |
| 168 | +//get reference to the child elements (2 paragraphs) |
| 169 | +let paragraphs = firstSection.children; |
| 170 | +console.log(paragraphs.length); //2 |
| 171 | +console.log(paragraphs[0]); //<p>First paragraph</p> |
| 172 | + |
| 173 | +//get reference to the the next sibling |
| 174 | +let sectionSection = firstSection.nextElementSibling; |
| 175 | +console.log(secondSection); //<section id="second-section"></section> |
| 176 | +``` |
| 177 | + |
| 178 | +- Note that these properties only deal with _HTML elements_—text content nodes are ignored. You can instead use equivalent properties `parentNode` and `childNodes` to also consider text content nodes. |
| 179 | + |
| 180 | + SVG content doesn't support `parentElement`, but does support `parentNode`. |
| 181 | + |
| 182 | +You can also call methods to create and add new HTML DOM elements to the tree. The `document.createElement()` function is used to create a new HTML element. However this element is _not_ created as a part of the tree (after all, you haven't specified where it would put into the page)! Thus you need to also use a method such as `appendChild` to add that new element as a child of another element: |
| 183 | + |
| 184 | +```js |
| 185 | +//create a new <p> (not yet in the tree) |
| 186 | +let newP = document.createElement("p"); |
| 187 | +newP.textContent = "I'm new!"; |
| 188 | + |
| 189 | +//create Node of textContent only (not an HTML element, just text) |
| 190 | +let newText = document.createTextNode("I'm blank"); |
| 191 | + |
| 192 | +let main = document.querySelector("main"); |
| 193 | +main.appendChild(newP); //add element INSIDE (at end) |
| 194 | +main.appendChild(newText); //add the text inside, AFTER the <p> |
| 195 | + |
| 196 | +//add anonymous new node BEFORE element. Parameters are: (new, old) |
| 197 | +main.insertBefore(document.createTextNode("First!"), newP); |
| 198 | + |
| 199 | +//replace node. Parameters are: (new, old) |
| 200 | +main.replaceChild(document.createTextNode("boo"), newText); |
| 201 | + |
| 202 | +//remove node |
| 203 | +main.removeChild(main.querySelector("p")); |
| 204 | +``` |
| 205 | + |
| 206 | +The `appendChild()` method is considered a cleaner approach than just modifying the `innerHTML` property, as it allows you to adjust the DOM tree without erasing what was previously there. A common practice is to use `document.createElement()` to create a _block_ element, then set the `innerHTML` of that element to its content (which can include _inline_ elements), and then use `appendChild` to add the new block element to the tree at the desired location. |
| 207 | + |
| 208 | +### Accessibility {-} |
| 209 | + |
| 210 | +Whenever you learn a new technology, you should ask: **how does this affect accessibility?** With the JavaScript code modifying the rendered DOM, it is possible that the content of a page will change _after_ it has been read by a screen reader. And while a sighted user will likely be able to see the change visually, a screen reader has no way of knowing that something on the page is different unless you tell it. |
| 211 | + |
| 212 | +You can let screen readers know that an element in a page may have its content change _in the future_ by making that element into an [ARIA Live Region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). Live regions are "watched" by assistive technologies, and whenever the content changes they will speak the new content to the reader as if it were being read for the first time. |
| 213 | + |
| 214 | +You make an element into a live region by giving it the `aria-live` attribute: |
| 215 | + |
| 216 | +```html |
| 217 | +<div aria-live="polite">This content can change!</div> |
| 218 | +``` |
| 219 | + |
| 220 | +The value assigned to the `aria-live` attribute is the "politeness level", which specifies the priority by which the screen reader should read the change. The most common option (that you should almost always use) is `"polite"`, which indicates that the changed text will be read only once the user has _paused_ whatever is currently being read. A `"polite"` alert doesn't interrupt the currently being read text or description, but instead will be injected when there is a break (if the current reading goes on for too long, then the new content will not be spoken). |
| 221 | + |
| 222 | +- The other option is `"assertive"`, which indicates that the new content should be spoken as soon as it changes, possibly interrupting other content. This should only be used for important information (like alerts, warnings, or errors), as it can interrupt the user's flow in ways that are very disorienting. In short: _always be polite!_ |
0 commit comments