107
BUILDING WEB APPS WITH AURELIA By Morris Singer Presented at #MobileTea Boston 28 May 2015 @twitter

Building Web Apps with Aurelia -debranded

Embed Size (px)

Citation preview

BUILDING WEB APPS WITH AURELIABy Morris Singer

Presented at #MobileTea Boston28 May 2015 @twitter

WHO AM I

SR. SOFTWARE ENGINEER / FRONT END LEAD Verilume, Inc.

AGENDA

• Aurelia in context

• Aurelia’s technology stack

• Aurelia essentials

• Q & A

WHERE WE CAME FROM

WHERE WE CAME FROM

WHERE WE CAME FROM

* Some people will disagree with this theory.

*

20TH CENTURY

Put the server in charge of everything.

<form action="post" url="path/to/endpoint.cgi"> <input type="submit" value="Click Me"> </form>

JQUERY

Enhanced static HTML returned from the server.

<form> <button id="submit">Click Me</button> </form>

$("button#submit").on("click", function (event) { $.post(/* Define an AJAX Request */); });

EXT.JS / SENCHA TOUCH

Made HTML a byproduct of JavaScript.

Ext.create('Ext.Button', { text: 'Click Me', renderTo: Ext.getBody(), listeners: { click: function () { /* Define a REST Request */ } } });

BACKBONE.JS

Offered rich data modeling, but poor presentation layer.

<form> <button id="submit">Click Me</button> </form>

var model = Backbone.Model.extend({ /* Define data model */ });

var MyView = Backbone.View.extend({ el: '#my-form', events: { 'click #submit' : 'submit' }, submit: function () { model.set( /* Define data to sync */ ) } });

ANGULAR.JS

Introduced dependency injection to the client.

<form ng-controller="myFormController as controller"> <button ng-click="submit">Click Me</button> </form>

angular.module('myModule', []) .service('mySubmitService', function ($http) { this.sendData = function (data) { $http.post(/* Define REST Request */); } }) .controller('myFormController', function (mySubmitService) { this.submit = function (data) { mySubmitService.submit(data); } });

THIS YEAR’S WISHLIST

Modularity

Performance

Flexibility

Syntax

v.

LINE COMPARISONS: AN EXAMPLE WITH APP.JSAdapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

LINE COMPARISONS: AN EXAMPLE WITH APP.JS

ANGULAR 2 AURELIA

Adapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

import {Component, Template} from 'angular2/angular2';

@Component({selector: 'my-app'}) @Template({url: 'app.html'})

class MyApp { constructor() { this.firstName = 'John'; this.lastName = 'Doe'; this.updateFullname(); } updateFullname() { this.fullName = this.firstName + ' ' + this.lastName; } firstNameChanged($event, first) { this.firstName = first.value; this.updateFullname(); } lastNameChanged($event, last) { this.lastName = last.value; this.updateFullname(); } }

export class MyApp{ constructor(){ this.firstName = 'John'; this.lastName = 'Doe'; } get fullName(){ return this.firstName + ' ' + this.lastName; } }

LINE COMPARISONS: AN EXAMPLE WITH APP.JS

ANGULAR 2 AURELIA

No Dependency

Adapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

import {Component, Template} from 'angular2/angular2';

@Component({selector: 'my-app'}) @Template({url: 'app.html'})

class MyApp { constructor() { this.firstName = 'John'; this.lastName = 'Doe'; this.updateFullname(); } updateFullname() { this.fullName = this.firstName + ' ' + this.lastName; } firstNameChanged($event, first) { this.firstName = first.value; this.updateFullname(); } lastNameChanged($event, last) { this.lastName = last.value; this.updateFullname(); } }

export class MyApp{ constructor(){ this.firstName = 'John'; this.lastName = 'Doe'; } get fullName(){ return this.firstName + ' ' + this.lastName; } }

LINE COMPARISONS: AN EXAMPLE WITH APP.JS

