JS UX

By Lea Verou (@LeaVerou)

Picture of me
Привет, я Лиа

#funfact

I grew up in Lesbos, Greece

…which technically makes me geographically Lesbian

I make stuff

CSS WG Invited Expert

MIT HCI researcher @ CSAIL

CSS Secrets by O’Reilly ★★★★★ on Amazon

JS? UX? JSUX…?

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

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

“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

A book is a UI

Slides are a UI

A faucet is a UI

Your code is a UI

Documentation is a last resort.

Make the simple easy
& the complex possible


			!!(node.compareDocumentPosition(otherNode) &
				Node.DOCUMENT_POSITION_CONTAINS);
		
node.contains(otherNode);
> var 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 = 5
> circle.cx.baseVal.value
< 50
<circle cx="50" cy="50" r="40" />

Sensible defaults

> new URL("foo.txt")
Uncaught TypeError: Failed to construct 'URL': Invalid URL
> new URL("foo.txt", location)
< URL {href: "https://leaverou.github.io/jsux/foo.txt", …}

ES6 Default Parameters


			function callback(
				timestamp = Date.now(),
				future = timestamp + 3000
			) {
				console.log(timestamp, future);
			}
		
> callback()
< 1472325217105 1472325220105
> callback(42)
< 42 3042
> callback(42, 1000000000000)
< 42 1000000000000

Boilerplate


			var xhr = new XMLHttpRequest();
			xhr.open("GET", "foo.json");
			xhr.responseType = "json";
			xhr.onload = function() {
				doStuff(xhr.response);
			}
			xhr.send(null);
		

			fetch("foo.json")
			  .then(response => response.json())
			  .then(doStuff);
		

			var open = indexedDB.open("MyDatabase", 1);

			open.onupgradeneeded = function() {
			    var db = open.result;
			    var store = db.createObjectStore("MyObjectStore", {keyPath: "id"});
			    var index = store.createIndex("NameIndex", ["name.last", "name.first"]);
			};

			open.onsuccess = function() {
			    var db = open.result;
			    var tx = db.transaction("MyObjectStore", "readwrite");
			    var store = tx.objectStore("MyObjectStore");
			    var index = store.index("NameIndex");

			    store.put({id: 12345, name: {first: "John", last: "Doe"}, age: 42});
			    store.put({id: 67890, name: {first: "Bob", last: "Smith"}, age: 35});

			    var getJohn = store.get(12345);
			    var getBob = index.get(["Smith", "Bob"]);

			    getJohn.onsuccess = function() {
			        console.log(getJohn.result.name.first);  // => "John"
			    };

			    getBob.onsuccess = function() {
			        console.log(getBob.result.name.first);   // => "Bob"
			    };

			    tx.oncomplete = function() {
			        db.close();
			    };
			}
		

Be liberal in what you accept (Robustness Principle)

> new URL("http://lea.verou.me")
< URL {href: "http://lea.verou.me", …}
> new URL(new URL("http://lea.verou.me"))
< URL {href: "http://lea.verou.me", …}
> new URL(location)
< URL {href: "https://leaverou.github.io/jsux/index.html", …}
> new Set(42)
Uncaught TypeError: (var)[Symbol.iterator] is not a function
> new Set([42])
< Set {42}

Support mass params


			var audio = document.createElement("audio");
			audio.setAttribute("src", "rain.mp3");
			audio.setAttribute("autoplay", "");
			audio.setAttribute("start", "00:00:00.00");
			audio.setAttribute("loopstart", "00:00:00");
			audio.setAttribute("end", "00:01:00");
			audio.setAttribute("playcount", "100");
			audio.setAttribute("controls", "true");
		

			var audio = document.createElement("audio");
			audio.setAttribute({
				src: "rain.mp3",
				autoplay: "",
				start: "00:00:00.00",
				loopstart: "00:00:00",
				end: "00:01:00",
				playcount: "100",
				controls: "true"
			});
		

			for (let el of document.querySelectorAll(".foo")) {
				el.setAttribute("data-foo", "bar");
			}
		

			var els = document.querySelectorAll(".foo");

			els.setAttribute("data-foo", "bar");

			// or…
			DOM.setAttribute(els, "data-foo", "bar");
		

Do not only support mass operations


			// This is stupid:
			element.setAttribute({name: value});

			// This is also stupid
			[element].setAttribute(name, value)
		

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

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

Accessor functions


			let date = new Date();

			date.getMonth(); // 5
			date.setMonth(1);
		

JS is not Java


			private String name;
			private String phone;
			private String gender;
			private int age;
			private Date birthday;
			private boolean alive;

			public void setName(String name) {
				this.name = name;
			}

			public String getName() {
				return name;
			}

			public void setPhone(String phone) {
				this.phone = phone;
			}

			public String getPhone() {
				return phone;
			}

			public void setGender(String gender) {
				this.gender = gender;
			}

			public String getGender() {
				return gender;
			}

			public void setAge(int age) {
				this.age = age;
			}

			public int getAge() {
				return age;
			}

			public void setBirthday(Date birthday) {
				this.birthday = birthday;
			}

			public Date getBirthday() {
				return birthday;
			}

			public void setAlive(boolean alive) {
				this.alive = alive;
			}

			public boolean getAlive() {
				return alive;
			}
		

ES5 Accessors


			Object.defineProperty(Date.prototype, "month", {
				get: function() { return this.getMonth(); },
				set: function(m) { this.setMonth(m); }
			});
		

			let date = new Date();

			date.month; // 5
			date.month = 1;
		

Won’t someone, please, think of parameters?!

Have both!


			let date = new Date();

			date.month = 1;
			date.setMonth(2, 31);
		

			localStorage.setItem("yolo", "yes");
			localStorage.getItem("yolo"); // "yes"
		

			localStorage.yolo = "yes";
			localStorage.yolo; // "yes"
		

