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
DOM involves a bunch of classes in a hierarchy
each instance in a subclass inherits all the properties and methods of the parent class
HTMLElements are basically things that are defined by html tags
Element includes things that aren't HTML such as SVGElement which include circle, animate, mask, and other graphical elements
Nodes include other parts of the DOM like text nodes and comment nodes
EventTargets are things that can have events happen to them (we'll discuss later)
HTMLHeadingELement
HTMLElement
Element
Node
EventTarget
Object
The DOM Environment and Global Variables/Objects
Your javascript runs in an "environment" defined for the window.
Some global variables and objects are already defined there before your code starts running
window is a global variable holding an object that represents the window
window has properties like document and methods like close()
every child property of window becomes a global variable!
basically, the window object is your global environment
window: {
innerHeight: /* height of window in pixels */,
close: function () {
/* closes window if invoked */ },
…
document: { /* object representing document */
title: /* title of document */
location: /* url of document */
head: /* HTMLElement representing head*/,
body: /* HTMLElement representing body*/
…
}
}
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
let over = evt => output.innerHTML = evt.target.innerHTML;
let out = evt => output.innerHTML = "";
button1.addEventListener("mouseover", over);
button2.addEventListener("mouseover", over);
button1.addEventListener("mouseout", out);
button2.addEventListener("mouseout", out);
We wrote this code with the intention to show the text of the button that the mouse is over. But it doesn't work!
Hover over the "me" in both buttons. What do you notice?
Sometimes bubbling is not actually desirable.
Not all events bubble
Bubbling is just another heuristic!
focus(but focusin does!)
blur(but focusout does!)
mouseenter(but mouseover does!)
mouseleave(but mouseout does!)
load
error
element.addEventListener(eventName, evt => {
if (evt.bubbles) {
evt.stopPropagation();
}
})
Event capturing
Events start off at document and propagate down to target
What happens when you press ⌘+S (or Ctrl + S) in Google Docs?
Preventing default actions
element.addEventListener("keyup", evt => {
if (evt.key === "S" and (evt.metaKey or evt.ctrlKey)) {
evt.preventDefault();
myApp.save();
}
})
In Vue, you can just append a [`.prevent`](https://vuejs.org/guide/essentials/event-handling.html#event-modifiers) modifier to the event name.
Preventing default actions
You can, but should you?
No hard and fast rule
Match user expectations
Don't remove feedback without replacing it
What about keyboard shortcuts that conflict with the browser’s?
For keyboard shortcuts:
- Probability of app action vs browser action
- Compromise: Opt-in or opt-out
Don't be annoying
Name: <input id=textfield>
textfield.addEventListener("keypress", evt => {
if (evt.key < "A" || evt.key > "Z") {
evt.preventDefault();
}
});
Often you want to restrict input to match a specific pattern (e.g. letters only or a specific length).
While it is easy to simply prevent letters from being entered, it is far better to let
the user type whatever they want and clearly communicate when their input is incorrect.
Not only does this provide better *feedback* (it is very confusing to press a key and have nothing happen),
users often prefer to tweak input to the correct format rather than re-entering it.
Bottom line is, always be mindful of the user experience you are creating, and use `preventDefault()` sparingly.
Some events cannot be prevented
unload(but beforeunload can!)
input(but its composite raw events can!)
scroll(use overflow: hidden;)
select(use user-select: none;)
fullscreenchange
resize
element.addEventListener(eventName, evt => {
if (!evt.cancelable) {
console.log("This event cannot be prevented!");
}
})
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)