ANGULAR 2 AURELIA

Convention

No Dependency

Adapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

import {Component, Template} from 'angular2/angular2';

@Component({selector: 'my-app'}) @Template({url: 'app.html'})

class MyApp { constructor() { this.firstName = 'John'; this.lastName = 'Doe'; this.updateFullname(); } updateFullname() { this.fullName = this.firstName + ' ' + this.lastName; } firstNameChanged($event, first) { this.firstName = first.value; this.updateFullname(); } lastNameChanged($event, last) { this.lastName = last.value; this.updateFullname(); } }

export class MyApp{ constructor(){ this.firstName = 'John'; this.lastName = 'Doe'; } get fullName(){ return this.firstName + ' ' + this.lastName; } }

LINE COMPARISONS: AN EXAMPLE WITH APP.JS

ANGULAR 2 AURELIA

Convention

No Dependency

Not needed; using getter

Adapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

import {Component, Template} from 'angular2/angular2';

@Component({selector: 'my-app'}) @Template({url: 'app.html'})

class MyApp { constructor() { this.firstName = 'John'; this.lastName = 'Doe'; this.updateFullname(); } updateFullname() { this.fullName = this.firstName + ' ' + this.lastName; } firstNameChanged($event, first) { this.firstName = first.value; this.updateFullname(); } lastNameChanged($event, last) { this.lastName = last.value; this.updateFullname(); } }

export class MyApp{ constructor(){ this.firstName = 'John'; this.lastName = 'Doe'; } get fullName(){ return this.firstName + ' ' + this.lastName; } }

LINE COMPARISONS: AN EXAMPLE WITH APP.JS

ANGULAR 2 AURELIA

Convention

No Dependency

Not needed; using getter

Not needed; using getter

Adapted from http://blog.durandal.io/2015/03/16/aurelia-and-angular-2-code-side-by-side/

import {Component, Template} from 'angular2/angular2';

@Component({selector: 'my-app'}) @Template({url: 'app.html'})

class MyApp { constructor() { this.firstName = 'John'; this.lastName = 'Doe'; this.updateFullname(); } updateFullname() { this.fullName = this.firstName + ' ' + this.lastName; } firstNameChanged($event, first) { this.firstName = first.value; this.updateFullname(); } lastNameChanged($event, last) { this.lastName = last.value; this.updateFullname(); } }

export class MyApp{ constructor(){ this.firstName = 'John'; this.lastName = 'Doe'; } get fullName(){ return this.firstName + ' ' + this.lastName; } }

THE AURELIA TECHNOLOGY STACK

THE AURELIA TECHNOLOGY STACK

SystemJS

“GREAT SCOTT!”

FINALLY

AURELIA ESSENTIALS

• Aurelia packages

• Setting up an Aurelia project

• Using dependency injection in Aurelia

• Creating templates

• Choosing a data binding mode

• Capturing events

• Creating custom attributes and custom elements

• Defining application states

• Test-driven development

AURELIA FRAMEWORK

AURELIA FRAMEWORK

TEMPLATING http://github.com/aurelia/templating

An extensible HTML templating engine.

TASK QUEUE http://github.com/aurelia/task-queue

A simple task queue for the browser.

PATH http://github.com/aurelia/path

Utilities for path manipulation.

METADATA http://github.com/aurelia/metadata

Utilities for reading and writing the metadata.

LOGGING http://github.com/aurelia/logging

A minimal but effective logging mechanism.

LOADER http://github.com/aurelia/loader

An interface for loading modules and view templates.

DEPENDENCY INJECTION http://github.com/aurelia/dependency-injection

An extensible dependency injection container.

BINDING http://github.com/aurelia/binding

A modern databinding library for JavaScript and HTML.

SOME RECOMMENDED OPTIONS

SOME RECOMMENDED OPTIONS

HTTP CLIENT http://github.com/aurelia/http-client

