A JavaScript Client Framework – Learn ES2015, Quickly! – View and View-Model



A JavaScript Client Framework – Learn ES2015, Quickly! – View and View-Model

1 2


aurelia-presentation-js-meetup

Slides for JavaScript meetup presentation on Aurelia and ES2015

On Github danielabar / aurelia-presentation-js-meetup

A JavaScript Client Framework

Presented by:

Learn ES2015, Quickly

Hello Aurelia

Aurelia from the Trenches

Learn ES2015, Quickly!

Also ES2016, sometimes called ES6/7.

JavaScript that doesn't suck :)

var is dead.Long live let & const

Constants!

var pi = 3.141592653;
// is now
const pi = 3.141592653;

Block scoping!

// this works. stop the insanity.
for (var i=0; i<10; i++) {
	console.log(i);
}
console.log(i);
// this does not work :)
for (let i=0; i<10; i++) {
	console.log(i);
}
console.log(i);

Arrow Notation

[1,2,3].map(a => a+1);

Lexical this

function() {
	var self = this;
	self.name = 'Sean';
	setInterval(function() {
		console.log(self.name); // ugly :(
	});
}
function() {
	this.name = 'Sean';
	setInterval(() => {
		console.log(this.name); // => shares the same this
		                        // with the surrounding code!
	});
}

Classes(finally)

class Person {
	constructor(firstName, lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}
	getFullName() {
		return this.firstName + ' ' + this.lastName;
	}
}
class Developer extends Person {
	// static method called with Developer.curse();
	static curse() { return 'thou shalt forever be off by one...'; }

	constructor(firstName, lastName, isRemote) {
		super(firstName, lastName);
		this._isRemote = isRemote;
	}

	// getter, used via developerInstance.isRemote
	get isRemote() { return this._isRemote; }
	// setter, used via developerInstance.isRemote = false
	set isRemote(newIsRemote) {
		throw new Error('Cannot re-assign isRemote!');
	}
}

Decorators

@isTestable(true)
class Person {
	constructor(firstName, lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}
	@readonly
	getFullName() {
		return this.firstName + ' ' + this.lastName;
	}
}

Decorators are annotations which allow you to define cross-cutting modifications to classes and methods.

Decorators are executed at runtime.

Built-in classes like Array, Date and DOM Elements can be subclassed!

Modules

Making module syntax a native part of the language!

// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;
// app.js
import * as math from "lib/math";
alert("2π = " + math.sum(math.pi, math.pi));
// otherApp.js
import {sum, pi} from "lib/math";
alert("2π = " + sum(pi, pi));

Template strings

/* before, in Person, we had this: */
getFullName() {
	return this.firstName + ' ' + this.lastName;
}
/* now we can do this!*/
getFullName() {
	return `${this.firstName} ${this.lastName}`;
}

for...of (Iterators)

let a = ['a','b','c'];
for (let i in a) {
  console.log(i);
}
// prints 0 1 2 (which is pretty useless)
for (let i of a) {
	console.log(prop);
}
// prints a b c :)

You can use the Iterator protocol in your own functions and classes to make anything iterable via for...of

Default, Rest and Spread

function f(x, y=12) {
  // y is 12 if not passed (or passed as undefined)
  return x + y;
}
f(3) == 15
function f(x, ...y) {
  // y is an Array
  return x * y.length;
}
f(3, "hello", true) == 6
function f(x, y, z) {
  return x + y + z;
}
// Pass each elem of array as argument
f(...[1,2,3]) == 6

Destructuring

let a, b, rest;

[a, b] = [1, 2]
{a, b} = {a:1, b:2}
// a === 1, b === 2

[a, b, ...rest] = [1, 2, 3, 4, 5]
// a === 1, b === 2, rest === [3,4,5]

Destructuring(multiple return values)

function f() {
	return [1,2];
}
[a, b] = f();

Sets and Maps

const s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;
const m = new Map();
m.set("hello", 42);
m.set("goodbye", 34);
m.get("goodbye") == 34;

