A JavaScript Client Framework – Why this Talk – Learn ES2015, Quickly!



A JavaScript Client Framework – Why this Talk – Learn ES2015, Quickly!

1 4


aurelia-presentation

Slides for Aurelia lunch and learn presentation

On Github danielabar / aurelia-presentation

A JavaScript Client Framework

Presented by:

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

Why this Talk

Seriously, do we really need to talk about another framework?

Times change, needs change, technolgy evolves

Frameworks aim to solve the same problems

  • Components
  • Routing
  • Dependency Injection

We Chose Aurelia!

  • Influent 3 planning was happening, and we were shopping for a framework
  • TellFinder 2 came along at an auspicious time
  • Even got QCR to drink the koolaid!
  • We needed to build complex shared components across applications (auth, case management)

We want to share our experiences

  • Did a bunch of research into different frameworks
  • Have used Aurelia for a few months now, and have a feel for the good/bad

Why Did We Choose Aurelia?

  • Uses ES2015/2016, ensuring that we are building apps that won't be outdated in a year (or less)
  • It's *JUST* JavaScript - no JSX, no extending framework classes
  • Beat feature - we aren't locked in
  • Now, let's start with the ES2015-2016 overview!

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

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

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

Aurelia Overview

What is it?
Why should you care?

Modern

  • Modern javascript
    Write in ES6, ES7, Typescript or *ES5
    * Presenter does not officially endorse use of ES5
  • Modern DOM
    Automatically polyfills older browser support
  • Modern Tooling
    **JSPM, Gulp, Karma, Protractor
    ** Presenter does not officially endorse JSPM

Components!

  • HTMLTemplateElement and ShadowDOM
  • Aurelia Component Lifecycle
  • Modularity

How can it compete?

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

Aurelia vs. Angular 1.x

“Angular Script”

angular.module ...controller

...filter ...factory

...service ...provider

Aurelia: Just classes

View Model(Controller)
export class Users {
	constructor() { ... }
	activate() { ... }
}
									
Service(Factory)
export class MyService {
	constructor() { ... }
	doSomething() { ... }
}
									
No framework specific code required!

“Angular Script” vs. ES 2015

angular.module('myApp', [ 'ngRoute' ])
	.config(function ($routeProvider) {
    $routeProvider
      .when('/users', {
				templateUrl: 'views/users.html',
        controller: 'UsersController',
        controllerAs: 'usersController',
        resolve: {
          users: function(UserService) {
            return UserService.getAll();
          }
        }
			})
			.otherwise({ redirectTo: '/' });
  });
 
						
export class App {

  configureRouter(config, router) {
    config.map([
      { route: ['', 'users'], moduleId: 'users' }
    ]);

    this.router = router;
  }

}
							

Dependency Injection: Angular

angular.module('myApp').controller('UserController',
['users', 'UserService', '$rootScope', 'toastr', '$state',
function (users, UserService, $rootScope, toastr, $state) {
		// ...
}]);
						

Where did the dependencies come from??

users Router resolve (data!) UserService Application factory $rootScope Angular core toastr Notification library $state UI Router (not Angular)

Dependency Injection: Aurelia

import {inject} from 'aurelia-framework';
import {UserService} from 'users/user-service';
import {EventAggregator} from
	'aurelia-event-aggregator';
import * as toastr from 'toastr';
						

Injection service and dependencies imported.

import {inject} from 'aurelia-framework';
import {UserService} from 'users/user-service';
import {EventAggregator} from
	'aurelia-event-aggregator';
import * as toastr from 'toastr';

@inject(UserService, EventAggregator, toastr)
export class Users {

}
						

Injection service and dependencies imported.

Injection via decorator.

import {inject} from 'aurelia-framework';
import {UserService} from 'users/user-service';
import {EventAggregator} from
	'aurelia-event-aggregator';
import * as toastr from 'toastr';

@inject(UserService, EventAggregator, toastr)
export class Users {

	constructor(userService, eventAggregator, toastr) {
		this.userService = userService;
		this.eventAggregator = eventAggregator;
		this.toastr = toastr;
	}
}
						

Injection service and dependencies imported.

Injection via decorator.

Deps injected as constructor args.

import {inject} from 'aurelia-framework';
import {UserService} from 'users/user-service';
import {EventAggregator} from
	'aurelia-event-aggregator';
import * as toastr from 'toastr';

@inject(UserService, EventAggregator, toastr)
export class Users {

	constructor(userService, eventAggregator, toastr) {
		this.userService = userService;
		this.eventAggregator = eventAggregator;
		this.toastr = toastr;
	}

