AngularJS 2

first impressions by @GionKunz

Gion Kunz

Front End Developer

@GionKunz

gion.kunz@oddeven.ch

https://oddeven.ch

We'll Cover the following

  • Core Concepts
  • New Syntax
  • New Template Syntax
  • Directives & Components
  • Dependeny Injection
  • Writing future proof code

We won't cover

  • Router (just got ported)
  • Testing and Mocking
  • E2E Testing
  • Forms
  • Core Services (XHR etc.)
  • Number crunching

THE EVOLUTION OF WEB STANDARDS

THE MODERN WEB

Object.observe()

ECMAScript 6

Web Components

the three main web standards behind AngularJS 2

OBSERVER

Object.observe()

var object = {};
Object.observe(object, function(changeRecords) {
  changeRecords.forEach(function(cr) {
    console.log(cr.name + ' ' + cr.type);
  });
});
 
object.p1 = 'Property 1';
object.p2 = 'Property 2';
object.p2 = 'Property II';
delete object.p1;

The future
iS

ES6

NOW

ES6 Class

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(this.name + ': Hi there!');
  }
  
  static createPerson(name) {
    return new Person(name);
  }
}

var gion = new Person('Gion');
gion.sayHello();

ES6 Class DESUGARED

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function sayHello() {
  console.log(this.name + ': Hi there!');
};

Person.createPerson = function createPerson(name) {
  return new Person(name);
};

var gion = new Person('Gion');
gion.sayHello();

ES6 Class Inheritance

class Organism {}

class Person extends Organism {
  constructor(name) {
    super();
    this.name = name;
  }
}

class Developer extends Person {
  constructor(name, lovesAngular) {
    super(name);
    this.lovesAngular = lovesAngular;
  }
}

var gion = new Developer('Gion', true);
console.log(gion);

ES6 Class Inheritance DESUGARED

let's skip this...

ES6 TEMPLATE STRINGS

var num = 100;
var str = `The square root of ${num} is ${Math.sqrt(num)}`;
console.log(str);

var className = 'my-class';
var title = 'Welcome';
var html = `
  <div class="${className}">
    <h1>${title}</h1>
  </div>
`;
console.log(html);

WITHOUT ES6 TEMPLATE STRINGS

var num = 100;
var str = 'The square root of ' + num + ' is ' + Math.sqrt(num);
console.log(str);

var className = 'my-class';
var title = 'Welcome';
var html = '\n' +
  '<div class="' + className + '">\n' +
  '  <h1>' + title + '</h1>\n' +
  '</div>\n';
console.log(html);

Many more super cool features!

Use ES6

TODAY!

WEB COMPONENTS

</>

The Standard

Custom Elements

HTML Imports

Templates

Shadow DOM

Templates

<body>
<template id="template">
  <h1>This is a template!</h1>
</template>
</body>
var template = document.querySelector('#template');
var instance = document.importNode(template.content, true);
document.body.appendChild(instance);

Shadow DOM

<body>
<div id="host"></div>
</body>
var host = document.querySelector('#host');
var root = host.createShadowRoot();
var elem = document.createElement('h1');
elem.textContent = 'Bombaclat!';
root.appendChild(elem);

Bored?

One more thing!

ES6 was not enough...

Type annotations

  • Availability at runtime
  • Framework support (DI)
  • Better tooling
  • Runtime type assertion
  • Can improve code quality
  • Code optimization (once standardized)

Meta annotations

  • Availability at runtime
  • Declarative
  • Famework support
  • Better tooling (once stabilized)
  • Less and more explicit code
  • Are proposed to the standard as "decorators"

Es6 vs. AtScript

es6

atscript

TypeScript

Modules

Classes

Type Annotations

Meta Annotations

VS. TYPESCRIPT

Modules

Classes

Type Annotations

Meta Annotations

Modules

Classes

Type Annotations

Meta Annotations

Robot Drumroll...

Are you ready?

AngularJS 2

Warning!
Under heavy construction

alpha 20

Hello World

@Component({
  selector: 'app'
})
@View({
  template: `
    <div>
      <h1>Hello {{ name }}!</h1>
    </div>
  `
})
class App {
  name:string;

  constructor() {
    this.name = 'World';
  }
}

Meta Annotations

Template Strings

Type Annotations

Desugaring TO ES6

class App {}
App.annotations = [
  new Component({
    selector: 'app'
  }),
  new View({
    template: `
      <div>
        <h1>Hello {{ name }}!</h1>
      </div>
    `
  })
];
@Component({
  selector: 'app'
})
@View({
  template: `
    <div>
      <h1>Hello {{ name }}!</h1>
    </div>
  `
})
class App {}