A simple, restful, message-based wrapper around XMLHttpRequest.

ROUTER http://github.com/aurelia/router

A powerful client-side router.

EVENT AGGREGATOR http://github.com/aurelia/event-aggregator

A lightweight pub/sub messaging system.

VIEW ALL PUBLIC REPOS (33) https://github.com/aurelia

INSTALL GULP AND JSPM npm install -g gulp jspm

CONFIGURE JSPM FOR GITHUB jspm registry config github

INSTALL YEOMAN AND THE AURELIA GENERATOR npm install -g yo generator-aurelia

RUN THE AURELIA GENERATOR yo aurelia

START THE SERVER gulp watch

LET’S GET OUR HANDS DIRTY

THE ENTRY POINT TO AN AURELIA APPLICATION<!doctype html> <html> <head> <!-- Load assets not loaded through SystemJS --> </head>

</html>

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body

System

</body>

<div class="splash"></div>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body

System

</body>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body

System

</body>

<script> System.import('aurelia-bootstrapper'); </script>

THE ENTRY POINT TO AN AURELIA APPLICATION

<body aurelia-app="animation-main">

<div class="splash"></div>

<script src="jspm_packages/system.js"></script> <script src="config.js"></script>

<script> System.import('aurelia-bootstrapper'); </script>

</body>

DEPENDENCY INJECTION

import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient) export class MyView{ constructor(http){ this.http = http; }

instanceMethod() { /* Use this.http here */ } }

import {inject} from 'aurelia-framework';

@inject( )

DEPENDENCY INJECTION

importimport

@injectexport class constructor }

instanceMethod }}

import {inject} from 'aurelia-framework';

@inject( )

STEP 1 Import the dependency injection decorator.

DEPENDENCY INJECTION

import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient) export class MyView{ constructor(http){ this.http = http; }

instanceMethod() { /* Use this.http here */ } }

DEPENDENCY INJECTION

importimport

@injectexport class constructor }

instanceMethod }}

import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient)

STEP 2 Import the dependency.

DEPENDENCY INJECTION

import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient) export class MyView{ constructor(http){ this.http = http; }

instanceMethod() { /* Use this.http here */ } }

DEPENDENCY INJECTION

importimport

@injectexport class constructor }

instanceMethod }}

@inject(HttpClient)

constructor(http){

}

STEP 3 Inject the dependency as an argument in the constructor.

DEPENDENCY INJECTION

import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient) export class MyView{ constructor(http){ this.http = http; }

instanceMethod() { /* Use this.http here */ } }

DEPENDENCY INJECTION

importimport

@injectexport class constructor }

instanceMethod }}

this.http = http;

/* Use this.http here */

STEP 4 Use the dependency in the instance methods of the class.

DEPENDENCY INJECTION

import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient) export class MyView{ constructor(http){ this.http = http; }

instanceMethod() { /* Use this.http here */ } }

TEMPLATES

<template>

<require from="./nav-bar"></require>

<nav-bar router.bind="router"></nav-bar>

</template>

TEMPLATES

<template>

</template>

<template>

</template>

STEP 1 Author the template inside a <template> tag.

TEMPLATES

<template>

<require from="./nav-bar"></require>

<nav-bar router.bind="router"></nav-bar>

</template>

TEMPLATES

<template>

</template>

<require from="./nav-bar"></require>

STEP 2 Require dependencies with a <require> tag.

TEMPLATES

<template>

<require from="./nav-bar"></require>

<nav-bar router.bind="router"></nav-bar>

</template>

TEMPLATES

<template>

</template>

<nav-bar router.bind="router"></nav-bar>

STEP 3 Use the custom behavior or custom element.

TEMPLATES

<template>

<require from="./nav-bar"></require>

<nav-bar router.bind="router"></nav-bar>

</template>

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

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

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

value.bind="firstName"

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

value.bind="firstName" .two-way .one-way .one-time

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

.two-way .one-way .one-time

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

