Angular 2 Advanced

shield-large Created with Sketch.

Angular 2 Advanced

 

  • Angular 2 and Redux
  • Lazy Loading, preloading
  • Performance and Production

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

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!

Angular 2: Advanced

By Pavel Nasovich

Angular 2: Advanced

Angular 2 Advanced topics

  • 1,242