ES6 Proxies

MyPublicObject = new Proxy(MyHiddenObject, {
	get: function(property) {
		if (property in myObject) {
			return myObject[property];
		}
		return myObject.getItem(property);
	},
	set: function(property, val) {…
});

ES6 Proxies


			MyPublicObject = new Proxy(MyHiddenObject, {
				…
				set: function(property, val) {
					if (property in myObject) {
						return myObject[property] = val;
					}
					myObject.setItem(property, val);
				},
				has: function(property, val) {…
			});
		

Parameter traps

element.cloneNode(true);

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

			event.initKeyEvent("keypress", true, true, null, null,
			                   false, false, false, false, 9, 0);
		
oldChild.parentNode.replaceChild(newChild, oldChild)
oldChild.parentNode.replaceChild(oldChild, 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"
		

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

ES6 Default Parameters
+ destructuring!


			function callback({
				option1 = 5,
				option2 = false
			} = {}) {
				console.log(option1, option2);
			}
		
> callback()
< 5 false
> callback({})
< 5 false
> callback({option1: 42})
< 42 false

Unnecessary error conditions

> var div = document.createElement("div"), p = document.createElement("p")
> document.body.replaceChild(div, p)
Uncaught DOMException: Failed to execute 'replaceChild' on 'Node': The
  node to be replaced is not a child of this node.

			oldChild.replaceWith(newChild);
		

Order parameters by importance


			history.pushState(null, "", "http://lea.verou.me");
		

			history.pushState("http://lea.verou.me");
		

Double Negatives

input.disabled = false;
input.enabled = true;

Thou shalt allow chaining


			document.createElement("audio")
				.setAttribute("src", "rain.mp3");
				.setAttribute("autoplay", "");
				.setAttribute("start", "00:00:00.00");
				.setAttribute("loopstart", "00:00:00");
				.setAttribute("end", "00:01:00");
				.setAttribute("playcount", "100");
				.setAttribute("controls", "true");
		

			ctx.clearRect(0, 0, 256, 192);
			ctx.save();
			ctx.translate(96, 96);
			ctx.rotate(45*Math.PI/180);
			ctx.scale(1.2);
			ctx.drawImage(img1, 0,0,192,192, -288,-288,576,576);
			ctx.drawImage(img2, 0,0,192,192, -96 ,-96 ,192,192);
			ctx.drawImage(img3, 0,0,192,192, -32 ,-32 ,64 ,64);
			ctx.restore();
		

			ctx.clearRect(0, 0, 256, 192)
			   .save()
			   .translate(96, 96)
			   .rotate(45*Math.PI/180)
			   .scale(1.2)
			   .drawImage(img1, 0,0,192,192, -288,-288,576,576)
			   .drawImage(img2, 0,0,192,192, -96 ,-96 ,192,192)
			   .drawImage(img3, 0,0,192,192, -32 ,-32 ,64 ,64)
			   .restore();
		

			yolo: function() {
				// Function code here

				return this;
			}
		

Returning undefined is wasteful


			function chainable(fn) {
				return function() {
					var ret = fn.apply(this, arguments);
					return ret === undefined? this : ret;
				}
			}
		

Redundancy

element.parentNode.removeChild(element);
element.remove();
element.parentNode.insertBefore(newChild, element);
element.before(newChild);

Use few simple
words, whole

element.parentNode
element.parent
element.childNodes
element.children
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

Describe what it does,
not how it works


			document.querySelector(".foo");
			document.querySelectorAll(".foo");
		

			document.query(".foo");
			document.queryAll(".foo");
		

			document.find(".foo");
			document.findAll(".foo");
		

			$(".foo");
		

Thou shalt respond
to changes

packery.metafizzy.co


			packery.layout();
		

Mutation observers

var observer = new MutationObserver(mutations => {
	packery.layout();
});

observer.observe(document.getElementById("grid"), {
	childList: true
});

Also:

Won’t someone, please, think of performance?!

Use with restraint

Forgetting toString()
and toJSON()

> document.documentElement.attributes[0] + ""
< "[object Attr]"

			Attr.prototype.toString = function(){
				return `${this.name}=${this.value}`;
			}
> document.documentElement.attributes[0] + ""
< "lang=en"
> JSON.stringify({date: new Date()})
< '{"date":"2016-06-27T22:39:17.358Z"}'
> JSON.stringify({regex: /a/gi})
< '{"regex":{}}'

			RegExp.prototype.toJSON = function(){
				return `/${this.source}/${this.flags}`;
			}
> JSON.stringify({regex: /a/gi})
< '{"regex":"/a/gi"}'

Thou shalt also
have an HTML API

Good HTML APIs
are easier for developers too

HTML API design basics

Ceiling cat is watching

Greetings, hoomans.

Consider inheritance


			<body class="language-javascript">
				Use <code>foo()</code> and <code>bar()</code> to…

				<pre class="language-css">
					<code>* { box-sizing: border-box }</code>
				</pre>
			</body>
		

Easy inheritance: CSS variables


			<body style="--language: javascript">
				Use <code>foo()</code> and <code>bar()</code> to…

				<pre style="--language: css">
					<code>* { box-sizing: border-box }</code>
				</pre>
			</body>
		

			getComputedStyle(element).getPropertyValue("--language");
		

Global settings on <script>

<script src="stretchy.min.js" data-filter=".foo"></script>
document.currentScript.getAttribute("data-filter")

Treating HTML
like a JS shortcut

Pointless HTML ≠ HTML API

smashingmagazine.com/2017/02/designing-html-apis

Soon: CSS API?

Parting words

Cherish the n00b within

Code with empathy