Angular 2 Advanced

by Gerard Sans (@gerardsans)

Laptops charged and ready by 10:00~ish

Instructions bit.ly/ng2-advanced-doc

shield-large Created with Sketch.

Agenda

10:00 Morning block

11:00 Coffee break

13:00  Lunch break

14:00 Afternoon block

15:00 Coffee break

17:00 Wrapping up

1x 35% discount ng-book 2 (£13)

twit to participate in raffle

@angular_zone   #ng2poznan

@angular_zone #ng2poznan

Google Developer Expert

International Speaker

Angular 2 Trainer

Community Leader

Workshop

Angular 2 Advanced

We are going to cover in this workshop

  • Angular 2 and Redux
  • Lazy Loading, preloading
  • Performance and Production
  • Angular 2 Universal
  • TypeScript In-depth
  • Custom decorators

Angular 2 and Redux

Data Layer

DATA CLIENTS

GraphQL

Real-time

ngrx/store

Redux

STATE MANAGEMENT

Dan Abramov

@gaearon

Main Principles

  • Unidirectional data flow
  • Single Immutable State
  • New states are created without side-effects

Unidirectional data flow

Single Immutable State

  • Helps tracking changes by reference
  • Improved Performance
  • Enforce by convention or using  a library. Eg: Immutable.js

Immutable by Convention

  • New array using Array Methods
    • map, filter, slice, concat
    • Spread operator (ES6) [...arr]
  • New object using Object.assign (ES6)

Using Immutable.js

let selectedUsers = Immutable.List([1, 2, 3]);  
let user = Immutable.Map({ id: 4, username: 'Spiderman'}):

let newSelection = selectedUsers.push(4, 5, 6); // [1, 2, 3, 4, 5, 6];  
let newUser = user.set('admin', true);  
newUser.get('admin') // true

Reducers

  • Reducers create new states in response to Actions applied to the current State
  • Reducers are pure functions
  • Don't produce side-effects
  • Composable

Middlewares

  • Sit between Actions and Reducers
  • Used for logging, storage and asynchronous operations
  • Composable

Redux Setup

import { App } from './app';
import { createStore } from 'redux';
 
const appStore = createStore(rootReducer);

@NgModule({
  imports: [ BrowserModule ],
  declarations: [
    App, ...APP_DECLARATIONS
  ],
  providers: [
    { provide: 'AppStore', useValue: appStore },
    TodoActions 
  ],
  bootstrap: [ App ]
})
export class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Adding a new Todo

  • Component subscribe to the Store
  • Component dispatches ADD_TODO action
  • Store executes rootReducer
  • Store notifies Component
  • View updates

Subscribing to the Store

@Component({
  template: 
      `<todo *ngFor="let todo of todos">{{todo.text}}</todo>`
})

export class TodoList implements OnDestroy {
  constructor(@Inject('AppStore') private appStore: AppStore){
    this.unsubscribe = this.appStore.subscribe(() => {
      let state = this.appStore.getState();
      this.todos = state.todos;
    });
  }
  private ngOnDestroy(){
    this.unsubscribe();
  }
}

ADD_TODO Action

// add new todo
{
  type: ADD_TODO,
  id: 1,
  text: "learn redux",
  completed: false
}

todos Reducer

const todos = (state = [], action) => {
  switch (action.type) {
    case TodoActions.ADD_TODO: 
      return state.concat({ 
          id: action.id,
          text: action.text,
          completed: action.completed });
    default: return state;
  }
}

// {
//  todos: [], <-- todos reducer will mutate this key
//  currentFilter: 'SHOW_ALL'
// }

currentFilter Reducer

const currentFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_CURRENT_FILTER':
      return action.filter
    default: return state
  }
}

// {
//  todos: [],
//  currentFilter: 'SHOW_ALL' <-- filter reducer will mutate this key
// }

rootReducer

import { combineReducers } from 'redux'

export const rootReducer = combineReducers({
  todos: todos,
  currentFilter: currentFilter
});

New State

{
  todos: [{
    id: 1,
    text: "learn redux",
    completed: false
  }],
  currentFilter: 'SHOW_ALL'
}