Class level meta annotations are instantiated and put into Class.annotations

Desugaring TO ES5

function App() {
  this.name = 'World';
}

App.annotations = [
  new Component({
    selector: 'app'
  }),
  new View({
    template: 
      '<div>\n' +
      '  <h1>Hello {{ name }}!</h1>\n' +
      '</div>\n'
  })
];

Architecture

AngularJS 1

View

Controller

Directive

Model

Model

Controller

View

Provider

Value

Factory

Constant

Service

DI

AngularJS 2 pre alpha 22

Directive

Component

View

Decorator

Viewport

<<abstract>>

AngularJS 2 POST alpha 22

Directive

Component

View

Injectable

<<*async>>

<<any>>

Bindings

ELEMENT Property bindings

@Component({
  selector: 'counter'
})
@View({
  template: '<input type="text" [value]="num">'
})
class App {
  num:number = 0;

  constructor() {
    setInterval(function() {
      this.num++;
    }.bind(this), 1000);
  }
}
@Component({
  selector: 'app'
})
@View({
  template: `
    <img [src]="src" [attr.data-custom]="custom">
    <input type="number" [class.invalid]="true">
    <div [style.background-color]="bgColor">Colored</div>
  `
})
class App {
  src:string = 'http://lorempixel.com/400/200/';
  custom:string = 'Random image';
  invalid:boolean = true;
  bgColor:string = '#f00';
}

More Property bindings

No 2-way bindings

EVENT Bindings

@Component({
  selector: 'app'
})
@View({
  template: `
    <button (click)="onClick()">Click me</button>
  `
})
class App {
  onClick() {
    alert('Cool!');
  }
}

More EVENT Bindings

@Component({
  selector: 'app'
})
@View({
  template: `
    <input id="inp" type="text"
           (keyup)="onKeyup($event)" (seven)="onSeven()">
  `
})
class App {
  onKeyup(event) {
    if(event.currentTarget.value.length === 7) {
      event.currentTarget.dispatchEvent(new Event('seven'));
    }
  }
  onSeven() {
    alert('Cool!');
  }
}

View references

@Component({
  selector: 'app'
})
@View({
  template: `
    <input #inp type="text">
    <button (click)="onClick(inp.value)">Click Me</button>
  `
})
class App {
  onClick(value) {
    alert(`The value is ${value}`);
  }
}

To sum it up

(event-binding)="expression"

[property-binding]="expression"

#view-reference

all together

@Component({
  selector: 'app'
})
@View({
  template: `
    <input #inp type="text"
           [value]="text" (keyup)="setText(inp.value)">
    <button (click)="reverseText()">Reverse</button>
  `
})
class App {
  text:string = '';

  setText(text) {
    this.text = text;
  }
  reverseText() {
    this.text = this.text.split('').reverse().join('');
  }
}

DO WE STILL NEED 2-WAY Binding?

Good old Directives

Directive

  • Directives are classes which get instantiated as a response to a particular DOM structure
  • Directives are usefull for encapsulating behavior
  • Multiple directives can be placed on a single element
@Directive({
  selector: 'input[pass]',
  properties: { 
    passPhrase: 'pass' 
  },
  events: ['passMatch'],
  hostListeners: { 
    keyup: 'onKeyup($event.target.value)' 
  }
})
class PassInput {
  passPhrase:string;
  passMatch:EventEmitter = new EventEmitter();
  
  onKeyup(value) {
    if(value === this.passPhrase) {
      this.passMatch.next('matched');
    }
  }
}
class MatchCounter {
  count:number = 0;
}

@Component({
  selector: 'app',
  injectables: [MatchCounter]
})
@View({
  template: `<input type="text" pass="hello123"
                    (pass-match)="onPassMatch($event)">
             Matched: {{matchCounter.count}}`,
  directives: [PassInput]
})
class App {
  constructor(matchCounter:MatchCounter) {
    this.matchCounter = matchCounter;
  }

  onPassMatch(event) {
    this.matchCounter.count++;
  }
}

Viewcontainer

  • Directives that use a ViewContainer can control instantiation of child views which are then inserted into the DOM
  • <template> creates a ProtoView that can be instantiated 
  • The child views show up as siblings of the directive in the DOM

template syntax

<ul>
  <li *foo="bar" title="text"></li>
</ul>
<ul>
  <template [foo]="bar">
    <li title="text"></li>
  </template>
</ul>

Becomes this

the new ng-repeat

<ul>
  <li *for="#item of todoService.items"></li>
</ul>

Everything is a Component!

Everything is a Component!

Dependency injection

Injection resolution stages

pre-existing injectors

component injectors

