AngularJS 2
first impressions by @GionKunz
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);
the video element is shadow dom: http://camendesign.com/code/video_for_everybody/test.html
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
- Dependencies on the current element
- Dependencies on element injectors and their parents until it encounters a Shadow DOM boundary
- Dependencies on component injectors and their parents until it encounters the root component
- 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)
Text
Back to the Future with AngularJS 2
By Gion Kunz
Back to the Future with AngularJS 2
- 17,319