The browser’s internal model of our HTML is called the DOM Tree
It’s a hierarchy of objects of different types, such as:
Document:
This is the root node, and does not correspond to any HTML element.
HTMLElement:
Every HTML element, such as html, body, or em
is of this type. Usually, they merely inherit from HTMLElement,
and are an instance of a more specific type such as
HTMLHtmlElement,
HTMLBodyElement and so on.
Text:
Text nodes, such as "Hello ", "world", and "!" in our example.
These never contain any other element, they are always leaves.
Comment:
HTML comments (<!-- like this -->) are represented by objects of this type.
This hierarchy of objects is crucial for CSS styling, as we will see in a couple lectures.
We can interact with, and manipulate the DOM tree via JavaScript!
Let’s unpack this…
>
$$("h1, h2, h3")
< [h1,
h2,
h3#general-information, ...]
DOM element class hiearchy
HTMLHeadingELement
HTMLElement
Element
Node
EventTarget
Object
DOM querying
let selector = "h1, h2, h3, h4, h5, h6";
// Get all headings in the current document
let headings = document.querySelectorAll(selector)
// Get the first heading in the current document
let firstHeading = document.querySelector(selector)
- “DOM querying” is the process of obtaining references to one or more DOM elements that you want to do stuff with.
- In older code you may find other DOM querying methods, such as `document.getElementById()`, `document.getElementsByTagName()`,
`document.getElementsByClassName()`, `document.getElementsByName()`.
These preceded the ability to be able to query by CSS selector, and are now rarely used.
IDs create global variables
<button id="submit">Submit</button>
console.log(submit, window['submit']);
This is very useful for rapid iteration, testcases etc.
Note that this is why you should avoid using untrusted (e.g. user-generated) content as ids (and if you have to, make sure the id has a fixed prefix).
This particular security vulnerability is called [DOM clobbering](https://portswigger.net/web-security/dom-based/dom-clobbering).
What if we wanted to change the titles of all Monday lectures?
3 ways to iterate
Classic C-style for
Low level, trades readability for power
for (let i=0;i<mondayLectures.length; i++) {
const lecture = mondayLectures[i];
lecture.textContent = "I hate Mondays 😫";
}
for...in
Iterate over object properties
for (const i in mondayLectures) {
const lecture = mondayLectures[i];
lecture.textContent = "I hate Mondays 😫";
}
for...of
Iterate over items of iterable objects
for (const lecture of mondayLectures) {
lecture.textContent = "I hate Mondays 😫";
}
Activity: Remove all CSS from a page
Hint: element.remove() removes an element from the DOM
We’ve removed external stylesheets and style elements. What remains?
Where do removed DOM elements go?
We can convert our CSS removal code to a bookmarklet!
Use $$() in your scripts
function $$(selector) {
let elements = document.querySelectorAll(selector);
return Array.from(elements);
}
The only difference between the `$$()` we've used in the console and `document.querySelectorAll()` that is available everywhere
is that the latter returns a `NodeList`, not an array.
NodeLists are like arrays in the sense that they have indexed properties and a `length` property, and they are [iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)
but they are more limited in the sense that they lack [all the array methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods).
You can convert any array-like object to an array by using `Array.from()`.
Another way would be to use the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax):
```js
function $$(selector) {
return [...document.querySelectorAll(selector)];
}
```
DOM Traversal
DOM Manipulation
Insert and remove nodes
DOM Manipulation
Attributes
DOM Manipulation
String-based
These methods are very useful for writing lots of HTML in one fell swoop, and every fast.
However, be careful: they create new elements, and throw your existing elements into DOM hyperspace, ready to be garbage collected (unless there are references to them).
This means that any existing references to these elements will now point at different, dangling elements!
Events
What are events?
Code that is executed non-linearly when something happens
If the button with id="clickme" gets clicked,
change its label to "Clicked"
DOM Events
<button id="clickme">Click me</button>
clickme.addEventListener("click", function handler(event) {
event.target.textContent = "Clicked";
});
Events allow you to run code when something happens.
In JS, you attach event listeners with element.addEventListener, where element is a reference to your element in the DOM tree.
Its mandatory arguments are the name of the event as a string, and a function to be executed.
When the function is called, it will receive the Event Object as an argument, which contains information about what happened.
They can be variable values, function arguments, return values etc
They can even have properties, just like any other object
Deep 🐇 hole. We will learn more about it after spring break
let handler = function (evt) {
evt.target.textContent = "Clicked";
};
clickme.addEventListener("click", handler);
Using functions as arguments is super useful when we want to pass code around that will be executed at a later point.
Events are one example of this, but not the only one.
Arrow functions
let handler = evt => evt.target.textContent = "Clicked";
clickme.addEventListener("click", handler);
A shorter way to declare functions.
There are a few differences from function declarations, that we will explore later in the semester.
The right is an older way. It has the advantage of being shorter and more straightforward,
so it can be useful for rapid prototyping.
However, with that syntax you can only have one listener for any given type, so it's quite restrictive.
To remove a function, you need to store a reference to it.
Just specifying the same anonymous function doesn't work, because these are different objects.
document.addEventListener("mousemove", evt => {
let x = 100 * evt.x / innerWidth;
let y = 100 * evt.y / innerHeight;
document.body.style.backgroundImage = `radial-gradient(
at ${x}% ${y}%,
black,
transparent
)`;
});
CSS for presentation, JS for computation
body {
background-image: radial-gradient(
at calc(var(--mouse-x, .5) * 100%)
calc(var(--mouse-y, .5) * 100%),
transparent, black
);
}
document.addEventListener("mousemove", evt => {
let x = evt.x / innerWidth;
let y = evt.y / innerHeight;
let root = document.documentElement;
root.style.setProperty("--mouse-x", x);
root.style.setProperty("--mouse-y", y);
});
Separation of concerns
Avoid manipulating CSS properties from JS
If you're applying static styles, just toggle a class
If the styles are dynamic, set CSS variables
Set CSS variables to pure data, not CSS values like "10px" or "50%"
addColor.addEventListener("click", evt => {
let template = palette.querySelector("template");
let item = template.content.cloneNode(true);
let del = item.querySelector(".delete");
del.addEventListener("click", e => {
e.target.closest(".item").remove();
});
palette.append(item);
});document.addEventListener("click", evt => {
if (evt.target.matches(".item .delete")) {
evt.target.closest(".item").remove();
}
else if (evt.target.matches(".items + .add-item")) {
let list = evt.target.previousElementSibling;
let template = list.querySelector("template");
let item = template.content.cloneNode(true);
list.append(item);
}
});
Event delegation
Decoupling: Event-handling code separate from element creation code
name.addEventListener("input", evt => {
console.log(evt.target.value);
});
name.value = "Lea";
let evt = new InputEvent("input");
name.dispatchEvent(evt);
We can fire our own synthetic events when we programmatically manipulate the DOM.
Do note that unless we are very careful, these will not be as "detailed" as the native ones,
e.g. this one is missing an `inputType` property to tell us what kind of edit actually happened.
We can distinguish "real" events from synthetic ones through the `isTrusted`
We can also make our own events!
let evt = new CustomEvent("itemadded", {
detail: {index: 2}
});
list.dispatchEvent(evt);
Custom events on custom objects!
class GithubAPI extends EventTarget {
constructor() {
super();
}
login() {
// ...
let evt = new CustomEvent("login", {name: "Lea"});
this.dispatchEvent(evt);
}
}
let github = new GithubAPI();
github.addEventListener("login", evt => {
greeting.textContent = `Hi ${evt.detail.name}!`; // display user info
});
It allows us module B to react to things module A does, without actually having to modify module A's code.
Examples of custom events:
- [Shoelace tab panel](https://shoelace.style/components/tab-group?id=events) (explore the other components as well!)
- [jQuery UI dialog](https://api.jqueryui.com/dialog/) (explore the others too)
- [Dragula (drag & drop library) events](https://github.com/bevacqua/dragula#drakeon-events)