Lecture 16 Events

Events

Events

What are events?

Code that is executed non-linearly when something happens

If this Clickme button gets clicked, change its label to Clicked
At 3PM, sound an alarm
When a message arrives, ping and display it to the user

The Vuecation Continues

<button @click="addItem()">Add item</button>

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?!

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.

			let handler = function (evt) {
				evt.target.textContent = "Clicked";
			};
			clickme.addEventListener("click", handler);
		

Let's think about this

Arrow functions

Events beyond UI interaction

Two ways to add events


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

				button.addEventListener("click", evt => {
					evt.target.textContent += "βœ…";
				});
			
Good way

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

				button.onclick = evt => {
					evt.target.textContent += "βœ…";
				};
			
Old way

Old way only allows for one event handler; setting another one overwrites previous.

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?


			let handler = evt => {
				evt.target.textContent = "πŸ’©"
			};

			button.addEventListener("click", handler);
			button.removeEventListener("click", handler);
		
  1. Its text is changed to πŸ’©
  2. Nothing

These two functions do the same thing but are not the same function

Important to use a variable, so that both calls refer to the same function.

What about now?


			button.addEventListener("click", evt => {
				evt.target.textContent = "πŸ’©"
			});
			button.removeEventListener("click", evt => {
				evt.target.textContent = "πŸ’©"
			});
		
  1. Its text is changed to πŸ’©
  2. Nothing

Naming the function lets you refer to it in both places

To remove a function, you need to store a reference to it. Just specifying a function with identical implementation doesn't work, because these functions are different objects.

Three ways to provide feedback

The pure CSS solution highlights the benefits of reactive programming. The style modification is in place when I am hovering and not in place when I am not hovering. I don't have to worry about implementing code for undoing the style modification; it just happens implicitly as a result of the interactivity. There's no way for bugs in my code to leave the modification "stuck" if I fail to run the undo.

generally, presentation should be a function of the current state of the information/system, so something you can describe just with css. if you are editing js to change presentation, that is a code smell that you are doing something wrong.

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

Presentation &rarr CSS

Another Example

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

Setting CSS inside JS is a sign you are using JS for presentation

CSS for presentation, JS for computation

This is better. It adds clarity to what we are doing, since you can now look at the CSS and see the intent separately from understanding how the js sets the properties. Also, we surface the values we want as css properties that can be used by any styling that needs them.

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

What about this?

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

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?

try this: mousedown on button two, then release after dragging the mouse out. then mousedown outside button 2, then release inside button 2.

		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?

what happens if I paste text into the input?

			<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

  • instead of having target listen for mousemove, have whole document listen for mousemove
  • move target based on mousemove
  • so need to remember target since it isn't what is hearing the mousemove
  • 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);
    	});
    });

    Event propagation

    Elements are nested, so on which does the event “happen”?

    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

    Different targets

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

    Stopping Propagation

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

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

    Bubbling or Capturing?

    Default actions

    What happens when you press ⌘+S
    (or Ctrl + S) in a web page?

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

    Synthetic events

    What happens?

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

    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)