JS? UX?

???

By Lea Verou (@LeaVerou)

What does JS have to do with UX?

User experience (UX) refers to a person's total experience using a particular product, system or service.”

Usability is the ease of use and learnability of a human-made object.”

“The User Interface (UI) is everything designed into an information device with which a human being may interact”

Handbook of Research on Web Information Systems Quality

But what about documentation?

Make the simple easy & the complex possible

“Simple things should be simple,
complex things should be possible.”
― Alan Kay

> let circle = document.querySelector("circle")
> circle.cx
< SVGAnimatedLength {baseVal: SVGLength, animVal: SVGLength}
> circle.cx.baseVal
< SVGLength {unitType: 1, value: 50, valueInSpecifiedUnits: 50, valueAsString: "50"}
> circle.cx.baseVal.value
< 50
<circle cx="50" cy="50" r="40" />

			// Complex/rare case
			let evt = new KeyboardEvent("keyup", {
				key: "Enter", code: "Enter",
				shiftKey: true
			});
			textarea.dispatchEvent(evt);
		

			// Simple/common case
			button.click();
		

Shortcuts

Optional arguments


				// Common case: standard, minified JSON
				JSON.stringify(data);
			

				// Complex case: custom serialization, formatting
				JSON.stringify(data, (key, value) => {
					if (value instanceof HTMLElement) {
						return value.outerHTML;
					}

					return value;
				}, "\t");
			

Function arguments


				// Common case: replace with string
				text.replaceAll(/\s+/g, " ");
			

				// Complex case: replace with function
				text.replaceAll(/-[a-z]/g, match => match.toUpperCase());
			

Polymorphism

Expressive Power Ease of use

Unnecessary error conditions

“The best error message is the one that never shows up”

Thomas Fuchs

> let div = document.createElement("div")
> document.body.removeChild(div)
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
element.remove();
element.parentNode.insertBefore(newChild, element);
> let div = document.createElement("div")
> document.documentElement.insertBefore(document.body, div)
Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
element.before(newChild);

Strive for a single source of truth.
Inputs should be independent.

Be liberal in what you accept
(Robustness Principle)

👎🏽
> new URL("https://lea.verou.me")
< URL {href: "https://lea.verou.me", …}
> new URL(new URL("https://lea.verou.me"))
< URL {href: "https://lea.verou.me", …}
> new URL(location)
< URL {href: "https://projects.verou.me/jsux/index.html", …}
> new Set(42)
Uncaught TypeError: number 42 is not iterable (cannot read property Symbol(Symbol.iterator))
> new Set([42])
< Set {42}

Sensible defaults

> new URL("foo.txt")
Uncaught TypeError: Failed to construct 'URL': Invalid URL
> new URL("foo.txt", location)
< URL {href: "https://projects.verou.me/jsux/foo.txt", …}
> CSS.registerProperty({name: "--color", syntax: "<color>"});
Uncaught TypeError: Failed to execute 'registerProperty' on 'CSS': required member inherits is undefined.
> CSS.registerProperty({name: "--color", syntax: "<color>", inherits: true});
Uncaught DOMException: Failed to execute 'registerProperty' on 'CSS': An initial value must be provided if the syntax is not '*'

Default Values


			function callback(
				timestamp = Date.now(),
				future = timestamp + 3000
			) {
				console.log(timestamp, future);
			}
		

Default Values
+ destructuring


			function callback({
				timestamp = Date.now(),
				future = timestamp + 3000
			} = {}) {
				console.log(timestamp, future);
			}
		

Order parameters by importance

👎 history.pushState(null, "", "https://lea.verou.me"); // (actual)
👍 history.pushState("https://lea.verou.me"); // (fictional)

Argument traps

element.cloneNode(true);
element.clone({deep: true})

			element.addEventListener("focus", callback, true);
		

			element.addEventListener("focus", callback, {
				capture: true
			});
		

			element.addEventListener("focus", callback, {
				capture: true,
				once: true
			});
		

			let event = document.createEvent('KeyboardEvent');
			event.initKeyEvent("keypress", true, true, null, null,
			                   true, false, true, false, 9, 0);
		

			let event = new KeyboardEvent("keypress", {
				ctrlKey: true,
				shiftKey: true,
				keyCode: 9
			});
		
oldChild.parentNode.replaceChild(newChild, oldChild)
oldChild.parentNode.replaceChild(oldChild, newChild)
oldChild.replaceWith(newChild)
// Change these numbers to see what each one is for...
ctx.ellipse(100, 100, 50, 75, .785398);

			ctx.ellipse({
				center: [100, 100],
				radius: [50, 75],
				rotation: .785398
			});
		

			ctx.ellipse({
				cx: 100,
				cy: 100,
				rx: 50,
				ry: 75,
				rotation: .785398
			});
		

“Code is written once but read many times”

Unknown

Object literal argument


			(50000).toLocaleString("en", {
				style: "currency",
				currency: "EUR",
				currencyDisplay: "name"
			}); // "50,000.00 euros"
		

Support bulk operations


			for (let img of document.querySelectorAll("img:nth-of-type(n+4)")) {
				img.setAttribute("loading", "lazy");
				img.setAttribute("importance", "low");
				img.setAttribute("decoding", "async");
			}
		

			let images = document.querySelectorAll("img:nth-of-type(n+4)");

			images.setAttribute({
				loading: "lazy",
				importance: "low",
				decoding: "async"
			});
		

Remember jQuery?


			$("img:nth-of-type(n+4)").attr({
				loading: "lazy",
				importance: "low",
				decoding: "async"
			});
		
parent.append(element1, "Some text", element2)
parent.append(...[element1, "Some text", element2])
> primes = new Set([2, 3, 5])
< Set {2, 3, 5}
> primes.add(7, 11, 13)
< Set {2, 3, 5, 7}

			Object.defineProperty(obj, "property1", descriptor1);

			Object.defineProperties(obj, {
				property1: descriptor1,
				property2: descriptor2,
				...
			});
		

Speak the user’s language

element.querySelectorAll(".foo")
element.find(".foo")

Describe what it does, not how it works

element.addEventListnener("click", callback)
element.bind("click", callback)
element.on("click", callback)
element.addEvent("click", callback)
element.getBoundingClientRect().bottom
element.rect().bottom
element.rect.bottom

Use few simple
words, whole

Parting
words

Cherish the n00b within

Code with empathy