WeakMaps and WeakSets

const obj = {
	// ...
}
const wm = new WeakMap();
wm.set(obj, 42); // store some metadata about obj.
  • Keys in a WeakMap must be objects
  • WeakMaps do not hold a strong reference to their keys.
  • Great way to store additional metadata on an object without polluting it.
  • WeakSets are similar

Native Promises

const p = new Promise((resolve, reject) => {
	setTimeout(() => {
		Math.random() < 0.5 ? resolve() : reject();
	}, 500);
});

p.then(() => {
	console.log('Resolved!');
})
.catch(() => {
	console.log('Rejected!');
});

Generators

function *getTime() {
	while(true) {
		yield Date.now();
	}
}
const timer = getTime();
console.log(timer.next()); // { value: 1454906307698, done: false }
console.log(timer.next()); // { value: 1454906307710, done: false }
console.log(timer.next()); // { value: 1454906307711, done: false }

You can also use the for...of loop with Generators :)

Generatorstwo-way communication

const summer = (function *sum() {
	let sum = 0;
	while(true) {
		sum += yield sum;
	}
})();
summer.next(); // start summer by making it yield once
// now we can pump values into it, and receive the current sum
console.log(summer.next(1)); // { value: 1, done: false }
console.log(summer.next(2)); // { value: 3, done: false }
console.log(summer.next(3)); // { value: 6, done: false }

Calling next() on a generator makes it pause execution. When the generator is restarted by another call to next(), the argument passed to next() replaces the yield expression.

Now for the crazy part...

Generatorsas a way to avoid callbacks

Let's say we have some asynchronous function returning a Promise:

function longRunning(done) {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve(Math.random());
		}, 500);
	});
}

Normally, we'd use it like this:

longRunning.then((result) => {
	console.log(result);
})

But now we can do something like this...

const script = function *() {
	let s = yield longRunning();
	console.log(s);
}();

With the assistance of this horrifying statement:

script.next().value.then((r) => {
	script.next(r);
});

Treating async code like it's synchronous is awesome!

const script = function *() {
	let s = yield longRunning(); // so cool!
	console.log(s);
}();

So how do we avoid the horror?

ES2016 async...await

const script = function *() {
	let s = yield longRunning();
	console.log(s);
}();
script.next().value.then((r) => {
	script.next(r);
});

becomes...

(async function script() {
	let s = await longRunning(); // even cooler!
	console.log(s);
})();
// no ugliness!

Or, more realistically...

(async function script() {
	try {
		let s = await longRunning(); // sequential async
		let t = await anotherLongRunning();
		console.log(s + t);
	} catch (err) {
		console.error(err);
	}
})();

Notice that good old-fashioned try-catch blocks work again!

Or, for parallel async

(async function script() {
	try {
		let [s,t] = await Promise.all(
			longRunning(),
			anotherLongRunning()
		);
		console.log(s + t);
	} catch (err) {
		console.error(err);
	}
})();

Almost all of this is available in Node.js natively, right now!

If you're not using it...start. My eyes will thank you.

If you want to use this in the browser, you'll need to use a transpiler until the ES2015 and ES2016 specifications are implemented natively. Popular transpilers include:

ES2015/ES2016Further reading

For more information, the Babel docs are a good reference

As is the Mozilla Developer Network JavaScript reference

Stuff I didn't cover:
  • Symbols (new basic type, allowing private class members)
  • Proxies
  • New Math, Number, String and Object APIs
  • Binary and Octal literals
  • Reflection API
  • Tail recursion

Learn ES2015, Quickly

Hello Aurelia

Aurelia from the Trenches

View and View-Model

// View model: app.js
export class App {

  constructor() {
    this.heading = 'Hello Aurelia!';
    this.firstName = 'John';
    this.lastName = 'Doe';
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  sayWelcome() {
    alert(`Welcome, ${this.fullName}!`);
  }
}
<!-- View: app.html -->
<template>