// {
//  todos: [], <-- we start with no todos
//  currentFilter: 'SHOW_ALL'
// }

Stateless Todo Component

// <todo id="1" completed="true">buy milk</todo>
@Component({
  inputs: ['id', 'completed'],
  template: `
    <li (click)="onTodoClick(id)"
      [style.textDecoration]="completed?'line-through':'none'">
      <ng-content></ng-content>
    </li>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Todo { 
  constructor(
    @Inject('AppStore') private appStore: AppStore, 
    private todoActions: TodoActions){ }

  private onTodoClick(id){
    this.appStore.dispatch(this.todoActions.toggleTodo(id));
  }
}

Redux Dev Tools

Features

  • Save/Restore State
  • Live Debugging
  • Time travel
  • Dispatch Actions

Libraries

  • Angular 2 bindings for Redux
  • Built on top of Redux
  • Compatible w/ DevTools and existing ecosystem
  • Re-implementation of Redux on top Angular 2 and RxJS 5
  • ngrx suite: store, effects, router, db
  • Not compatible w/ DevTools and existing ecosystem

Advanced Router

Advanced Features

  • Child routes
  • Navigation Guards
  • Lazy loading/preloading
  • Auxiliary routes
  • Resolve

Child Routes

Child Routes

// users.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { Users } from './users.component';
import { User } from './user.component';
 
const usersRoutes: Routes = [
  { path: 'users', component: Users },
  { path: 'users/:id', component: User }
];

export const UsersRouting = RouterModule.forChild(usersRoutes);

Navigation

  • Using regular links with hash
    • #/home
  • Using routerLink directive
    • ['home']
    • ['users', 34] /users/:id
  • Programatically
    • router.navigate(['users', 34])
    • router.navigateByUrl('/users/34')

routerLink

import { Component } from '@angular/core';
 
@Component({
  selector: 'users',
  template: `
    <h1>Users</h1>
    <tr *ngFor="let user of users">
      <td>
        <a [routerLink]="['/users', user.id]">{{user.username}}</a>
      </td>
    </tr>
  `
})
export class Users { }

Access Router Data

Accessing Current Url

// { path: 'users/:id', component: User } Url: #/users/34
import { Router } from '@angular/router';

@Component({
  selector: 'user-details'
})
export class User implements OnInit { 
  constructor(private router: Router){ }

  ngOnInit() {
    console.log(this.router.url); // /users/34
  }
}

Accessing hash fragment

// { path: 'users/:id', component: User } Url: #/users/34#section
import { Router } from '@angular/router';

@Component({
  selector: 'user-details'
})
export class User implements OnInit { 
  constructor(private router: Router){
    router.routerState.root.fragment.subscribe(f => {
      let fragment = f; // section
    });
  }
  ngOnInit() {
    let fragment = this.router.routerState.snapshot.root.fragment; // section
  }
}

Accessing queryParams

// { path: 'users/:id', component: User } Url: #/users/34?q=1
import { Router } from '@angular/router';

@Component({
  selector: 'user-details'
})
export class User implements OnInit { 
  constructor(private router: Router){
    router.routerState.root.queryParams.subscribe(params => {
      let q = params.q; // 1
    });
  }
  ngOnInit() {
    let q = this.router.routerState.snapshot.root.queryParams.q; // 1
  }
}

Accessing Data

// { path: 'users/:id', component: User, data: { key: 1 } }
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'user-details'
})
export class User implements OnInit { 
  constructor(private route: ActivatedRoute){
    route.data.subscribe(data => {
      let key = data.key; // 1
    });
  }
  ngOnInit() {
    let key = this.route.snapshot.data.key; // 1
  }
}

Accessing Parameters

// { path: 'users/:id', component: User } Url: #/users/34;flag=true
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'user-details'
})
export class User implements OnInit { 
  constructor(private route: ActivatedRoute){
    route.params.subscribe(params => {
      let id = params.id; // 34
    });
  }
  ngOnInit() {
    let id = this.route.snapshot.params.id;  // 34
    let flag = this.route.snapshot.params.flag; // true
  }
}

Navigation Guards

Ask before navigating 

// users.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { UserCanDeactivate } from './user.canDeactivate';

const usersRoutes: Routes = [
  { path: 'users', component: Users },
  { path: 'users/:id', component: User, 
    canDeactivate: [UserCanDeactivate] 
  }
];

export const UsersRouting = RouterModule.forChild(usersRoutes);

Ask before navigating

// user.canDeactivate.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
 
Injectable()
export class UserCanDeactivate implements CanDeactivate {
  canDeactivate() {
    return window.confirm('Do you want to continue?');
  }
}

Restrict Access

// users.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { AuthCanActivate } from './auth.canActivate';

const usersRoutes: Routes = [
  { path: 'users', component: Users },
  { path: 'users/:id', component: User, 
    canDeactivate: [UserCanDeactivate],
    canActivate: [AuthCanActivate]  
  }
];

export const usersRouting = RouterModule.forChild(usersRoutes);

Restrict Access

// auth.canActivate.ts
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { LoginService } from './login.service';

export class AuthCanActivate implements CanActivate {
  constructor(private loginService: LoginService, private router: Router) {}  
  
  canActivate() {
    if (!this.loginService.authorised) {
      this.router.navigate(['/home']);
      return false;
    }
    return true;
  }
}

Lazy Loading

Lazy Loading

// app.routing.ts
const aboutRoutes: Routes = [
  { path: 'about', loadChildren: './app/about.module.ts' },
];
export const AboutRouting = RouterModule.forChild(aboutRoutes);

//about.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AboutRouting } from './about.routing';
import { About } from './about.component';

@NgModule({
  imports: [ CommonModule, AboutRouting ],
  declarations: [ About ]
})
export default class AboutModule { }

Preloading

default preloading

// app.routing.ts
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

const appRoutes: Routes = [
  { path: 'about', loadChildren: './app/about.module.ts' }, 
];

export const AppRouting = RouterModule.forRoot(appRoutes, { 
  useHash: true,
  preloadingStrategy: PreloadAllModules
});

custom preloading

// app.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { CustomPreload } from './custom.preload';

const appRoutes: Routes = [
  { path: 'about', loadChildren: ..., data: { preload: true } }, 
];

export const AppRouting = RouterModule.forRoot(appRoutes, { 
  useHash: true,
  preloadingStrategy: CustomPreload
});

custom preloading

// custom.preload.ts
import { PreloadingStrategy } from '@angular/router';

export class CustomPreload implements PreloadingStrategy {
  preload(route: Route, preload: Function): Observable<any> {
    return route.data && route.data.preload ? preload() : Observable.of(null);
  }
}

Auxiliary Routes

Auxiliary Routes

// app.routing.ts
const routes: Routes = [
  {path: 'home', component: HelloCmp},
  {path: 'banner', component: Banner, outlet: 'bottom'}
];
// app.component.ts
@Component({
  selector: 'my-app',
  template: `
    <router-outlet></router-outlet>
    <router-outlet name="bottom"></router-outlet>`,
})
export class App { }
// templates/code
<a href="#/home(bottom:banner)">Show Banner!</a>
<a [routerLink]="['/home(bottom:banner)']">Show Banner!</a>
router.navigate('/home(bottom:banner)');

Resolve

Resolve

// about.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { About } from './about.component';
import { AboutResolver } from './about.resolver';

const aboutRoutes: Routes = [
  { path: '', component: About, 
    resolve: { magicNumber: AboutResolver } 
  }
];

export const AboutRouting = RouterModule.forChild(aboutRoutes);

Resolve

// about.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';

@Injectable()
export class AboutResolver implements Resolve {
  resolve() {
    console.log('Waiting to resolve...');
    return new Promise(resolve => setTimeout(() => resolve(4545), 2000));
  }
}

Resolve

// about.component.ts
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'about',
  template: `
    <h1>About</h1>
    <p>Magic Number: {{magicNumber}}</p>`
})
export class About {
  magicNumber: number;
  
  constructor(private route: ActivatedRoute){
    this.magicNumber = route.snapshot.data.magicNumber;
  }
}

Demo

  • Nested routes with parameters
  • Asking before leaving a route
  • Restricting route access
  • Cool SVG loading spinner
  • Lazy Loading, preloading and resolve

Angular 2 Performance

Overview

  • Reduce time for initial load
  • Reduce bundle size
  • Runtime Optimisations
  • Server Side Rendering

Reduce time for initial load

  • Non-blocking scripts (async)
  • Reduce http requests
  • Reduce assets and bundle size
  • Reduce rendering time

Angular 2 Options

  • AoT Compilation (tree shaking)
  • Angular Universal
  • Router (Lazy Load, preload)
  • Angular Mobile Toolkit (Service Worker: offline, cached resources)

Reduce bundle size

  • Webpack (bundling, tree shaking)
  • Rollup (bundling, tree shaking)
  • Closure Compiler (bundling, tree shaking, minification, dead code elimination)
  • AoT Compilation (tree shaking inc. template generated code)

Runtime Optimisations

  • Enable Production Mode
  • AoT Compilation (render during build time)
  • Web Worker (Angular 2 in separate thread)

Server Side Rendering

  • Pre-render content in the Server
  • Client receives rendered content
  • Better SEO for dynamic content

Change Detection

  • Using OnPush
    •  Execute only on input changes
  • Using detach/reattach
    • Execute only when we decide

Change Detection

Overview

  • Keep App state in sync with UI
  • Unidirectional Data Flow
  • Single pass
  • Uses Zone.js to catch changes external to Angular 2

Change Detection

  • Each Component has a CD instance that can take control
  • Executed on every async event
  • Optimal code generated for each view for best performance
  • Reference checks vs deep equals

Change Detection

source: blog

Zone.js

  • Patches async events
  • Adds logic to trigger CD
  • Events covered include:
    • Document events
    • setTimeout(), setInterval()
    • XHR, Observables
  • Doesn't patch IndexedDB events

Change Detection

source: blog

Change Detection

source: blog

Manual CD

import {ChangeDetectorRef} from '@angular/core'

@Component({ selector: 'dashboard', inputs: ['data'] })
export class DataDashboard {
  constructor(private ref: ChangeDetectorRef) {
    ref.detach();
    setInterval(() => {
      this.ref.detectChanges();
    }, 5000);
  }
}

Angular 2 in Production

AoT Compilation

Overview

  • Render templates during Build
  • Reduce Angular bundle size
  • Speed up First Load
  • Reduce Application size

Template Compilation

  • Template Renderer
  • Change detection
  • Just-in-Time Compilation
  • Ahead-of-Time Compilation

ES5/6 Bundle

Change detection

JiT Compilation

V8 (Chrome)

JiT Compilation

Browser

Build

AoT Compilation

ES5/6 Bundle

Change Detection

V8 (Chrome)

AoT Compilation

Build

Browser

Lucidchart using JiT

Lucidchart using AoT

Angular 2 Universal

Angular 2 Universal

Overview

  • SEO (Search Engine Optimisation)
  • Link previews in Social Media
  • Speed up First Load
  • Improve User Experience
  • Render outside the browser

server

response

assets

download

client

init

client

data

render

without SSR

server

response

assets

download

client

init

client

data

render

server view displayed

client

responsive

with SSR

UI unresponsive

Web, Mobile and Desktop

Platforms

DESKTOP

MOBILE

Ionic 2

NativeScript

Electron

MacOS 10.9

Windows 7

Linux

iOS 7

Android 4.2

[Windows 10]

iOS 7

Android 4.1

Windows 8.1

Typescript In-depth

Overview

  • Productivity
  • Quality
  • Collaboration
  • Large Projects

Productivity

  • Access to API documentation
  • Code navigation
  • Auto-complete
  • Enable support for refactoring

Quality

  • Catch errors in the IDE vs runtime
  • TypeScript Linting
  • Automate tasks
  • Less errors

TypeScript Compiler

  • Transpiled to human-readable ES5/6
  • Uses powerful type inference and types
  • Creates type definitions files
  • Allows static code analysis

Environments

  • Command line: tsc
  • Build process: Webpack
  • Browser: SystemJS

Basic Commands

> npm install -g typescript
> tsc --version
> tsc <file.ts> // -> <file.js> (ES5/6)
> tsc --sourcemap <file.ts> // -> sourcemaps 
> tsc --t ES5 main.ts // transpile ES5 (ES3 default)
> tsc -w *.ts  // watch mode
> tsc // will try to use local tsconfig.json with default setup

Main Features

  • Full support for ES5/6
  • Optional types
  • Classes, inheritance
  • Public, private, protected modifiers
  • Decorators, Interfaces, Generics

Optional Types

  • Basic types: number, boolean, string, and void
  • null, undefined, any
  • Browser APIs. Eg: HTMLElement, Document

ES6 Modules

// languagesService.ts
export class LanguagesService {
  get() {
    return ['en', 'es', 'fr'];
  }
}

// home.component.ts
import { LanguagesService } from '../services/languagesService';

class Home {
  constructor(languages: LanguagesService) {
    this.languages = languages.get();
  }
}

Functions

  • Define parameter and return value type
  • Default parameters
  • Optional parameters
  • Arrow functions

Functions Examples

function add(x: number, y: number): number {
  return x + y;
}

let myAdd = function(x: number, y: number): number { 
  return x+y; 
};

let myAdd2 = (x: number, y: number): number => x+y;

Classes

  • Syntax sugar over prototypal inheritance
  • class, constructor, methods
  • getter and setters

Greeter Class

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello, ${this.greeting}`;
  }
}

