Lecture 16 DOM & Events

DOM & Event

The Document Object Model (DOM)


			<!DOCTYPE html>
			<html>
			<head><title>Hello world</title></head>
			<body><p>Hello <em>world</em> 👋</p></body>
			</html>
		

Our DOM Tree

Document node Element nodes Text nodes
  • 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)

The DOM Environment and Global Variables/Objects


				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?

🦅 Garbage Collection

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

Nodes element.parentNode element.childNodes element.firstChild element.lastChild element.nextSibling element.previousSibling
Elements element.parentElement element.children element.firstElementChild element.lastElementChild element.nextElementSibling element.previousElementSibling

DOM Manipulation
Insert and remove nodes

Remove nodes element.remove()
Insert nodes element.append(newElement) element.before(newElement) element.after(newElement) element.replaceWith(newElement) el.insertAdjacentElement("beforebegin", newEl)

DOM Manipulation
Attributes

DOM Manipulation
String-based

With text (no HTML allowed) element.textContent = "Hi"
With HTML element.innerHTML = "<em>Hi</em>" element.outerHTML = "<em>Hi</em>" element.insertAdjacentHTML("afterend", "<em>Hi</em>")
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.
  • You can find a list of available events on MDN

Functions as function arguments?!


			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.

Events beyond UI interaction

Two ways to add events


				button.addEventListener("click", evt => {
					evt.target.textContent += "😊";
				});

				button.addEventListener("click", evt => {
					evt.target.textContent += "✅";
				});
			

				button.onclick = evt => {
					evt.target.textContent += "😊";
				};

				button.onclick = evt => {
					evt.target.textContent += "✅";
				};
			
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.

Removing events


			let handler = evt => evt.target.textContent = "😊";
			button.addEventListener("click", handler);
			button.removeEventListener("click", handler);
		

What happens when I click the button?


			button.addEventListener("click", evt => {
				evt.target.textContent = "💩"
			});
			button.removeEventListener("click", evt => {
				evt.target.textContent = "💩"
			});
		
  1. Its text is changed to 💩
  2. Nothing
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.

What about now?


			let handler = evt => {
				evt.target.textContent = "💩"
			};

			button.addEventListener("click", handler);
			button.removeEventListener("click", handler);
		
  1. Its text is changed to 💩
  2. Nothing
To fix the code in the previous slide, we use a variable, so that both calls refer to the same function.

Three ways to provide feedback

Javascript CSS Result

							button.addEventListener("mouseover", evt => {
								evt.target.style.background = "hsl(330, 100%, 50%)";

								button.addEventListener("mouseout", evt => {
									evt.target.style.background = "";
								}, {once: true});
							});
						

							button.addEventListener("mouseover", evt => {
								evt.target.classList.add("hovered");

								button.addEventListener("mouseout", evt => {
									evt.target.classList.remove("hovered");
								}, {once: true});
							});
						

							button.hovered {
								background: hsl(330, 100%, 50%);
							}
						

							button:hover {
								background: hsl(330, 100%, 50%);
							}
						

What about this?

First attempt


		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

Raw input events

state transition in the input hardware

Input Event Javascript event
Key pressed or released keydown, keyup
Mouse moved mousemove
Mouse button pressed or released mousedown, mouseup

Translated events

Higher level events from raw events

Input Event Javascript event
Clicking click
Double-clicking dblclick
Character held down keypress
Form element value changed input
Entering or exiting an object’s bounding box mouseenter, mouseleave

Click = mousedown + mouseup?


		let handler = evt => {
			evt.target.textContent += "✅";
		};

		button1.addEventListener("click", handler);
		button2.addEventListener("mousedown", evt => {
			evt.target.addEventListener(
				"mouseup",
				handler,
				{once: true}
			);
		});
	

Input event: Which events?


			<textarea id=tweet></textarea>
			<span id="output"></span>
		

			tweet.addEventListener("input", evt => {
				output.textContent = evt.target.value.length;
			});
		

Translated events are usually more complex than they appear
Use them instead of rolling your own!

Event object


		document.addEventListener("mousemove", evt => {
			document.body.textContent = `${evt.x} ${evt.y}`;
		});
	

Event object

Metadata about the event

Naïve dragging


		let start = {x: 0, y: 0};
		element.addEventListener("mousedown", evt=> {
			start.x = start.x || evt.x;
			start.y = start.y || evt.y;

			let mousemove = evt => {
				evt.target.style.left = (evt.x - start.x) + "px";
				evt.target.style.top = (evt.y - start.y) + "px";
			};
			evt.target.addEventListener("mousemove", mousemove);
			evt.target.addEventListener("mouseup", evt => {
				evt.target.removeEventListener("mousemove", mousemove);
			});
		})
	

Event coalescing

Dragging, revisited

let start = {x: 0, y: 0};
dragme.addEventListener("mousedown", evt=> {
	start.x = start.x || evt.x;
	start.y = start.y || evt.y;
	let target = evt.target;

	let mousemove = evt => {
		target.style.left = (evt.x - start.x) + "px";
		target.style.top = (evt.y - start.y) + "px";
	};
	document.addEventListener("mousemove", mousemove);
	document.addEventListener("mouseup", evt => {
		document.removeEventListener("mousemove", mousemove);
	});
});

What about this?

<button onclick="this.textContent += '😊'">Click me</button>

Event propagation

What do I get when I click on "me"?


				<button id=button>Click <mark>me</mark>!!</button>
			

				button.addEventListener("click", evt => {
					evt.target.innerHTML += "🦄";
				});
			

But… we only had a listener on <button>!

If a tree falls in a forest and no one is around to hear it, does it make a sound?

Events occur whether we listen to them or not

Event bubbling Events start off at a target, and propagate up the DOM tree

What do I get when I click on "me"?


				<button id=button>Click <mark>me</mark>!!</button>
			

				button.addEventListener("click", evt => {
					evt.currentTarget.innerHTML += "🦄";
				});
			

Event bubbling

Mostly helpful

Event delegation


				<ol id="palette" class="items">
					<template>
					<li class="item">
						<input type="color">
						<button class="delete">🗑</button>
					</li>
					</template>
				</ol>
				<button id="addColor" class="add-item">
					Add item
				</button>
			
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

When bubbling is a problem…


				<button id=button1>Click <em>me</em>!</button>
				<button id=button2>No, click <strong>me</strong>!</button>
				<span id=output></span>
			

				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!


			element.addEventListener(eventName, evt => {
				if (evt.bubbles) {
					evt.stopPropagation();
				}
			})
		

Event capturing Events start off at document and propagate down to target


				element.addEventListener(
					eventName,
					callback,
					{capture: true}
				)
			

Default actions

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?

For keyboard shortcuts: - Probability of app action vs browser action - Compromise: Opt-in or opt-out

Don't be annoying


			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


			element.addEventListener(eventName, evt => {
				if (!evt.cancelable) {
					console.log("This event cannot be prevented!");
				}
			})
		

What happens?

<input id="name" />

				name.addEventListener("input", evt => {
					console.log(evt.target.value);
				});

				name.value = "Lea";
			

Synthetic events

Synthetic events


			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
				});
			

Why fire our own events?

[Decoupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)), decoupling, decoupling!

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)