On Github danielabar / aurelia-presentation
Presented by:
Seriously, do we really need to talk about another framework?
Times change, needs change, technolgy evolves
JavaScript that doesn't suck :)
var pi = 3.141592653; // is now const pi = 3.141592653;
// 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);
[1,2,3].map(a => a+1);
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! }); }
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!'); } }
@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!
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));
/* before, in Person, we had this: */ getFullName() { return this.firstName + ' ' + this.lastName; }
/* now we can do this!*/ getFullName() { return `${this.firstName} ${this.lastName}`; }
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
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
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]
function f() { return [1,2]; } [a, b] = f();
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;
const obj = { // ... }
const wm = new WeakMap(); wm.set(obj, 42); // store some metadata about obj.
const p = new Promise((resolve, reject) => { setTimeout(() => { Math.random() < 0.5 ? resolve() : reject(); }, 500); }); p.then(() => { console.log('Resolved!'); }) .catch(() => { console.log('Rejected!'); });
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 :)
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.
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?
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.
For more information, the Babel docs are a good reference
As is the Mozilla Developer Network JavaScript reference
angular.module ...controller
...filter ...factory
...service ...provider
export class Users { constructor() { ... } activate() { ... } }Service(Factory)
export class MyService { constructor() { ... } doSomething() { ... } }No framework specific code required!
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; } }
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)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
Usage looks similar, devil is in the implementation details...
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.
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.
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.
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.
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.
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
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.
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.
It's just a view layer
You have to provide everything else
That means lots of freedom but also lots of inconsistency
It provides lots of modules but lets you pick the ones you want to use
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') );
In other words, you're locked in
It does, however, provide sensible binding defaults and powerful mechanisms like propertyObserver.
You can use it with React, Flux or Redux
Same goals, different approach.
Conceptually, they support the same ideas/goals:
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.
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:
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.
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({ ⋮ });
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.
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.
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}!`); } }
${fullName}
Submit