let greeter = new Greeter("world");

Greeter Class ES5

var Greeter = (function () {
  function Greeter(message) {
    this.greeting = message;
  }
  Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
  };
  return Greeter;
}());

var greeter = new Greeter("world");

Modifiers

  • public, protected, private
  • Accessors are public by default
  • private affects only while using IDE but not during runtime
  • Constructor public/private arguments modifier

Constructor modifier

class Greeter {
  constructor(private greeting: string) { }
}

class Greeter {
  private greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
}

Type Conversions

// <textarea id="myElement" rows="4" cols="50">Some text</textarea>

var txt = (<HTMLTextAreaElement>document.getElementById("myElement")).value

Quick Overview

let isDone: boolean = false;           // boolean
let height: number = 6;                // number
let name: string = "bob";              // string

let list: number[] = [1, 2, 3];        // arrays
let list: Array<number> = [1, 2, 3];   // generics

enum Color {Red, Green, Blue};         // enums
let c: Color = Color.Green;

let notSure: any = 4;                  // if not defined
let list: any[] = [1, true, "free"];

function warnUser(): void {            // return type
  alert('WAT?');
}

TS

Play time!

Custom Decorators

Introduction

  • Available in TypeScript
  • Use metadata to configure classes in a declarative way
  • Requires polyfill for ES7 spec (reflect-metadata)
    • Reflect.defineMetadata(k,v,target)
    • Reflect.getMetadata(k,target)

Options

  • Class decorator
    • @Component, @NgModule
  • Property/Method decorator
    • @Input, @Output
  • Parameter decorator
    • @Inject

Class decorator

// @Component({ selector: "my-app" }) class App { }

function Component(annotation: any): Function {
 return (target: Function) => {
   // add implementation here    
 };
}

@AddTag Class decorator

//@AddTag('super-power') class SuperHero { name: string }

function AddTag(annotation: any): Function {
 return (target: TFunction) => {
   Reflect.defineMetadata("tag", annotation, target);
 };
}

Property decorator

// class SuperHero { 
//   @Input('nickName') name: string;
// }

function Input(annotation: any): Function {
  return (target: TFunction, key: string) => {
    // add implementation here
  }
}

Parameter decorator

// class SuperHero { 
//   constructor(@Log service: Service) { } 
//   constructor(@Log('my-service') service: Service) { } 
// }

function Log(target: TFunction, key: string, index: number) {
  // add implementation here
}

function Log(annotation: string) {
  return (target: TFunction, key: string, index: number) => {
    // add implementation here
  }
}

Thanks!

Made with Slides.com