  <h2>${heading}</h2>

  <form>
    <input type="text" value.bind="firstName">
    <input type="text" value.bind="lastName">

    <p>${fullName}</p>

    <button click.trigger="sayWelcome()">
			Submit
		</button>
  </form>

</template>

Minimal framework intrusion in code.

Data Binding

  • Append .bind to any html or custom attribute.
  • Defaults to one-way, except <input> which is two-way.
  • Want more control? .one-way, .two-way, .one-time.
<input type="text" value.bind="user.name">

<div class.bind="row.isActive ? 'active' : ''">
	some content
</div>

<div show.bind="hasError" class="error">
	${err.message}
</div>

<my-datepicker data.two-way="user.dob">
</my-datepicker>

Adaptive Binding picks optimal observation strategy & minimizes dirty checking.

Routing

  • Define container for routing view with <router-view> element.
  • Implement configureRouter() method in view model.
  • Implement view and view model pairs for each route (welcome.js, welcome.html, users.js, etc.).
<!-- View: anywhere.html -->
<template>
	<router-view></router-view>
</template>
// View Model: anywhere.js
export class Anywhere {
	configureRouter(config, router) {
		config.map([
			{route: ['', 'welcome'], moduleId: 'welcome'},
			{route: 'users', moduleId: 'users'},
			{route: 'users/:id', moduleId: 'userdetail'}
		]);
	}
}

Any view can be a routing container and they can be nested.

Screen Activation Lifecycle

  • activate() method called with url and query parameters.
  • Waits for promise to resolve before rendering view.
  • When navigating away from the view, deactivate() is called.
  • canActivate() and canDeactivate() to control navigation.
export class UserDetail {
	constructor() { ... }

	activate(params) {
		// assume this.userService is available
		return this.userService.getUser(params.id)
			.then(user => this.user = user)
			.catch(err => ... error handling)
	}

	deactivate() {
		// cleanup
	}

	canActivate() { ... }

	canDeactivate() { ... }
}

All lifecycle methods can optionally be implemented.

Dependency Injection

  • Import dependencies.
  • Import inject decorator.
  • Delcare dependencies with inject decorator.
  • DI container constructs instance of the dependency and passes reference to constructor.
  • Consuming class can now use the depdendency.
import {UserService} from 'path/to/user-service';
import {inject} from 'aurelia-framework';

@inject(UserService)
export class UserDetail {
  constructor(userService) {
    this.userService = userService;
  }

  activate(params) {
    return this.userService.getUser(params.id)
    	.then(...)
  }
}

Constructor based DI makes it easy to mock out dependencies in unit tests.

Custom Elements

// View Model: contact-card.js
import {bindable, inject} from 'aurelia-framework';

@inject(Element)
export class ContactCard {
	@bindable name;

	constructor(element) {
    this.element = element;
  }

	attached() {
		// DOM manipulation
	}

	detached() {
		// cleanup
	}
}
<!-- View: contact-card.html -->
<template>
	<div class="contact-card">
		<input type="text" value.bind="name">
		...
	</div>
</template>
<!-- Usage: some-view.html -->
<template>
	<require from="contact-card"></require>

	<contact-card
		name.two-way="user.name">
	<contact-card>
	...

</template>

Component lifecycle methods:

created(view), bind(bindingContext), attached(), detached(), unbind()

A few more things...

Learn ES2015, Quickly

Hello Aurelia

Aurelia from the Trenches

Our Experiences

Building Large Scale Apps with Aurelia

Gotchas

It's new...

  • Still in beta → moving target
  • Documentation is a work in progress
  • Not a lot of Q & A on Stack Overflow
  • JSPM is new and quirky (recommended package manager and module loader)
    • JSPM link and associated transitive dependencies are flakey
    • Implicitly pulls in all transitive dependencies
  • Bundling and optimization still needs work

Q & A

A JavaScript Client Framework Presented by: Sean McIntyre Daniela Baron Kent English Phil Laliberte