.two-way .one-way .one-time

Default for form elements

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

.two-way .one-way .one-time

Default for form elementsDefault for everything else

DATA BINDING: FLEXIBILITY AND SMART DEFAULTS

.two-way .one-way .one-time

Default for form elementsDefault for everything elseMust be explicitly specified

UI EVENTS WITH TRIGGER

<button click.trigger="sayHello()">Say Hello</button>.trigger="sayHello()"

UI EVENTS WITH TRIGGER

.trigger="sayHello()"

UI EVENTS WITH TRIGGER

.trigger="sayHello()"

export class MyViewModel{

sayHello(){ /* Do something... */ }

}

UI EVENTS WITH DELEGATION

<ul> <li repeat.for="item of items"> <button click.trigger="handleClicks($index)"> ${item.name} </button> </li> </ul>

UI EVENTS WITH DELEGATION

<ul click.delegate="handleClicks($event)"> <li repeat.for="item of items"> <button> ${item.name} </button> </li> </ul>

UI EVENTS WITH DELEGATION

.delegate="handleClicks($event)"

UI EVENTS WITH DELEGATION

.delegate="handleClicks($event)"

export class MyViewModel{

handleClicks(event){ /* Do something... */ }

}

APPLICATION EVENTS — PUB/SUB

@inject(EventAggregator) export class Example { constructor(eventAggregator){ this.eventAggregator = eventAggregator; }}

import {inject} from 'aurelia-framework'; import {EventAggregator} from 'aurelia-event-aggregator';

APPLICATION EVENTS — PUB/SUB

@inject(EventAggregator) export class Example { constructor(eventAggregator){ this.eventAggregator = eventAggregator; }

}

APPLICATION EVENTS — PUB/SUB

@inject(EventAggregator) export class Example { constructor(eventAggregator){ this.eventAggregator = eventAggregator; }

}

publish(){ var payload = {}; //any object this.eventAggregator.publish('channel', payload); }

APPLICATION EVENTS — PUB/SUB

@inject(EventAggregator) export class Example { constructor(eventAggregator){ this.eventAggregator = eventAggregator; }

}

publish(){ var payload = {}; //any object this.eventAggregator.publish('channel', payload); } subscribe(){ this.eventAggregator.subscribe('channel', (payload) => { /* Do something... */ }); }

APPLICATION EVENTS — PUB/SUB

publish(){ var payload = {}; //any object this.eventAggregator.publish('channel', payload); }

subscribe(){ this.eventAggregator.subscribe('channel', (payload) => { /* Do something... */ }); }

APPLICATION EVENTS — PUB/SUB

.publish('channel', payload);

.subscribe('channel', (payload) => {});

APPLICATION EVENTS — PUB/SUB

.publish('channel', payload);

var payload = new SpecialObject();

.subscribe(SpecialObject, (payload) => {});

CUSTOM ATTRIBUTES — USAGE

<div show.bind="isSaving"></div>

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ATTRIBUTES — DEFINITION

import

@customAttribute@injectexport class constructor }

valueChanged } }

STEP 1 Import the inject and customAttribute decorators.

import {inject, customAttribute} from 'aurelia-framework';

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ATTRIBUTES — DEFINITION

import

@customAttribute@injectexport class constructor }

valueChanged } }

@customAttribute('show')

STEP 2 Name the custom attribute using the @customAttribute decorator.

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ATTRIBUTES — DEFINITION

import

@customAttribute@injectexport class constructor }

valueChanged } }

@inject(Element)

STEP 3 Inject the element on which the attribute appears in the DOM.

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ATTRIBUTES — DEFINITION

import

@customAttribute@injectexport class constructor }

valueChanged } }

export class Show { constructor(element) { this.element = element; }

}

STEP 4 Add a reference to the element onto the custom attribute object.

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ATTRIBUTES — DEFINITION

import

@customAttribute@injectexport class constructor }