	activate(params) {
		return this.userService.getAll()
			.then( users => this.users = users );
	}
}
						

Injection service and dependencies imported.

Injection via decorator.

Deps injected as constructor args.

Lifecycle activate method invoked with view state. Screen Activation Lifecycle

Data Binding

Append .bind to any html or custom attribute. Uses Angular-specific custom attributes. Defaults to two-way for <input>, one-way for everything else. Two-way all the time, except for custom directives. Want more control? .one-way, .two-way, .one-time. Each binding adds a watcher on the $scope model. Adaptive Binding picks optimal observation strategy & minimizes dirty checking. Built in directives trigger digest cycle on scope model changes (dirty checking).

Data Binding Examples


								
								
									

								
							
									
some content
some content
${err.message}
{{ err.message }}

								
								
								

								
							

HMTL Customization


 

								
						

Usage looks similar, devil is in the implementation details...

Angular Directive

angular.module('myApp').directive('myDatepicker',
	function () {
    return {
      templateUrl: 'views/mydatepicker.html',
			restrict: 'E',
			scope: { data: '=' },

			controller: function($scope, $element) {
				// Directive-specific logic
			},

      link: function postLink(scope, element) {
        // DOM manipulation here
      }
    };
  });
						

Configure template.

restrict  specifies 'E' or 'A' for element or attribute.

Isolate scope, o.w. can access parent!

'=' indicates two-way data binding, '@' for one way, '&' for binding functions.

Controller function runs first, then Link function.

Directive: Template


Directive: Datepicker

link: function(scope, element) {
	// Initialize datepicker
	element.find('.date').datepicker();

	// Try to update data binding?
	element.find('.date').datepicker().on('changeDate',
		function(e) {
			scope.data = e.date;
		}
	);

}
						

Element will not update because event happened outside of Angular’s context.

link: function(scope, element) {
	// Initialize datepicker
	element.datepicker();

	// Update data binding
	element.datepicker().on('changeDate',
		function(e) {
			scope.$apply(function () {
				scope.data = e.date;
			});
	});

}
						

Run update inside scope.$apply to trigger a digest cycle.

Need to understand framework internals to make this work.

Directive: Watch

link: function(scope, element) {
	// ...

	scope.$watch('data',
		function(newVal) {
    	element.datepicker('setDate', newVal);
  	});

}
						

To have datepicker respond to model changes, must add scope.$watch to add to list of watchers evaluated in digest cycle.

Aurelia Custom Element

import {inject, bindable} from 'aurelia-framework';
import 'bootstrap-datepicker/js/bootstrap-datepicker';

@inject(Element)
export class MyDatepickerCustomElement {
  @bindable data;

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