element injectors

  • The terminal injector throws exception or resolves @Optional to null
  • The platform injector resolves browser singleton resources like cookies and location
  • Each component has its own injector
  • Inherited through DOM order of nested components
  • Each element within the shadow DOM of a component has its own injector
  • Inherited through DOM order of nested elements

pre-existing injectors

component injectors

element injectors

  1. Dependencies on the current element
  2. Dependencies on element injectors and their parents until it encounters a Shadow DOM boundary
  3. Dependencies on component injectors and their parents until it encounters the root component
  4. Dependencies on pre-existing injectors

Injection resolution Order

class ListService {
  list:Array<string> = ['One', 'Two', 'Three'];
}

@Component({
  selector: 'app',
  injectables: [ListService]
})
@View({
  template: `List: {{ list }}`
})
class App {
  constructor(listService:ListService) {
    this.list = listService.list;
  }
}

Simple injection

class HelloAbstract {
  hello() { throw new Error('Abstract!'); }
}
class HelloAmerican extends HelloAbstract {
  hello() { return 'Howdy doodie?'; }
}

@Component({
  selector: 'app',
  injectables: [
    HelloAmerican
  ]
})
@View({template: '{{ helloService.hello() }}' })
class App {
  helloService:HelloAbstract;
  constructor(helloService:HelloAmerican) {
    this.helloService = helloService;
  }
}

Using Abstract CLASSES

With @Inject

class HelloAbstract {
  hello() { throw new Error('Abstract!'); }
}
class HelloAmerican extends HelloAbstract {
  hello() { return 'Howdy doodie?'; }
}

@Component({
  selector: 'app',
  injectables: [
    HelloAmerican
  ]
})
@View({template: '{{ helloService.hello() }}' })
class App {
  helloService:HelloAbstract;
  constructor(@Inject(HelloAmerican) helloService:HelloAbstract) {
    this.helloService = helloService;
  }
}

With Injector Binding

class HelloAbstract {
  hello() { throw new Error('Abstract!'); }
}
class HelloAmerican extends HelloAbstract {
  hello() { return 'Howdy doodie?'; }
}

@Component({
  selector: 'app',
  injectables: [
    bind(HelloAbstract).toClass(HelloAmerican)
  ]
})
@View({template: '{{ helloService.hello() }}' })
class App {
  helloService:HelloAbstract;
  constructor(helloService:HelloAbstract) {
    this.helloService = helloService;
  }
}

Promise Injection

class Store {
  values:Array<number> = [1, 2, 3, 4];
  static asyncFactory() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        resolve(new Store());
      }, 5000);
    });
  }
}

@Component({
  selector: 'app',
  injectables: [ bind(Store).toAsyncFactory(Store.asyncFactory) ]
})
@View({ template: 'Values: {{ values }}' })
class App {
  constructor(@InjectPromise(Store) promise:Promise<Store>) {
    promise.then(function(valueStore) {
      this.values = valueStore.values;
    }.bind(this));
  }
}

Element Level Injection

@Directive({
  selector: 'button[kamikaze]'
})
class KamikazeButton {
  constructor(elem:NgElement) {
    elem.domElement.addEventListener('click', function(event) {
      event.currentTarget.style.display = 'none';
    }.bind(this));
  }
}

@Component({ selector: 'app' })
@View({
  template: '<button kamikaze>Remove me</button>',
  directives: [KamikazeButton]
})
class App {}

More Element Level Injection

@Directive({
  selector: '[color-button]',
  hostListeners: {click: 'onClick()'}
})
class ColorButton {
  constructor(@Parent() panel:ColorPanel,
              @Attribute('color-button') color:string) {
    this.color = color;
    this.panel = panel;
  }
  onClick() {
    this.panel.bgColor(this.color);
  }
}

@Directive({ selector: 'color-panel' })
class ColorPanel {
  constructor(@PropertySetter('style.background-color') bgColor:Function) {
    this.bgColor = bgColor;
  }
}

DEMO Time

Future proof Code

Things need to stabilize first

  • Still very experimental and things are changing a lot
  • Router is not fully ported / documented yet
  • No ngResource alternative yet
  • Forms module has not matured yet

My Personal Opinion
Not proven yet

  • Use ECMAScript 6
  • Make everything a directive
  • Always use controllerAs syntax
  • Avoid $scope.$watch and use reactive approaches
  • Only create services
  • Use ES6 classes for controllers and services
  • Use the new router for 1.4 (angular-new-router)

Gion Kunz

Front End Developer

@GionKunz

gion.kunz@oddeven.ch

https://oddeven.ch

Text

Back to the Future with AngularJS 2

By Gion Kunz

Back to the Future with AngularJS 2

  • 17,362