Angular 2 Advanced
by Gerard Sans (@gerardsans)
Laptops charged and ready by 10:00~ish
Instructions bit.ly/ng2-advanced-doc
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
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
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!
Angular 2 Advanced | Gerard Sans
By Mateusz Jabłoński
Angular 2 Advanced | Gerard Sans
Angular 2 is a revolutionary development platform for creating applications using modern web standards and powered by the Open Source Community following the success of AngularJS. Angular 2 introduces some major improvements over its predecessor. We are going to cover the main areas so you can start creating your own projects and enjoy its benefits Today.
- 1,113