valueChanged } }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } }

STEP 5 Watch for changes in the bound data.

CUSTOM ATTRIBUTES — DEFINITION

import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('show') @inject(Element) export class Show { constructor(element) { this.element = element; }

valueChanged(newValue){ if (newValue) { this.element.classList.remove('aurelia-hide'); } else { this.element.classList.add('aurelia-hide'); } } }

CUSTOM ELEMENTS — USAGE

<template>

<require from="./say-hello"></require>

<input type="text" ref=“name"> <say-hello to.bind=“name.value"></say-hello>

</template>

CUSTOM ELEMENTS — DEFINITION

import {customElement, bindable} from 'aurelia-framework';

@customElement('say-hello') export class SayHello { @bindable to;

speak(){ alert('Hello ${this.to}!'); } }

CUSTOM ELEMENTS — DEFINITION

import

@customElementexport class @bindable to

speak } }

STEP 1 Import the bindable and customElement decorators.

import {customElement, bindable} from 'aurelia-framework';

CUSTOM ELEMENTS — DEFINITION

import {customElement, bindable} from 'aurelia-framework';

@customElement('say-hello') export class SayHello { @bindable to;

speak(){ alert('Hello ${this.to}!'); } }

CUSTOM ELEMENTS — DEFINITION

import

@customElementexport class @bindable to

speak } }

STEP 2 Name the custom element using the @customElement decorator.

@customElement('say-hello')

CUSTOM ELEMENTS — DEFINITION

import {customElement, bindable} from 'aurelia-framework';

@customElement('say-hello') export class SayHello { @bindable to;

speak(){ alert('Hello ${this.to}!'); } }

CUSTOM ELEMENTS — DEFINITION

import

@customElementexport class @bindable to

speak } }

STEP 3 Declare bindable variables using the @bindable decorator.

@bindable to;

CUSTOM ELEMENTS — DEFINITION

import {customElement, bindable} from 'aurelia-framework';

@customElement('say-hello') export class SayHello { @bindable to;

speak(){ alert('Hello ${this.to}!'); } }

CUSTOM ELEMENTS — DEFINITION

import

@customElementexport class @bindable to

speak } }

STEP 4 Provide any methods that should be available in the element’s template.

speak(){ alert('Hello ${this.to}!'); }

CUSTOM ELEMENTS — DEFINITION

import {customElement, bindable} from 'aurelia-framework';

@customElement('say-hello') export class SayHello { @bindable to;

speak(){ alert('Hello ${this.to}!'); } }

CUSTOM ELEMENTS — TEMPLATE

<template> <button click.trigger="speak()">Say Hello To ${to}</button> </template>

APPLICATION STATES

export class MyView{

configureRouter(config, router) { config.map([ { route: ['url'], moduleId: 'path/to/module', nav: true, title:’Page Title’ } ]); }

}

TEST-DRIVEN DEVELOPMENT

• Easiest experience I have had writing tests.

• Simple because of ES6 modularity and dependency injection. Framework stays out of the way wherever possible.

• Process:

• Mock dependencies.

• Inject mocks into module under test.

• Test module behavior.

UNIT TESTING EXAMPLE — MOCK BACK END

class HttpStub { jsonp(url) { var response = this.itemStub; this.url = url; return new Promise((resolve) => { resolve({ content: { items: response } }); }) } }

UNIT TESTING EXAMPLE — TESTS

import {MyView} from '../../src/myview';

describe('the MyView module', () => {

it('sets response to items', (done) => { var http = new HttpStub(), sut = new MyView(http), itemStubs = [1], itemFake = [2];

http.itemStub = itemStubs; sut.activate().then(() => { expect(sut.items).toBe(itemStubs); expect(sut.items).not.toBe(itemFake); done(); }); });

});

http://linkedin.com/in/morrissinger

http://morrissinger.com

@morrissinger

!

"

GET IN TOUCH

Thanks for listening!