Angular 2 Advanced
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
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
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
Change Detection
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