	bind() { // DOM manipulation... }

	unbind() { // Cleanup... }

}
					

Naming convention marks this as custom element.

Bindable decorator for data binding.

View template loaded by convention.

bind and unbind are Component Lifecycle methods.

Custom Element: Template


Custom Element: Datepicker

bind() {
	// Find input element
	this.selector = $(this.element).find('.date');

	// Initialize datepicker
	this.selector.datepicker();

	// Update when user picks a date
	this.selector.datepicker().on('changeDate', (e) => {
		this.data = e.date;
	});

}
					

Changing data in datepicker event just works.

No internal framework knowledge required.

Custom Element: Update

export class DatePicker {
	@bindable data;
	...

	dataChanged(newVal, oldVal) {
		this.selector.datepicker('setDate', this.data);
	}

}
					

xxxChanged method called for changes to bindable properties.

this.data automatically populated with newVal. Full example

Angular Inheritance

Components can access their parent via $scope.

Prevents re-use and leads to hard to track down bugs due to prototypal inheritance.

See the Pen Angular Demo Parent Child Madness by Daniela Baron (@danielabar) on CodePen.

Aurelia Inheritance

By default, components can not access their parent.

Can be configured if needed:

export class ChildViewModel {
  bind(bindingContext) {
    this.$parent = bindingContext;
  }
}
					

Even better, use EventAggregator or PropertyObserver for component communication.

Summary

Conventions Configuration Web standards “The Angular Way” Simple DOM manipulation Directives are difficult Data binding just works Leaky abstractions View and Component lifecycle Kind of, but unclear

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

Aurelia vs. React

React is not a framework

It's just a view layer

You have to provide everything else

That means lots of freedom but also lots of inconsistency

Aurelia gives you freedom, too

It provides lots of modules but lets you pick the ones you want to use

JSX

class WelcomeBox extends React.Component {
  render() {
    let divStyle = { color: this.props.color };
    return (
      <div className="well">
        <div style={divStyle}>Hello, {this.props.name}!</div>
      </div>
    );
  }
}

ReactDOM.render(
  <WelcomeBox name="Hodor" color="firebrick"/>,
  document.getElementById('welcome')
);

Reactions to JSX Vary

Opinions aside,one fact remains

In other words, you're locked in

That's not a concern with Aurelia

State Management in React

  • Flux
  • Redux

Flux: Unidirectional Data Flow

Redux: Three Principles

  • Single source of truth
  • State is read-only
  • Changes are made with pure functions

Aurelia doesn't dictate how your data should flow

It does, however, provide sensible binding defaults and powerful mechanisms like propertyObserver.

Aurelia plays well with others

You can use it with React, Flux or Redux

Why this Talk

Learn ES2015, Quickly

Aurelia Overview

Framework Comparisons

Aurelia vs. Ember

Same goals, different approach.

Conceptually, they support the same ideas/goals:

  • Routing
  • Components
  • Dependency injection

Aurelia has the advantage that it doesn't have any "legacy" versions to support, and no backwards compatibility to worry about. It was designed up-front to use ES6, instead of migrating to ES6 over time.

Ember-isms

Using Ember means using Ember.

Much like Angular or React, when you use Ember, you are forever using Ember. There is no easy way to switch to another framework, since you are consuming/extending very specific Ember items.

Things to remember when using Ember:

  • Start using the CLI, to help with adding functionality (not a bad thing, but a thing nonetheless)
  • Using Handlebars for templates (and templates live in a different folder from their source code)

Oh, and that whole "if you use Ember 1.x you get a different experience from using Ember 2.x" thing. ES6 goodies only come into play with Ember 2.x.

Let's talk routing

Routing (in Ember/ES5)

Todos.Router.map(function () {
	this.resource('todos', { path: '/' }, function () {
		this.route('active');
		this.route('completed');
	});
});

Todos.TodosRoute = Ember.Route.extend({
	model: function () {
		return this.store.find('todo');
	}
});

Todos.TodosIndexRoute = Todos.TodosRoute.extend({
	templateName: 'todo-list',
	controllerName: 'todos-list'
});

Todos.TodosActiveRoute = Todos.TodosIndexRoute.extend({
	⋮
});

Todos.TodosCompletedRoute = Todos.TodosIndexRoute.extend({
	⋮
});
							
						

Routing (in Ember/ES6)

import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
  this.route('about');
  this.route('contact');
  this.route('rental', {path: '/rental/:rental_id'});
  this.route('city', {path: '/city/:city_id'});
});

export default Router;
						

ES6 makes Ember nicer, but still lots of verbocity.

Each route has a separate module under /routes/ where more code lives.

Routing (in Aurelia)

export class App {
	constructor() {
		// whatever you want
	}

	configureRoutes(config, router) {
		this.router = router; // if you want to reference it
													// in other phases of the lifecycle

		config.title = 'TodoMVC';
		config.map([
			{ route: ['', ':filter'], moduleId: 'todos' },
			{ route: '/login', moduleId: 'auth/login' }
		]);
	}
}
						

Routes typically wired in as part of the app initialization.

The route's moduleId references an ES6 class.

Components (in Ember)

THIS.

Components (in Aurelia)

export class Welcome {
  constructor() {
    this.firstName = 'John';
    this.lastName = 'Doe';
    this.previousValue = this.fullName;
  }

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

  submit() {
    this.previousValue = this.fullName;
    alert(`Welcome, ${this.fullName}!`);
  }
}
						

Components (in Aurelia)


    

${fullName}

Submit

Dependency Injection (in Ember)

  • create a file that exports a function and a default object; the function is called with an application parameter that is used to register the factory. The factory itself has to extend Ember.Object if you want Ember to instantiate the factory for you. The default object is used to tell the app, again, how to access the factory. You then choose to register it as a singleton, or not - the latter seems kind of pointless from the DI perspective, but hey.
  • You can then use application.inject to tell Ember to ensure that whenever a specific factory is instantiated that it has to wire in your new factory. In the other factory, you can now call 'this.get('injfectedFactory')' by name to get it. No idea how it gets there, though.
  • Or, you can do an ad hoc injection via Ember.inject.service - again, by name.
  • Or... wait for it... You can use applicationInstance.lookup s
  • reiterate the ease of aurelia - DI to inject a singleton, otherwise directly reference the ES2015 class directly.

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

What's Next?

We can help you figure out if Aurelia is right for you!

Q & A

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