Further
Angular
Mallon Associates
June 2020
Observers and Observables
• Observable publishes stream of data / events
• Observers handles data stream generated by Observable
– aka a subscriber
• Stream may be processed in between
Observable
filter()
Subscriber
Reactive Programming
• Observer / Observable
– is a well-known design pattern
– is a push model
– supported by a range of languages
– good for asynchronous processing
• Need to be able to
– cancel notifications
– be notified of end of data stream
– to replay data etc.
• Reactive Programming
– is about creating responsive (fast) event-driven applications
– more than just the Observer pattern
– range of libraries support reactive programming
– Angular uses the RxJS library
Observers and Observables
• Observable publishes stream of data / events
• Observers handles data stream generated by Observable
– aka a subscriber
• Stream may be processed in between
Observable
filter()
Subscriber
What is Rxjs?
• Provides an implementation of the Observable type
– needed until the type becomes part of the Angular language
– and, until browsers support it
• Provides utility functions
– for creating and working with observables
• These functions can be used for:
– converting existing code for async operations into observables
– iterating through the values in a stream
– mapping values to different types
– filtering streams
– composing multiple streams
• Library documentation
– see https://rxjs-dev.firebaseapp.com/
Observables
• Observable
– defined in rxjs
– supplies a stream of data values
• Subscribers
– can subscribe to an observable
– will be notified when a value is available
• Observable additional features
– can be cancelled
– and operators such as map, forEach, reduce, retry, replay
Defining Observables
• A handler for receiving observable notifications
• Must implement the Observer interface
– an object that defines callback methods
– to handle the three types of notifications that an observable can send
next Required: Handler for each delivered value
error Optional: Handler for an error notification
complete Optional. Handler for execution-complete notification
Creating Observables
• Creating observables can be done using:
• of
– converts the arguments to an observable sequence
• from
– creates an Observable from:
– an Array, an array-like object, a Promise, an iterable object
– or an Observable-like object
• interval
– creates an Observable that emits sequential numbers
– every specified interval of time, on a specified ShedulerLike
• merge
– creates an output Observable
– concurrently emits all values from every given input Observable
Subscribe Using Observer
app.component.ts
import { Component } from '@angular/core';
import { of } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
// Create simple observable that emits three values
const myObservable = of(1, 2, 3);
// Create observer object
const myObserver = {
next: x => console.log(‘Got next value: ' + x),
error: err => console.error(‘Got an error: ' + err),
complete: () => console.log(‘Got a complete notification'),
};
// Execute with the observer object
myObservable.subscribe(myObserver);
Subscribe Using Observer
Subscribe Using Observer
app.component.ts
import { Component } from '@angular/core';
import { of } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
// Create simple observable that emits three values
const myObservable = of(1, 2, 3);
myObservable.subscribe(
x => console.log(‘Got a next value: ' + x),
err => console.error(‘Got an error: ' + err),
() => console.log(‘Got a complete notification')
);
Subscribe Using Observer
Observable Creation Functions
• Creating an observable from a promise:
app.component.ts
import { Component } from '@angular/core';
import { from } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
// Create an Observable out of a promise
const data = from(fetch('/'));
// Subscribe to begin listening for async result
data.subscribe({
next(response) { console.log(response); },
error(err) { console.error('Error: ' + err); },
complete() { console.log('Completed'); }
});
Observable Creation Functions
• Creating an observable from a promise:
Observable Creation Functions
• Creating an observable from a counter:
app.component.ts
import { Component } from '@angular/core';
import { interval } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
console.log(`It's been ${n} seconds since subscribing!`));
Observable Creation Functions
• Creating an observable from a counter:
Observable Creation Functions
• Creating an observable with a constructor:
app.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
return {unsubscribe() {}};
}
...
Observable Creation Functions
• Creating an observable with a constructor:
app.component.ts
...
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
// execute the Observable and print the result of each notification
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
Observable Creation Functions
• Creating an observable with a constructor:
Operators
• Functions that build on the observables
– to enable sophisticated manipulation of collections
• RxJS defines operators such as:
– map(), filter(), concat(), and flatMap()
• Operators take configuration options
– they return a function that takes a source observable
• When executing this returned function
– operator observes the source observable’s emitted values
– transforms them
– returns a new observable of those transformed values
Operators
• Using the map() operator app.component.ts
import { Component } from '@angular/core';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
const nums = of(1, 2, 3);
const squareValues = map((val: number) => val * val);
const squaredNums = squareValues(nums);
squaredNums.subscribe(x => console.log(x));
Operators
• Using the map() operator
Pipes
• Use to link operators together
– combine multiple functions into a single function
• pipe() function:
– takes the functions to combine as its arguments
– returns a new function which runs the composed functions
– functions are run in sequence
• Need to call subscribe()
– to produce a result through the pipe
Pipes
• Using the pipe() operator app.component.ts
import { Component } from '@angular/core';
import { of, pipe } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
const nums = of(1, 2, 3, 4, 5);
...
Pipes
• Using the pipe() operator
app.component.ts
...
// Create a function that accepts an Observable.
const squareOddVals = pipe(
filter((n: number) => n % 2 !== 0),
map(n => n * n)
);
// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);
// Subscribe to run the combined functions
squareOdd.subscribe(x => console.log(x));
Pipes
• Using the pipe() operator
Pipes
• Using the pipe() method on the Observable
import { Component } from '@angular/core'; app.component.ts
import { of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
const squareOdd = of(1, 2, 3, 4, 5)
.pipe(
filter(n => n % 2 !== 0),
map(n => n * n)
);
squareOdd.subscribe(x => console.log(x));
Observables in Angular
• Angular makes use of observables as an interface
– to handle a variety of common asynchronous operations
• For example:
– custom events sending output data from child to a parent component
– HTTP module for AJAX requests and responses
– Router and Forms modules listen for & respond to user-input events
– AsyncPipe subscribes to an observable & returns latest value emitted
Forms
• HTML provides basic forms features
– input field of different types
• Supporting
– simple validation
– submission of data to URLs
• But limited for real world applications
– custom validation rules
– client-side behavior e.g. data transformations
– approaches to data submission for SPAs
• Many libraries available
– for many technologies / tools / frameworks
Angular Forms
• Fields are treated as first-class citizens
– with fine-grained control over form data
– fully integrated with framework
• Provides two approaches to working with forms
– template driven approach and reactive approach
– supported by different APIs
• Template driven forms
– view template defines structure of form
– format of fields and validation rules
• Reactive forms
– form model created programmatically
Reactive Forms
• Two step process
– create the model programmatically
– connect the model to the view template using directives
Use the ReactiveFormsModule
– provides components, directives and providers
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
ReactiveFormsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Form Model
• Underlying data structure for Form data
– constructed out of forms classes
• FormControl
– atomic form unit
– typically corresponds to a single <input> element
– but can represent a calendar or slider etc.
– keeps current value of element it corresponds to
• FormGroup
– represents a part of a form
– is a collection of FormControls
– status of group is aggregate of components
• FormArray
– variable length collection of FormContols
– e.g. to allow user to enter variable number of family members
Reactive Form Example
• Angular provides a form and input directive
– can be used in view template
app.component.html
<form [formGroup]="loginForm" (ngSubmit)="login()">
<label>username</label>
<input type="text" name="username"
formControlName="username">
<br/>
<label>password</label>
<input type="password" name="password"
formControlName="password">
<br/>
<button type="submit">Submit Details</button>
</form>
Reactive Form Example
• Associated component
– imports form classes from @angular/forms
app.component.ts
import { Component } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from
'@angular/forms';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
username = new FormControl('usrname')
password = new FormControl('')
constructor(private builder: FormBuilder) { }
– from input field names properties on object
Reactive Form Example
constructor(private builder: FormBuilder) { }
loginForm: FormGroup = this.builder.group({
username: this.username,
password: this.password
});
login() {
console.log(this.loginForm.value);
console.log(`Username: ${this.username}`);
console.log(`Password: ${this.password}`)
}
}
Reactive Form Example
• Running the application
Validating Reactive Forms
• Many validator functions provided
• Validators class
– provides validation function provided as static methods
– including required, minLength, maxLength and pattern
– can be imported from @angular/forms
• Specified when model objects are instantiated
username = new FormControl('usrname', Validators.required)
• Can provide a list of validators
password = new FormControl('',
[Validators.required, Validators.minLength(6)])
Validating Reactive Forms
• To test form element validity use valid property
let pwdIsValid: boolean = this.password.valid
• Can obtain list of errors user errors property
– returns JavaScript error objects
– has property with same name as the validator
let errors: {[key: string] : any} = this.password.errors;
– different validators provider different information
– e.g. minLength indicates the required minimum length and the actual
length
– required just returns whether it was required or not
Validating Reactive Forms Example
• AppComponent specifying validators
app.component.ts
import { Component } from '@angular/core';
import { Validators, FormGroup, FormControl, FormBuilder }
from '@angular/forms';
@Component( {
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
username = new FormControl('usrname', Validators.required)
password = new FormControl( '',
[Validators.required, Validators.minLength(6)])
constructor( private builder: FormBuilder ) { }
loginForm: FormGroup = this.builder.group( {
username: this.username,
password: this.password
});
Validating Reactive Forms Example
login() {
let unameIsValid = this.username.valid
if ( !unameIsValid ) {
console.log( this.username.errors )
}
let pwdIsValid: boolean = this.password.valid
if ( !pwdIsValid ) {
let errors: { [key: string]: any } =
this.password.errors;
console.log( 'Pwd Field Errors: ' +
JSON.stringify( errors ) );
}
console.log( this.loginForm.value )
}
}
Validating Reactive Forms Example
• Can also use in view template
– query control object with hasError
app.component.html
<form [formGroup]="loginForm" (ngSubmit)="login()">
<label>username</label>
<input type="text" name="username"
formControlName="username">
<br/>
<label>password</label>
<input type="password" name="password"
formControlName="password">
<br/>
<button type="submit">Submit Details</button>
<div [hidden]="!password.hasError('required')">
The password is required.
</div>
</form>
Validating Reactive Forms Example
• Running the example
Reactive Forms Custom Validation
• Validator functions conform to the ValidatorFn interface
interface ValidatorFn {
(c: AbstractControl): {[key: string]: any};
}
• Can define functions to perform validation
function hasXYZ(input: FormControl) {
// test condition
// return null or error object
}
• Passing values to validation functions
– one function that returns validator function
function minLength(minimum) {
return function(input) {
return
input.value.length >= minimum ? null : {minLength: true};
};
}
Forms Custom Validation Example
• Define hasExclamation function
– can be used with any control
– passed to control constructor
app.component.ts
function hasExclamation(input: FormControl) {
const hasExclamation = input.value.indexOf('!') >= 0;
return hasExclamation ? null : {needsExclamation: true};
}
@Component( {
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
username = new FormControl('usrname', Validators.required)
password = new FormControl( '',
[Validators.required, hasExclamation])
constructor( private builder: FormBuilder ) { }
Forms Custom Validation Example
loginForm: FormGroup = this.builder.group( {
username: this.username,
password: this.password
});
login() {
let unameIsValid = this.username.valid
if ( !unameIsValid ) {
console.log( this.username.errors )
}
let pwdIsValid: boolean = this.password.valid
if ( !pwdIsValid ) {
let errors: { [key: string]: any } =
this.password.errors;
console.log( 'Pwd Field Errors: ' +
JSON.stringify( errors ) );
}
console.log( this.loginForm.value )
}
}
Forms Custom Validation Example
• Can also use in view template
– query control object with hasError
app.component.html
<form [formGroup]="loginForm" (ngSubmit)="login()">
<label>username</label>
<input type="text" name="username"
formControlName="username">
<br/>
<label>password</label>
<input type="password" name="password"
formControlName="password">
<br/>
<button type="submit">Submit Details</button>
<div [hidden]="!password.hasError('needsExclamation')">
The password must include an Exclamation mark.
</div>
</form>
Forms Custom Validation Example
• Using custom validation
Comparison of Approaches
• Both approaches utilize a model
– underlying data structure for storing form data
– created implicitly by template-driven approach
– created programmatically in reactive approach
• Model is not an arbitrary object
– must be type defined by @angular/forms module
– such as FormControl, FormGroup and FormArray
• Both approaches require a HTML template
– Angular does not generate a view automatically
Comparison of Approaches
• Template driven approach
– limited to HTML syntax
– typically used for simpler scenarios
• Reactive approach
– suitable for more complex scenarios
– data structures handled in code
– link HTML template elements to model
– using special directives prefixed with form
Using Pipes
• Pipe is a template element
– that filters / transforms data
Data Source
e.g. component
data or method
Pipe
HTML Template
Using Pipes
• Can apply multiple pipes in a chain
Data Source
e.g. component
data or method
Pipe
Pipe
HTML Template
Predefined Pipes
• Several predefined pipes available
– each implemented by a class e.g. CurrencyPipe, DatePipe
– and name used in a HTML template e.g. currency, date
• CurrencyPipe
– transforms number into desired currency
– name is currency
• DatePipe
– displays a date in different formats
– name is date
• UpperCasePipe / LowerCasePipe
– converts input into upper case / lower case
– name is uppercase / lowercase
Working with Pipes
• The '|' used to link pipe to data
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<p>Total price: {{ price | currency }}</p>`
})
export class AppComponent {
price = 100.12348;
}
• AppComponent defined price instance variable
– accessed within HTML template
– built in currency filter applied to price
– renders currency in USD to 2 decimal places
Working with Pipes
• Result of apply currency pipe to price
Pipes and Parameters
• Parameters passed to pipes
– follow pipe with ':' and parameter value
– can have multiple parameters each separate by ':"
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<p>Total price: {{ price | currency: "GBP" }}</p>
<p>Total price: {{ price | currency: "GBP": true}}</p>`
})
export class AppComponent {
price = 100.12348;
}
Pipes and Parameters
• Result of applying param
Chaining Pipes
• Can combine pipes together
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Total price: {{ price | currency | lowercase }}</p>
`
})
export class AppComponent {
price = 100.12348;
}
Chaining Pipes
• Result of chaining pipes
Custom Pipes
• Can define new pipes
– as utilities or application specific
• Need to annotate class with @Pipe decorator
– providing the name of the pipe
@Pipe( {
name: '<pipename>'
})
export class <ClassName> ...
• Implement the angular/core PipeTransform interface
– PipeTransform interface defines a single method transform
interface PipeTransform {
transform(value: any, ...args: any[]) : any
}
Custom Pipes
• Custom pipe to format file sizes
import { Pipe, PipeTransform } from '@angular/core';
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB'];
const FILE_SIZE_UNITS_LONG =
['Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes'];
@Pipe( {
name: 'formatFileSize'
})
export class FormatFileSizePipe implements PipeTransform {
transform(sizeInBytes: number, longForm: boolean): string
{
const units = longForm
? FILE_SIZE_UNITS_LONG
: FILE_SIZE_UNITS;
let power = Math.round( Math.log( sizeInBytes ) /
Math.log( 1024 ) );
power = Math.min( power, units.length - 1 );
Custom Pipes
// size in new units
const size = sizeInBytes / Math.pow( 1024, power );
// keep up to 2 decimals
const formattedSize = Math.round( size * 100 ) / 100;
const unit = units[power];
return `${formattedSize} ${unit}`;
}
}
Using Custom Pipes
• Module imports the pipe
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormatFileSizePipe } from './format-file-size.pipe';
@NgModule({
declarations: [
AppComponent,
FormatFileSizePipe
],
imports: [ BrowserModule ],
bootstrap: [AppComponent]
})
export class AppModule { }
Using Custom Pipe
• AppComponent uses named pipe in template
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<p *ngFor="let f of fileSizes">{{f |
formatFileSize}}</p>
<p>{{ largeFileSize | formatFileSize: true }}</p>
</div>`
})
export class AppComponent {
fileSizes = [10, 1024, 10240];
largeFileSize = 10000000;
}
Using Custom Pipes
• Output generated from formatFileSize pipe
Stateful Pipes
• Two categories of Pipe
• Stateless Pipes
– pure functions
– filter input through to output
– without storing anything or having any side effects
– most pipes are stateless
• Stateful Pipes
– hold some state / data
– can influence processing of data based on state
– use with care
– AsyncPipe is a stateful pipe
Implementing Stateful Pipes
• Pipes are stateless by default
• Must set pure property of decorator to false
import { Pipe, PipeTransform } from '@angular/core';
@Pipe( {
name: 'logNumber',
pure: false
})
export class LogNumberPipe implements PipeTransform {
private lastNumber: number = 5;
private temp: number = this.lastNumber;
transform( n: number ): number {
this.temp = this.lastNumber;
this.lastNumber = n;
console.log("previousNumber= " + this.temp +
", n = " + n);
return n + 1;
}
}
Implementing Stateful Pipes
• Module declares the pipe and AppComponent
– references pipe in HTML template
import { Component } from '@angular/core';
@Component( {
selector: 'app-root',
template: `
<ul>
<li>The number is {{ total | logNumber }}</li>
</ul>`
})
export class AppComponent {
total = 1;
constructor() {
setInterval(() => {
this.total += 2;
}, 1000 );
}
}
Implementing Stateful Pipes
• Output generated from log number pipe
Projection
• Allows developers to create reusable components
• Allows content placed between components start and end
tags
• Can be projected into child's HTML template
• Makes use of the Shadow DOM
• Shadow DOM
– refers to the ability of the browser to include a subtree of DOM
elements
– into the rendering of a document, but not into the main document
DOM tree
Projection
• Content defined between the child start and end tags
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h2>My Application</h2>
<child>
This is the <i>content</i> from the parent.
</child>
`
})
export class AppComponent { }
Projection
• ng-content used to indicate projection insertion point
child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'child',
template: `
<div style="border: 1px solid blue; padding: 1rem;">
<h4>Child Component</h4>
<ng-content></ng-content>
</div>`
})
export class ChildComponent { }
Projection
• A child having content projected from parent
Projection Selection
• It is possible to select the content to be projected
• Can be based on
– predefined directive selection or css classes
• Child with several different sections and div as content
<div>
<h4>App Component</h4>
<child>
<section>Section Content</section>
<div class="selectionkey">
div with selectionkey
</div>
<footer>Footer Content</footer>
<header>Header Content</header>
</child>
</div>
Projection Selection
child.component.ts
@Component({
ng-content select used
selector: 'child',
to identify content to
template: `
project
<div>
<h4>Child Component</h4>
<div style="border: 1px solid orange; padding: 1rem">
<ng-content select="header"></ng-content>
</div>
<div style="border: 1px solid green; padding: 1rem">
<ng-content select="section"></ng-content>
</div>
<div style="border: 1px solid blue; padding: 1rem">
<ng-content select=".selectionkey"></ng-content>
</div>
<div style="border: 1px solid purple; padding: 1rem">
<ng-content select="footer"></ng-content>
</div>
</div>`
})
export class ChildComponent { }
Projection Selection
• Child Component select specific content to project
@ContentChild and @ContentChildren
• Angular projects content into ng-content
– can access components generating content
App
Component
HelloList
Component
content
Hello Hello Hello
Component Component Component
@ContentChild and @ContentChildren
• Similar to @ViewChild and @ViewChildren
– but selects from projected content
app.component.ts
import { Component } from '@angular/core';
@Component( {
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {}
app.component.html
<h2>The message list</h2>
<hellolist>
<app-hello name="World"></app-hello>
<app-hello name="Other World"></app-hello>
<app-hello name="Last World"></app-hello>
</hellolist>
@ContentChild and @ContentChildren
• ng-content used to to include any content provided
hellolist.component.ts
import {Component, ContentChildren, QueryList}
from '@angular/core';
import {HelloComponent} from './hello.component';
@Component( {
selector: 'hellolist',
template: `<div><ng-content></ng-content></div>
<p><button (click)="onClickChangeColors()">
Randomize colours
</button></p>`
})
export class HelloListComponent {
@ContentChildren( HelloComponent )
children: QueryList<HelloComponent>;
onClickChangeColors() {
this.children.forEach( c=> c.randomizeColor() );
}
}
@ContentChild and @ContentChildren
• HelloComponent displays message in random colors
hello.component.ts
@Component({
selector: 'app-hello',
template: `<p [ngStyle]="{ 'color': color }">Hi
{{name}}</p>`
})
export class HelloComponent {
@Input() name: string;
colors: string[] = ['#ff0000', '#00FF00', '#0000FF',
'#00ffff', '#ff00ff', '#ffff00'];
color = this.colors[0];
private getRandomColor() {
return this.colors[Math.floor(Math.random() * 6)];
}
randomizeColor() {
this.color = this.getRandomColor();
}
}
@ContentChild and @ContentChildren
• Executing the random color HelloComponent example
Component Lifecycle
• Managed by Angular framework
– manages creation, rendering, data binding etc.
• Lifecycle hooks available
– ngOnChanges - called when an input binding value changes
– ngOnInit - after the first ngOnChanges
– ngDoCheck - after every run of change detection
– ngAfterContentInit - after component content initialized
– ngAfterContentChecked - after every check of content
– ngAfterViewInit - after component's view(s) are initialized
– ngAfterViewChecked - after every check of a component's view(s)
– ngOnDestroy - just before the component is destroyed
• Information on lifecycle hooks
– https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
Component Lifecycle Example
• Component with lifecycle hooks
import {Component, SimpleChange, OnChanges, OnInit,
DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit,
AfterViewChecked, OnDestroy, EventEmitter
} from '@angular/core';
@Component( {
selector: 'my-component',
template: `
<div>
<h2>My Component</h2>
</div>
`
})
export class ChildComponent implements OnChanges,
OnInit, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit,
AfterViewChecked, OnDestroy {
Component Lifecycle Example
ngOnInit() {
console.log( 'onInit' );
}
ngDoCheck() {
console.log( 'doCheck' );
}
ngAfterContentInit() {
console.log( 'afterContentInit' );
}
// Called after every change detection check
// of the component (directive) CONTENT
// Beware! Called frequently!
ngAfterContentChecked() {
console.log('afterContentChecked');
}
Component Lifecycle Example
ngAfterViewInit() {
console.log('afterViewInit');
}
// Called after every change detection component check
// Beware! Called frequently!
ngAfterViewChecked() {
console.log('afterViewChecked');
}
// Only called if there is an [input] variable set by parent
ngOnChanges(changes: {
[propertyName: string]: SimpleChange }) {
console.log('ngOnChanges' + changes);
}
ngOnDestroy() {
console.log('onDestroy');
}
}
Component Lifecycle Example
• Console output for example
ElementRef
• The ElementRef class allows direct access to the DOM
– provides access to the underlying DOM element
import {AfterContentInit, Component, ElementRef}
from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements AfterContentInit {
constructor(private elementRef: ElementRef) { }
ngAfterContentInit() {
const el = this.elementRef.nativeElement;
const tmp = document.createElement('p');
const node = document.createTextNode("Hi New World");
tmp.appendChild(node);
el.appendChild(tmp); <h1>My App</h1>
} <div>
} I am here <b>OK</b>
</div>
app.component.html
ElementRef
• Result of appending child directly to DOM element
• Angular documentation advises caution
– only use this API as a last resort
What is a Directive?
• HTML has a finite, limited vocabulary
– <ul> specifies render an unordered list
– <video> specifies to render a video
• Fixed nature of HTML
– makes developing modern web apps harder
• Developers use variety of technologies
– such as CSS and JavaScript to implement new features
– but results may not be obvious
• Image being able to extend HTML
– to generate output / change the display
– alter the behaviour or layout of DOM elements
What is a Directive?
• Directives are Angular solution to fixed nature of HTML
– "HTML enhanced for web apps"
• Directives elegant way to extend HTML
– add new attributes, tags etc.
• Enable reusable data and functions
– e.g. ngClick
• Simplify definition and rendering of HTML view
• Components really one class of custom directive!
Categories of Directive
• Three main types of directive in Angular
Angular 2
Directives
Attribute Structural
Component
directive directive
• Component
– a directive with a HTML template
– extend HTML as required
• Attribute Directives
– change the behavior of a component or element
– but don't affect the template
• Structural Directives
– modify how the template is rendered
– in order to change behavior of a component or element
Attribute Directives
• Change appearance or behavior of native DOM elements
• Typically independent of the DOM element
• Examples of attribute directives
– ngClass
– ngStyle
– both work on any element
• May be applied in response to user input, service data etc.
NgStyle Directive
• Built in directive
– used to modify the style attribute of an element
• Can be used within a component's template
– can bind a directive in similar manner to a property
– e.g. <p [ngStyle] ="{'color': 'blue'}">Some test</p>
• Can generate style information from component data
– allows style to change dynamically
– e.g. <p [ngStyle] ="componentStyles">Some test</p>
export class StyleExampleComponent {
borderStyle = 'black';
componentStyles = {'color': 'blue',
'border-color': this.borderStyle };
}
Structural Directives
• Handles way component or element renders
– through use of the template tag
• Several built in structural directives
– ngIf, ngFor and ngSwitch
• Template tag
– HTML element with special properties
– nested content is not rendered within page
– intended for client side code to handle
@Component({
selector: 'my-comp',
template: `
<template [myStructuralDirective]="somedata">
<p>
Under a structural directive.
</p>
</template>`
})
Structural Directives
• Have special syntax to make then easier to use
– can be prefixed with an '*'
– don't then need to use '[' and ']'
@Component({
selector: 'app-directive-example',
template: `
<p *structuralDirective="somedata">
Under a structural directive.
</p>
`
})
– *ngIf indicates this is a structural directive
– commonly asked question on forums – why the '*'
Custom Directives
• Can define custom directives
• Typescript class decorated with @Directive
• @Directive decorator defines metadata
– selector used with directive
– inputs list of class property names to data bind
– outputs list of class property names that expose output events
– providers list of providers available for directive
• Must belong to an NgModel
• Can implement lifecycle hooks
– similar to Components
Defining a Custom Directive
• Attribute directive
– to change the color of any content to red
change-color.directive.ts
import { Directive, ElementRef, Renderer }
from '@angular/core';
@Directive( {
selector: '[changecolor]'
})
export class ChangeColorDirective {
constructor( private el: ElementRef,
private render: Renderer ) {
this.render.setElementStyle(
this.el.nativeElement, 'color', 'red' );
}
Defining a Custom Directive
• Must declare directive as part of module
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ChangeColorDirective } from './change-color.directive';
@NgModule( {
declarations: [
AppComponent,
ChangeColorDirective
],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
Defining a Custom Directive
• Can use directive within component template
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<p changecolor>{{message}}</p>`
})
export class AppComponent {
message = 'Hello!';
}
Defining a Custom Directive
• Result of applying custom attribute directive
Passing data to directives
• It is also possible to pass data into a directive
– value bound to directive property marked with @Input()
@Directive( {
selector: '[changecolor]'
})
export class ChangeColorDirective {
@Input('changecolor') highlightColor: string;
constructor( private el: ElementRef,
private render: Renderer ) {
this.render.setElementStyle(
this.el.nativeElement, 'color',
highlightColor );
}
}
• Values specified when attribute directive is specified
– note must now surround with square brackets as value being bound
<p [changecolor]="blue">{{message}}</p>
Passing information
• Common to use services the share data
– between components
– between parent and child components etc.
• Can also use input bindings
– e.g. @Input info:string or @Input('myalias') info:string
• Can intercept property changes with a setter method
@Component({
...
})
export class ChildComponent {
private _name;
@Input()
set name(name: string) {
this._name = name.trim();
}
}
Reactive to Property Changes
• Can react to input property changes
– using ngOnChanges
@Component({
...
})
export class ChildComponent implements OnChanges {
@Input() private name;
ngOnChanges(changes: SimpleChanges}) {
for (let propName in changes) {
let changedProp = changes[propName];
console.log(
`Changed ${propName} from ${changedProp.previousValue}
to ${changedProp.currentValue}');
}
}
}
Parents Listen for Child Events
• Parent components can listen for child events
• Child Component exposes an EventEmitter property
– can use to emit events
• An EventEmitter
– is an instance variable decorated with @Output
– an instance of EventEmitter<type>()
– defined in @angular/core
• Parent can bind to event emitter property
– can react to those events
Component Event Example
• Count Component
– emits an event indicating new value counter.component.ts
import { Component, Input, Output, EventEmitter }
from '@angular/core';
@Component({
selector: 'counter',
template: `<p><ng-content></ng-content>
Count: {{ count }} -
<button (click)="increment()">Increment</button></p>`
})
export class CounterComponent {
@Input() count: number = 0;
@Output() countChanged = new EventEmitter<number>();
increment() {
this.count++;
this.countChanged.emit(this.count);
}
}
Component Event Example
• Parent component
app.component.ts
@Component( {
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
num: number = 0;
childValue: string = 'empty';
onCountChanged( n: number ) {
this.childValue = `From Child ${n}`;
}
app.component.html
}
<counter [(count)]="num"
(countChanged)="onCountChanged($event)">
Number:
</counter>
<ul>
<li>num: {{num}}</li>
<li>Child Value: {{childValue}}</li>
</ul>
Component Event Example
• The parent is notified when the child makes a change
DOM Events
• Can also handle DOM events
• Structure of elements in a page
– elements are nested inside other elements
<body>
<div>
<p>
<ul>
<li>...</li>
• What happens when an DOM event occurs?
– one (or more) of the elements could be responsible for handling event
– when the list item is clicked – what should respond?
DOM Level 2 Event Propagation
• Two alternative approaches
capturing bubbling
<table>
<tr>
<td>
• Event capture
– event is first given to most all-encompassing element
– then successively more specific ones
• Event bubbling
– event is first sent to most specific element
– then successively less specific ones
• Not all browsers used to support capture
– default is therefore bubbling
Handling DOM Events
• Define event handler method in component
– note receive event object in method
@Component(...)
class MyComponent {
clicked(event) {
console.log(event);
}
}
– can prevent further process of event using event.preventDefault();
• Indicate method to call on event in template
– e.g. clicking a button
– note use parenthesis notation in templates
<button (click)="clicked()">Click</button>
– to capture event object pass $event
<button (click)="clicked($event)">Click</button>
Child Routes
• Child route only accessible within context of (parent) route
– parent view presents product information
– tabs within this allow details, pictures, specifications to be viewed
– but these tabs only make sense in the content of the select product
– each tab is a child of the parent produce details view
• If the user selects product with id 3
– overview view is relative to id 3
– details view is relative to id 3 etc.
– overview and details are child routes of the product route
• Child routes defined within Route Definition object
– using the children property
Child Routes
• Two child routes defined relative to 'comp-two' route
– can define a default path
– each child route has a name and the target for the path
export const routes: Routes = [
{ path: '', redirectTo: 'comp-one', pathMatch: 'full' },
{ path: 'comp-one', component: ComponentOne },
{ path: 'comp-two', component: ComponentTwo,
children: [
{ path: '', redirectTo: 'child-one',
pathMatch: 'full' },
{ path: 'child-one', component: ChildOne },
{ path: 'child-two', component: ChildTwo }
]
}
];
Child Routes Example
• Application with child routes
App
Component
top level routes
Component Component
One Two
child routes
Child One Child Two
Child Routes Example
• Root Component unaware of child routes
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {}
app.component.html
<nav>
<a routerLink="/comp-one">Component One</a>
<a routerLink="/comp-two">Component Two</a>
</nav>
<h2>Selected View</h2>
<div style="border: 2px solid blue; padding: 1rem">
<router-outlet></router-outlet>
</div>
Child Routes Example
• Component Two
– references child routes
– has router-output in template for location of child content
– children are encapsulated into the component
@Component( {
selector: 'component-two',
templateUrl: './component-two.html'
})
export class ComponentTwo {}
component-two.html
<h3>Component Two</h3>
<a [routerLink]="['child-one']">Child One</a>
<a [routerLink]="['child-two']">Child Two</a>
<div style="border: 2px solid red">
<h4>Component Two: Child View</h4>
<router-outlet></router-outlet>
</div>
Child Routes Example
• Child components defined as usual
import { Component } from '@angular/core';
@Component({
selector: 'child-one',
template: 'Child One'
})
export class ChildOne {
}
import { Component } from '@angular/core';
@Component({
selector: 'child-two',
template: 'Child Two'
})
export class ChildTwo {
}
Child Routes Example
• The running application
Child Routes and Parameters
• Child routes can have parameters
– just as top-level routes have parameters
– specified in Route Definition Object
export const routes: Routes = [
{ path: '', redirectTo: 'comp-one', pathMatch: 'full' },
{ path: 'comp-one', component: ComponentOne },
{ path: 'comp-two', component: ComponentTwo,
children: [
{ path: '', redirectTo: 'child-one',
pathMatch: 'full' },
{ path: 'child-one', component: ChildOne },
{ path: 'child-two/:num', component: ChildTwo }
]
}
];
Child Routes and Parameters
• Child component access route parameter information
– in same way as top-level route parameters
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'child-two',
template: 'Child Two (num: {{ num }})'
})
export class ChildTwo {
private sub: any;
private num;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.num = +params['num'];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
Child Routes and Parameters
• Parent can set value for child route parameter
@Component( {
selector: 'component-two',
templateUrl: './component-two.html'
})
export class ComponentTwo {}
component-two.html
<h3>Component Two</h3>
<a [routerLink]="['child-one']">Child One</a>
<a [routerLink]="['child-two', 987]">Child Two</a>
<div style="border: 2px solid red">
<h4>Component Two: Child View</h4>
<router-outlet></router-outlet>
</div>
Accessing Parent Path Parameters
• Child can access parent's route parameters
– accessible from ActivatedRoute object's parent property
– provides access to parent property
– parent property allows access to params
• Handle same was as other parameters
– can subscribe to property
– notified when value changes
• Parent unaware child is access property
– parent can still subscribe to params as usual
Accessing Parent Path Parameters
• Routes array defined paths with ids
– comp-two path has :id
• Can access this from child components
– e.g. in ChildOne from 'child-one' route
export const routes: Routes = [
{ path: '', redirectTo: 'comp-one', pathMatch: 'full' },
{ path: 'comp-one', component: ComponentOne },
{ path: 'comp-two/:id', component: ComponentTwo,
children: [
{ path: '', redirectTo: 'child-one',
pathMatch: 'full' },
{ path: 'child-one', component: ChildOne },
{ path: 'child-two', component: ChildTwo }
]
}
];
Accessing Parent Parameter
• ChildOne access :id from parent path
– stored in parentRouteId used in view template
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'child-one',
template: `
Child One, reading parent route param.
<b><code>Parent ID: {{ parentRouteId }}</code></b>
`
})
export class ChildOne {
private sub: any;
private parentRouteId: number;
constructor(private route: ActivatedRoute) {}
Accessing Parent Parameter
ngOnInit() {
this.sub = this.route.parent.params.subscribe(params =>
{
this.parentRouteId = +params["id"];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
Controlling Access to Routes
• Route guards
– can be used to control whether a user can
– navigate to or from
– a given route
• For example,
– a route is only available once user has logged in
– or they are unable to navigate away from a route until they have
accepted terms & conditions etc.
• Three interfaces used to define guards
– CanActivate used to allow a route to be followed
– CanDeactivate defines class used to say if route can be deactivated
– CanActivateChild for guarding child routes
– both defined in @angular/route
Controlling Access to Routes
• CanActivate interface
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) :
Observable<boolean>|Promise<boolean>|boolean
• CanDeactivate interface
canDeactivate(component: T,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot) :
Observable<boolean>|Promise<boolean>|boolean
}
Controlling Access to Routes
• Registering Route Guards
– route definition object
– defines canActivate and canDeactivate properties
– canActivate defines a guard that decides if route can be activated
– canDeactivate defines a guard to check if route can be deactivated
export const routes: Routes = [
{ path: '', redirectTo: 'comp-one', pathMatch: 'full' },
{ path: 'comp-one', component: ComponentOne },
{ path: 'comp-two', component: ComponentTwo,
canActivate: [ActivateGuard],
canDeactivate: [DeactivateGuard]
}
];
– ActivateGuard will be checked by router before activating comp-two
– DeactivateGuard will be checked before leaving route
Implementing Access Control Guards
• Application structure
App Module
providers
App
Component
comp-one comp-two
ActivateGuard
Component Component
One Two
DeactivateGuard
passed as parameter
Implementing Access Control Guards
• Must implement appropriate interfaces
– if injected into components must be marked as @Injectable()
– common as application typically determines activation
import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';
@Injectable()
export class ActivateGuard implements CanActivate {
private flag: boolean = false;
canActivate() {
if (!this.flag) {
alert('Activation blocked');
return false;
}
return true;
}
setCanActivate(flag) {
this.flag = flag;
}
}
Implementing Access Control Guards
• DeactivateGuard references CompnentTwo
– this is injected by Angular & used to check if route can be deactivated
import { CanDeactivate } from '@angular/router';
import { ComponentTwo } from './component-two';
export class DeactivateGuard
implements CanDeactivate<ComponentTwo> {
canDeactivate(component: ComponentTwo) {
let flag = component.canDeactivate();
if (!flag) {
alert('Deactivation blocked');
return false;
}
return true;
}
}
Implementing Access Control Guards
• Module declaration
– guards listed in providers property
– ensures they are scoped to the module
– act like a service for routing decisions
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes)
],
declarations: [AppComponent, ComponentOne, ComponentTwo ],
providers: [ ActivateGuard, DeactivateGuard ],
bootstrap: [ AppComponent ]
})
export class AppModule {}
Implementing Access Control Guards
• AppComponent
– has activate guard injected to it
– can then set flag on ActivateGuard as appropriate
import { Component } from '@angular/core';
import { ActivateGuard } from './activate-guard';
@Component( {
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor( private activateGuard: ActivateGuard ) { }
checkboxChanged( canActivate ) {
// Update guard when checkbox checked.
this.activateGuard.setCanActivate( canActivate );
}
}
Implementing Access Control Guards
• ComponentTwo maintains a flag
– used to check whether the route can be deactivated
import { Component } from '@angular/core';
@Component({
selector: 'component-two',
templateUrl: './component-two.html'
})
export class ComponentTwo {
private checked = false;
canDeactivate() {
return this.checked;
}
checkboxChanged(checked) {
this.checked = checked;
}
}
Implementing Access Control Guards
• Component two guarded
Implementing Access Control Guards
• Deactivation blocked
Secondary Routes
• Multiple independent routes in a single application
• Each component has a primary route
– zero or more auxiliary outlets
– auxiliary outlets uniquely named within component
• Secondary routes
– are independent routes
– can have own child routes
– may also have own auxiliary routes
– can have own route-params
– have own history stack
• The <router-outlet> has an optional name property
– referenced in routerLink
Secondary Routes Example
• AppComponent
– defines 'footer' router-outlet in view template
– plus routerLink for named outlet and comp-aux path
app.component.html
<div>
<profile #profile></profile>
<a [routerLink]="['/comp-one']">Component One</a>
<a [routerLink]="['/comp-two']">Component Two</a>
<a [routerLink]=
"[{ outlets: { 'footer': ['comp-aux'] } }]">Component
Aux</a>
<p><b>Outlet:</b></p>
<div style="border: 2px solid green">
<router-outlet></router-outlet>
</div>
<p><b>Sidebar Outlet:</b>
<div style="border: 2px solid blue">
<router-outlet name="footer"></router-outlet>
</div>
Secondary Routes Example
• Routes define auxiliary route object
– with a named outlet
app.component.ts
export const routes: Routes = [
{ path: '', redirectTo: 'comp-one', pathMatch: 'full' },
{ path: 'comp-one', component: ComponentOne },
{ path: 'comp-two', component: ComponentTwo },
{ path: 'comp-aux', component: ComponentAux, outlet: 'footer'
}
];
• Module must declare the auxiliary component
app.module.ts
@NgModule( {
...
declarations: [
AppComponent, ComponentOne, ComponentTwo, ComponentAux ],
})
export class AppModule {
Secondary Routes Example
• The secondary display
Change Detection
• Very different to AngularJS 1.x approach
– based on watchers checked each time the digest-cycle ran
– bindings could be two-way
– could result in a lot of checking
– could represent a performance bottleneck
• Angular approach
– all flow is unidirectional
– two-way event binding is treated as 2 one-way bindings
– application code is responsible for updating models
– Angular only responsible for reflecting changes
Two Phases
• Separate phases for
– updating the application model
– reflecting state of model in the views
Two Phases
Change
Event Phase Detection
Phases
Event Model Property View
Bindings Updates Bindings Updates
Change Detector Classes
• Created at runtime by Angular
– have AppComponent
– will get an AppComponent_ChangeDetector
• Detector object has reference back to component
• Determines which model properties
– used in the components view template
– have changed
– since the change detection process ran
• Conceptually change detector
– records component property values
– next time it runs it compares current values with those recorded
– if values have changed updated the component
Change Detection
• Angular applications built from hierarchy of components
App
Component
Menu Admin ToDoList
Component Component Component
ToDo ToDo
Component Component
Change Detection
• Angular applications built from hierarchy of components
change detector
traverses each node App
once from the root Component
Menu Admin ToDoList
Component Component Component
ToDo ToDo
Component Component
Change Detection
• Angular applications built from hierarchy of components
App parent is always
checked before
Component
child components
Menu Admin ToDoList
Component Component Component
ToDo ToDo
Component Component
Change Detection
• Angular applications built from hierarchy of components
App
Component
1 3
2
Menu Admin ToDoList
Component Component Component
4 5
evaluation is depth ToDo ToDo
first
Component Component
How Change Detection Works
• Every component has a change detector
App App_ChangeDetector
Component
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
ToDo ToDo ToDo_ ToDo_
Component Component ChangeDetector ChangeDetector
How Change Detection Works
• Every component has a change detector
App App_ChangeDetector
Component
todos:List<ToDo>
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
todo: ToDo todo: ToDo
ToDo ToDo
Component Component {{todo.text}} {{todo.text}}
todo.done todo.done
ToDo_ ToDo_
ChangeDetector ChangeDetector
Change Detection and Two-Way Bindings
• No change detection mechanism for two-way bindings
– implemented as two one-way bindings
– via NgModel and the banana syntax e.g. [(ngModel)]-"property"
– really a data binding from model to view
– an event binding view to the component
Event Binding
View Template Component
Property Binding
• Allows change detection graph to be a directed tree
– no cycles
– promotes more performant system
– more predictable behavior
– compared to AngularJS 1.x approach
Change Detection Strategies
• Every component has a change detection strategy
• There are two change detection strategies available
– Default
– OnPush
• Default
– change detector's mode will be set to CheckAlways
• OnPush
– change detector's mode will be set to CheckOnce
• Defined on the @Component changeDetection property
– values defined by the ChangeDetectionStrategy class
Change Detection Strategies
• Setting the change detection strategy on a component
app.component.ts
import { Component, ChangeDetectionStrategy }
from '@angular/core';
@Component({
selector: 'app-root',
template: `<h2>Parent</h2>
<hr />
<child-comp></child-comp>
<hr />
`,
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent { }
Change Detection Strategy
• Default Strategy
– will traverse all the components in the tree to check for changes
• Performance Impact
– checking all components can be quite costly
• OnPush strategy
– used to indicate that a component only relies on its inputs
– any other component passed to it is considered immutable
– children do not need to keep being checked; they are immutable
– can cut out portions of the dependency tree
Change Detection Strategies
• If the ToDoList is marked as using OnPush
App App_ChangeDetector
Component
OnPush
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
ToDo ToDo ToDo_ ToDo_
Component Component ChangeDetector ChangeDetector
can skip change
detection on these
components
Zones
• Zones provide an execution context
– that encapsulates and intercepts asynchronous activities
– can think of it as thread local storage for JavaScript
– provided by Zone.js
• Used by Angular to track the start and completion
– of asynchronous operations
– but also supports change detection
• Angular has a global zone
– defined by NgZone service with extensions
• NgZone service
– makes several observables available
– useful for determining state of Angular's Zone
In the Zone
• NgZone exposes set of Observables
– to determine status / stability of Angular Zone
• Observables available
• onUnstable – notifies code has entered & executing in zone
• onMicrotaskEmpty – indicates microtasks are queued for execution
• onStable – notifies when the last onMicroTaskEmpty has run
• onError – notifies when an error has occurred
• To subscribe inject NgZone
• into components / services etc.
• Can run code inside or outside of zone
• code outside of Angular Zone
• not part of change detection cycle
In the Zone
import { Injectable, NgZone } from '@angular/core';
@Injectable()
export class ZoneWatchService {
constructor(private ngZone: NgZone) {
this.ngZone.onStable.subscribe(this.onZoneStable);
this.ngZone.onUnstable.subscribe(this.onZoneUnstable);
this.ngZone.onError.subscribe(this.onZoneError);
}
onZoneStable() {
console.log('We are stable');
}
onZoneUnstable() {
console.log('We are unstable');
}
onZoneError(error) {
console.error(error.toString());
}
}
In the Zone
• Console output from application
Zones and Change Detection
• All asynchronous code within Angular's zone
– can trigger change detection processing
• Can execute code outside of Angular's Zone
– avoids change detection overhead
– but of course not monitored for change detection effects
• Supported by
– ngZone.runOutsideAngular
ngZone.runOutsideAngular(this.<someMethod>.bind(this));
Executing Outside the Zone
• Service to run behavior inside or outside Zone
executor.service.ts
import { Injectable, NgZone } from '@angular/core';
@Injectable()
export class ExecutorService {
private interval: any;
constructor( private ngZone: NgZone ) {
this.ngZone.onStable.subscribe(
this.onZoneStable.bind( this ) );
this.ngZone.onUnstable.subscribe(
this.onZoneUnstable.bind( this ) );
}
onZoneStable() {
console.log( 'Angular zone is stable' );
}
onZoneUnstable() {
console.log( 'Angular zone is unstable' );
}
Executing Outside the Zone
start( outside: boolean ) {
if ( outside ) {
this.ngZone.runOutsideAngular(
this.createInterval.bind(this) );
} else {
this.createInterval();
}
}
createInterval() {
clearInterval( this.interval );
this.interval =
setInterval(this.logRandom.bind(this), 1000);
}
logRandom() {
console.log('Calc: ' + Math.floor(Math.random() *
2000));
}
}
Executing Outside the Zone
• The AppComponent using the Service
app.component.ts
import { Component } from '@angular/core';
import { ExecutorService } from './executor.service';
@Component({
selector: 'app-root',
template: `
<button (click)="handleClick(false)">Inside Angular Zone
</button>
<button (click)="handleClick(true)">Outside Angular Zone
</button>
`
})
export class AppComponent {
constructor(private service: ExecutorService) { }
handleClick(outside: boolean) {
this.service.start(outside);
}
}
Executing Outside the Zone
• Running the application
Additional Resources
• Angular API Docs: Change Detection Strategy
– https://angular.io/docs/ts/latest/api/core/index/ChangeDetectionStrate
gy-enum.html
• Last Guide For Angular Change Detection You'll Ever
Need
– https://www.mokkapps.de/blog/the-last-guide-for-angular-change-
detection-you-will-ever-need
• Change Detection in Angular 2
– https://vsavkin.com/change-detection-in-angular-2-
4f216b855d4c#.it783qd8q
• Two Phases of Angular 2 Applications
– https://vsavkin.com/two-phases-of-angular-2-applications-
fda2517604be#.4mgodvk0z
• Understanding Zones
– https://blog.thoughtram.io/angular/2016/01/22/understanding-
zones.html
What is Angular Material?
• A third-party package
– used on Angular projects to facilitate the development process
• Provides reutilization of common components e.g.:
– cards
– inputs
– data tables
• For a full reference of components with examples
– https://material.angular.io/
• With Angular
– entire app is a composition of components
• Leveraging with Angular Material
– provides out-of-the-box styled components
– instead of building and styling components from the group up
Angular Material Components
• 6 categories of components:
– form controls
– navigation
– layout
– buttons & Indicators
– popups & Modals
– data table
Form Controls
Navigation
Layout Controls
Buttons and Indicators
Popups and Modals
Data Table
The Component Development Kit (CDK)
• A set of tools
– that implement common interaction patterns
– whilst being unopinionated about their presentation
• Represents an abstraction of the core functionalities
– found in the Angular Material library
– without any styling specific to Material Design
• 3 categories
– common behaviours
– components
– testing
• Documentation is at:
– https://material.angular.io/cdk/categories
Common Behaviors
Components
Testing
Example Application
• A service providing names of Shakespeare plays
• A welcome screen giving basic information
• A component to display the names of the plays
– and the ability to delete plays in the service
• Routes defined for these 2 components
• A dialog box for adding new plays to the service
Setting up the Project
• Firstly, create the project
– and add a number of Node.js modules:
$ ng new angular-material
$ cd angular-material
$ npm install @angular/material @angular/cdk
Angular Material Theme
• After installing Angular Material
– configure a theme that defines what colors will be used
– by the Angular Material components
– add selected theme to src/styles.css
– e.g.:
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
Adding Angular Material Gesture
• Dependencies exist on HammerJS
– to capture touch gestures
– used by sliders and tooltips, for example
• Add the module:
$ npm install hammerjs
• Then add the following at the top of src/main.ts:
import 'hammerjs';
Adding Material Icons
• To add support for the large collection of Material Icons
– update src/index.html as follows:
<!doctype html>
<html lang="en">
<head>
<!-- ... other tags ... -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons”
rel="stylesheet">
</head>
<!-- ... body and app root ... -->
</html>
Importing Material Components
• To import the Material Components
– create a new module
import {NgModule} from '@angular/core';
@NgModule({
imports: [],
exports: []
})
export class MaterialModule {}
Importing Material Components
• Need to import and configure the main module
– update ./src/app/app.module.ts
// ..other modules as already defined
import {BrowserAnimationsModule} from
'@angular/platform-browser/animations';
import {MaterialModule} from './material.module';
@NgModule({
// declarations property
imports: [
BrowserModule,
BrowserAnimationsModule,
MaterialModule,
],
// providers and bootstrap properties
})
export class AppModule { }
Angular Material Sidenav
import {NgModule} from '@angular/core';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
@NgModule({
imports: [
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatListModule,
],
exports: [
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatListModule,
]
})
export class MaterialModule {}
Angular Material and Flexbox
• Using the Flexlayout schema
– makes the layour of the Angular application easier
– uses an Angular directive called fxFlex
• To add support, add the following modules:
$ npm install @angular/flex-layout rxjs
Angular Material and Flexbox
• The module then needs to be imported and configured
– done in the src/app.module.ts file:
// ... other imports
import {FlexLayoutModule} from '@angular/flex-layout';
@NgModule({
// ... declarations property
imports: [
// .. other imports
FlexLayoutModule,
],
// ... providers and bootstrap properties
})
export class AppModule { }
More Angular Material Components
• Adding2 more components to the application:
– a component with a welcome message
– a component for viewing the plays
$ ng g c welcome --module app.module
$ ng g c dashboard --module app.module
– note the use of --module app.module
• Add some content to welcome.component.html
<div style="text-align:center">
<h1>The Play Management System</h1>
<p>
This is a tool to keep track of (Shakespeare) plays
</p>
</div>
Creating Routes
• Adding routes to app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WelcomeComponent } from './welcome/welcome.component';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{path: '', component: WelcomeComponent},
{path: 'dashboard', component: DashboardComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
• Also need to update app.component.html
The Welcome Component
Creating Routes
• Update app.module.ts
– if not already there
/// ... previous imports
import { AppRoutingModule } from './app-routing.module’;
// .. further imports
@NgModule({
/// declarations
imports: [
// .. other imports
AppRoutingModule,
],
// .. providers and bootstrap
})
export class AppModule { }
The Play Service
• Define the export class Play {
src/app/play.ts
structure of a play playid: number;
title: string;
constructor(i: number, t: string) {
this.playid = i;
this.title = t;
}
}
• And create the $ ng g s play/play --module app.module
service
The Play Service
• Implementing the play service
src/app/play/play.service.ts
import { Injectable } from '@angular/core';
import {Observable, of} from 'rxjs';
import { Play } from 'src/play';
@Injectable()
export class PlayService {
PLAYS: Play[] = [
{ playid: 0, title: 'Hamlet' },
{ playid: 1, title: 'The Tempest' },
{ playid: 2, title: 'Othello' },
{ playid: 3, title: 'Macbeth' },
{ playid: 4, title: 'Henry III' }
];
constructor() {}
getPlays(): Observable<Play[]> {
return of<Play[]>(this.PLAYS);
}
...
The Play Service
src/app/play/play.service.ts
...
addPlay(play) {
this.PLAYS.push(play);
}
deletePlay(index) {
this.PLAYS = [...this.PLAYS.slice(0, index),
...this.PLAYS.slice(index + 1, this.PLAYS.length+1)];
// a hack to update the playid field!!!
let x=0;
for (let i in this.PLAYS) {
this.PLAYS[x].playid = x;
x++;
}
}
playLength() {
return this.PLAYS.length;
}
}
Updating the Dashboard
src/app/dashboard/dashboard.component.ts
import {Component} from '@angular/core';
import {PlayService} from '../play/play.service';
import {Play} from 'src/play';
import {DataSource} from '@angular/cdk/table';
import {Observable} from 'rxjs';
import {MatDialog} from '@angular/material/dialog';
import { PlayDialogComponent } from
'../play-dialog/play-dialog.component';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent {
constructor(public dialog: MatDialog, private playService:
PlayService) { }
...
Updating the Dashboard
src/app/dashboard/dashboard.component.ts
...
displayedColumns = ['playid', 'title', 'delete'];
playSource = new PlayDataSource(this.playService);
deletePlay(id) {
this.playService.deletePlay(id);
this.playSource = new PlayDataSource(this.playService);
}
openDialog(): void {
let dialogRef = this.dialog.open(PlayDialogComponent, {
width: '600px',
data: 'Add Play'
});
dialogRef.componentInstance.event.subscribe((result) => {
this.playService.addPlay(result.play);
this.playSource = new PlayDataSource(this.playService);
});
}
}
...
Updating the Dashboard
src/app/dashboard/dashboard.component.ts
...
export class PlayDataSource extends DataSource<any> {
constructor(private playService: PlayService) {
super();
}
connect(): Observable<Play[]> {
return this.playService.getPlays();
disconnect() {
}
}
Rendering the Dashboard
src/app/dashboard/dashboard.component.html
<div class="container">
<div fxLayout="row" fxLayoutAlign="center center"
class="content">
<mat-card class="card" >
<mat-card-title fxLayout.gt-xs="row” fxLayout.xs="column">
<h3>Plays</h3>
</mat-card-title>
<mat-card-content>
<div class="example-container mat-elevation-z8">
<mat-table #table [dataSource]="playSource">
<ng-container matColumnDef="playid">
<mat-header-cell *matHeaderCellDef> Play ID
</mat-header-cell>
<mat-cell *matCellDef="let element">
{{element.playid}}
</mat-cell>
</ng-container>
...
Rendering the Dashboard
src/app/dashboard/dashboard.component.html
...
<mat-header-row *matHeaderRowDef="displayedColumns">
</mat-header-row>
<mat-row *matRowDef="let row; columns:displayedColumns;">
</mat-row>
</mat-table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
The Dashboard.
The Dashboard.
Enabling Data Input
• Add the button to open the dialog
...
<button button="submit" mat-raised-button color="primary"
(click)="openDialog()">
<mat-icon>add</mat-icon> Add Post
</button>
• Angular Material buttons are just native buttons
– with Material styling
Enabling Data Input
• Add the button to open the dialog
• Angular Material buttons are just native buttons
– with Material styling
The Dialog Component
• A new component is created for the dialog:
$ ng g c play-dialog --module app.module
The Dialog Component
• The HTML for the Dialog
<h1 mat-dialog-title>{{data}}</h1>
<div mat-dialog-content>
<form class="example-form" (ngSubmit)="onSubmit()">
<mat-form-field>
<input matInput placeholder="Play Title" type="text" required
[(ngModel)]="playPost.title" name="title">
</mat-form-field>
<button mat-raised-button type="submit" color="primary">
Save
</button>
</form>
</div>
<div mat-dialog-actions>
<button mat-raised-button class="close" (click)="onNoClick()"
color="warn">Cancel</button>
</div>
The Dialog Component
• The component implementation
import {Component, EventEmitter, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {PlayService} from '../play/play.service';
@Component({
selector: 'app-play-dialog',
templateUrl: './play-dialog.component.html',
styleUrls: ['./play-dialog.component.css']
})
export class PlayDialogComponent {
playPost = {
title: '',
playid: 0
};
public event: EventEmitter<any> = new EventEmitter();
...
The Dialog Component
• The component implementation
...
constructor(
public dialogRef: MatDialogRef<PlayDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
public playService: PlayService) { }
onNoClick(): void {
this.dialogRef.close();
}
onSubmit(): void {
this.playPost.playid = this.playService.playLength();
this.event.emit({play: this.playPost});
this.dialogRef.close();
}
}
The Dialog Component
• After opening the dialog
The Dialog Component
• After the new play has been added
Best Practices
• Keep current with the latest Angular library releases
– the Angular libraries are updated regulalry
– and these updates may fix security defects discovered in previous
versions.
– check the Angular change log for security-related updates
– use npm outdated
• Don't modify your copy of Angular
– private, customized versions of Angular fall behind the current version
– may not include important security fixes and enhancements
– share your Angular improvements with the community
– and make a pull request
• Angular APIs marked “Security Risk” should be avoided
– see the API documentation
Preventing Cross-Site scripting (XSS)
• Cross-site scripting (XSS)
– enables attackers to inject malicious code into web pages
– used to steal user data (in particular, login data)
– or perform actions to impersonate the user
– one of the most common attacks on the web
• Examples of common attacks include:
– blog sites where people can enter text
– users put <script> tags into the message they write
– code then run on the browser if not caught
Preventing Cross-Site scripting (XSS)
• To block XSS attacks
– prevent malicious code from entering the DOM
• For example
– if attackers can trick you into inserting a <script> tag in the DOM
– they can run arbitrary code on your website
– not only limited to <script> tags
Angular’s Cross-Site Scripting Security Model
• To systematically block XSS bugs
– Angular treats all values as untrusted by default
• When a value is inserted into the DOM
– from a template, via property, attribute, style, class binding ...
– Angular sanitizes and escapes untrusted values
Angular’s Cross-Site Scripting Security Model
• Angular templates are the same as executable code
• HTML, attributes, and binding expressions
– in templates are trusted to be safe
– but not the values bound
• Never generate template source code
– by concatenating user input and templates
• Use the offline template compiler
– also known as template injection
Sanitization and Security Contexts
• Sanitization is the inspection of an untrusted value
• turning it into a value that's safe to insert into the DOM
• In many cases, sanitization doesn't change a value at all
– depends on context
– a value that's harmless in CSS is potentially dangerous in a URL
Sanitization and Security Contexts
• Angular defines the following security contexts:
• HTML
– used when interpreting a value as HTML
– for example, when binding to innerHtml
• Style
– used when binding CSS into the style property
• URL
– used for URL properties, such as <a href>
• Resource URL
– a URL that will be loaded and executed as code
– for example, in <script src>
Sanitization Example
• Example html template
interpolating into an element's content
<h3>Binding innerHTML</h3>
<p>Bound value:</p>
<p class="e2e-inner-html-interpolated">{{htmlSnippet}}</p>
<p>Result of binding to innerHTML:</p>
<p class="e2e-inner-html-bound" [innerHTML]="htmlSnippet"></p>
binding to innerHTML property of an element
Sanitization Example
• Example component code
export class InnerHtmlBindingComponent {
// For example, a user/attacker-controlled value from a URL.
htmlSnippet = 'Template <script>alert("0wned")
</script> <b>Syntax</b>';
}
Sanitization Example
Direct Use of the DOM
• Built-in browser DOM APIs
– don't automatically protect you from security vulnerabilities
• Many APIs contain unsafe methods, for example
– document
– the node available through ElementRef
• If interacting with other libraries
– any that manipulate the DOM
– don’t have same automatic sanitization as with Angular interpolations
• Avoid directly interacting with the DOM
– instead use Angular templates where possible
Explicit Sanitization Calls
• For cases where this is direct DOM access unavoidable
– use the built-in Angular sanitization functions
• Sanitize untrusted values
– with the DomSanitizer.sanitize method
– and the appropriate SecurityContext
• Also accepts values that were marked as trusted
– by using the bypassSecurityTrust... functions
– will not sanitize them
Content Security Policy
• A defense-in-depth technique to prevent XSS
• To enable:
– requires configuration of the web server
– to return an appropriate Content-Security-Policy HTTP header
Use the Offline Template Compiler
• The offline template compiler
– prevents a whole class of vulnerabilities called template injection
– greatly improves application performance
• Use in production deployments
– don't dynamically generate templates
• Angular trusts template code
– generating templates, in particular templates containing user data
– circumvents Angular's built-in protections
Server-side XSS protection
• HTML constructed on the server
– vulnerable to injection attacks
• Injecting template code into an Angular application
– same as injecting executable code into the application
– gives the attacker full control over the application
• To prevent this
– use a templating language that automatically escapes values
– prevents XSS vulnerabilities on the server
• Don't generate Angular templates on the server side
– by using a templating language
– carries a high risk of introducing template-injection vulnerabilities
Trusting Safe Values
• Sometimes applications genuinely need to
– include executable code
– display an <iframe> from some URL
– construct potentially dangerous URLs
• To prevent automatic sanitization
– tell Angular that you inspected a value
– checked how it was generated
– and made sure it will always be secure
• To mark a value as trusted
– inject DomSanitizer and call one of the following methods:
– bypassSecurityTrustHtml
– bypassSecurityTrustScript
– bypassSecurityTrustStyle
– bypassSecurityTrustUrl
– bypassSecurityTrustResourceUrl
Trusting Safe Values
• For the following template
– that needs to bind a URL to a javascript:alert(...) call:
<h4>An untrusted URL:</h4>
<p><a class="e2e-dangerous-url" [href]="dangerousUrl">Click me</a></p>
<h4>A trusted URL:</h4>
<p><a class="e2e-trusted-url" [href]="trustedUrl">Click me</a></p>
Trusting Safe Values
• Normally, Angular automatically sanitizes the URL
– and disables the dangerous code
• To prevent this
– mark the URL value as a trusted URL
– using the bypassSecurityTrustUrl call:
constructor(private sanitizer: DomSanitizer) {
// javascript: URLs are dangerous if attacker controlled.
// Angular sanitizes them in data binding, but you can
// explicitly tell Angular to trust this value:
this.dangerousUrl = 'javascript:alert("Hi there")';
this.trustedUrl =
sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);
Trusting Safe Values
• The untrusted URL is sanitized, as expected:
Trusting Safe Values
• The trusted URL is permitted:
Trusting Safe Values
• To convert user input into a trusted value
– use a controller method
template
<h4>Resource URL:</h4>
<p>Showing: {{dangerousVideoUrl}}</p>
<p>Trusted:</p>
<iframe class="e2e-iframe-trusted-src" width="640"
height="390" [src]="videoUrl"></iframe>
<p>Untrusted:</p>
<iframe class="e2e-iframe-untrusted-src" width="640"
height="390" [src]="dangerousVideoUrl"></iframe>
updateVideoUrl(id: string) { component
// Appending an ID to a YouTube URL is safe. Always make sure
// to construct SafeValue objects as close as possible to the
// input data so that it's easier to check if the value is safe.
this. dangerousVideoUrl = 'https://www.youtube.com/embed/' + id;
this.videoUrl =
this.sanitizer.bypassSecurityTrustResourceUrl
(this. dangerousVideoUrl);
}
HTTP-Level Vulnerabilities
• Support to help prevent two common HTTP vulnerabilities
– cross-site request forgery (CSRF or XSRF)
– cross-site script inclusion (XSSI)
• Both must be mitigated primarily on the server side
– Angular provides helpers to make integration on the client side easier
Cross-site request forgery
• In a cross-site request forgery (CSRF or XSRF)
– attacker tricks the user into visiting a different web page
– with malignant code that secretly sends a malicious request
– to the application's web server
• To prevent this
– application must ensure user requests originate from real application
– not from a different site
– server and client must cooperate to thwart this attack
• Common anti-XSRF technique
– application server sends a randomly generated authentication toke
– in a cookie
• Angular's HttpClient has built-in support
– for the client-side half of this technique
Cross-site script inclusion (XSSI)
• Also known as JSON vulnerability
– can allow an attacker's website to read data from a JSON API
• The attack works on older browsers
– overrides native JavaScript object constructors
– then includes an API URL using a <script> tag
• This attack is only successful
– if the returned JSON is executable as JavaScript
• Servers can prevent an attack
– by prefixing all JSON responses to make them non-executable
– using the well-known string ")]}',\n".
• Angular's HttpClient library recognizes this convention
– automatically strips the string ")]}',\n" from all responses
– before further parsing
Summary
• Angular app must follow the same security principles
– as regular web applications
– and must be audited as such
• APIs that should be audited in a security review
– such as the bypassSecurityTrust methods
– are marked in the documentation as security sensitive
• Plenty of external documentation and resources:
– use OWASP
Protractor Introduction
• Protractor is an end-to-end test framework
– for Angular and AngularJS applications
– runs tests against your application running in a real browser
– interacting with it as a user would
• Built on top of Selenium WebDriverJS
– uses native events and browser-specific drivers
– to interact with your application as a user would
• Supports Angular-specific locator strategies
– allows testing of Angular-specific elements
– without any setup effort
Protractor vs. Jasmine
• Angular-CLI ships with two frameworks for testing:
– unit tests using Jasmine and Karma
– used to test the logic of the components and services
– end-to-end tests using Protractor
– used to ensure that the high-level functionality
• Can use Jasmine to write not just unit tests
– but basic UI tests also
• However, to test the front-end functionality
– from start to end
– Protractor is the way to go
Prerequisites for Using Protractor
• Need to have Node.js installed
– check the version of Node.js by running node --version
– check the compatibility notes in the Protractor README
– ensure version of Node.js is compatible with Protractor
• By default
– Protractor uses the Jasmine test framework for its testing interface
• Local standalone Selenium Server
– used to control browsers
– requires the Java Development Kit (JDK)
Setup
• Use npm to install Protractor globally:
npm install -g protractor
– will install two command line tools
– protractor and webdriver-manager
– run protractor --version to make sure it's working
• webdriver-manager is a helper tool
– to easily get an instance of a Selenium Server running
– use it to download the necessary binaries with:
webdriver-manager update
Setup
• Start up a server with:
webdriver-manager start
• This will start up a Selenium Server
– and will output a collection of info logs.
• Protractor tests will send requests to this server
– to control a local browser
– status of the server at http://localhost:4444/wd/hub
Configuring Protractor
• The default project template
– depends on two files to run the tests:
– the spec files that reside inside the e2e directory
– the configuration file (protractor.conf.js).
• We start with the configuration
Configuring Protractor
e2e/protractor.conf.js
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
...
Configuring Protractor
e2e/protractor.conf.js
...
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter
({ spec: { displayStacktrace: true } }));
}
};
A First Test
• Using a brand-new app
– created using ng new demo
– initial e2e test spec exists to test for welcome message
• 2 files used to define the tests
– app.e2e-spec.ts
– a typescript file specifying the tests
– app.po.ts
– a typescript file modelling a page, or part of an application
– e.g. text, headings, tables, buttons and links
– objects are imported into the spec file and their methods are invoked
A First Test
app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {page = new AppPage();});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('demo app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs()
.get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
A First Test
app.po.ts
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span’))
.getText() as Promise<string>;
}}
A First Test
$ ng e2e
...
** Angular Live Development Server is listening on localhost:4200,
open your browser on http://localhost:4200/ **
: Compiled successfully.
[00:53:34] I/launcher - Running 1 instances of WebDriver
[00:53:34] I/direct - Using ChromeDriver directly...
Jasmine started
[00:53:38] W/element - more than one element found for locator By
(css selector, app-root .content span) - the first result will be
used
workspace-project App
✓ should display welcome message
Executed 1 of 1 spec SUCCESS in 0.668 sec.
[00:53:39] I/launcher - 0 instance(s) of WebDriver still running
[00:53:39] I/launcher - chrome #01 passed
A Second Set of Tests
• Added 2 new components:
– greeting (ng g c greeting)
– user (ng g c user)
• Updated app-component.html to:
<app-greeting></app-greeting>
<router-outlet></router-outlet>
• Configured routing:
...
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'app-greeting' },
{ path: 'user', component: UserComponent },
];
...
A Second Set of Tests
• App with empty path:
A Second Set of Tests
• App with user/ in the path:
A Second Set of Tests
app.po.ts
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
navigateToUser(): Promise<unknown> {
return browser.get('user/') as Promise<unknown>;
}
getGreetingText(): Promise<string> {
return element(by.css('app-root app-greeting p’))
.getText() as Promise<string>;
}
getRouterOutletUserText(): Promise<string> {
return element(by.css('app-root app-user p’))
.getText() as Promise<string>;
}
}
A Second Set of Tests
app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
const page: AppPage = new AppPage();;
beforeEach(() => {
browser.get('/');
});
it('greeting component should display message', () => {
page.navigateTo();
expect(page.getGreetingText()).toEqual('greeting works!');
});
it('user component should display message', () => {
page.navigateToUser();
expect(page.getRouterOutletUserText()).toEqual('user works!');
});
afterEach(async () => {
...
A Second Set of Tests
$ ng e2e
...
** Angular Live Development Server is listening on localhost:4200,
open your browser on http://localhost:4200/ **
: Compiled successfully.
[03:00:47] I/launcher - Running 1 instances of WebDriver
[03:00:47] I/direct - Using ChromeDriver directly...
Jasmine started
workspace-project App
✓ greeting component should display message
✓ user component should display message
Executed 2 of 2 specs SUCCESS in 2 secs.
[03:00:56] I/launcher - 0 instance(s) of WebDriver still running
[03:00:56] I/launcher - chrome #01 passed
Using Locators
• The heart of end-to-end tests for webpages is:
– finding DOM elements
– interacting with them
– getting information about the current state of your application
• Protractor exports a global function element
– takes a Locator and will return an ElementFinder
– finds a single element
– if you need to manipulate multiple elements
– use the element.all function.
• The ElementFinder has a set of action methods
– such as click(), getText(), and sendKeys
– the core way to interact with an element
– and get information back from it
Locators
• Common locators:
// Find an element using a css selector
by.css('.myclass')
// Find an element with the given id
by.id('myid')
// Find an element using an input name selector
by.name('field_name')
• The locators are passed to the element function:
element(by.css('some-css'));
element(by.model('item.name'));
element(by.binding('item.name'));
Actions
• The element() function returns an ElementFinder
var el = element(locator);
// Click on the element.
el.click();
// Send keys to the element (usually an input).
el.sendKeys('my text');
// Clear the text in an element (usually an input).
el.clear();
// Get the value of an attribute, for example,
// get the value of an input
el.getAttribute('value');
Finding Multiple Elements
• To deal with multiple DOM elements
– use the element.all function
element.all(by.css('.selector')).then(function(elements) {
// elements is an array of ElementFinders.
});
• element.all() has several helper functions:
// Number of elements.
element.all(locator).count();
// Get by index (starting at 0).
element.all(locator).get(index);
// First and last.
element.all(locator).first();
element.all(locator).last();
Finding Sub-Elements
• Using a single locator to find:
– an element:
element(by.css('some-css'));
– a list of elements:
element.all(by.css('some-css'));
• Using chained locators to find:
– a sub-element:
element(by.css('some-css')).element(by.tagName('tag-within-css'));
– to find a list of sub-elements:
element(by.css('some-css')).all(by.tagName('tag-within-css'));
Organizing Tests and Refactoring
• Separate E2E tests from unit tests
• Group your E2E tests sensibly
– organize your tests in a way that matches the structure of your project
• If there are multiple pages
– page objects should have a separate directory of their own
• If the page objects have some methods in common
– such as navigateToHome()
– create a base page object
– other page models can inherit from the base page model
• Make your tests independent from each other
– don't want all your tests to fail because of a minor change in the UI
• Keep page object defs free of assertions/expectations
– assertions should be made inside the spec file
Overview of Angular Libraries
• An Angular library
– is an Angular project
– differs from an app in that it cannot run on its own
– must be imported and used in an app
• Libraries extend Angular's base functionality
• For example, to add reactive forms to an app
– add the library package using ng add @angular/forms
– import the ReactiveFormsModule from the @angular/forms library
• Any app developer
– can use these libraries
– other libraries that have been published as npm packages
– by the Angular team or by third parties
Creating Libraries
• For developed functionality that is suitable for reuse
– can create your own libraries
– can be used locally in your workspace
– can publish them as npm packages to share with other projects
• They can be published to:
– the npm registry
– a private npm Enterprise registry
– private package management system that supports npm packages
• Packaging functionality as a library
– forces artifacts to be decoupled from the application's business logic
– can help to avoid various bad practices or architecture mistakes
• Putting code into a separate library
– more complex than simply putting everything in one app
• Requires investment in time and thought
– for managing, maintaining, and updating the library
Creating Libraries
• You can create and publish new libraries
– to extend Angular functionality
– if you find that you need to solve the same problem in more than one
app
– or want to share your solution with other developers), you have a
candidate for a library.
• A simple example might be a button that sends users to
your company website, that would be included in all apps
that your company builds.
The Library Structure
• To create a monorepo workspace
– i.e. one workspace for many apps / libraries
$ ng new dev-workspace --create-application=false
$ cd my-workspace
• Then create libraries
$ ng generate library my-lib
• Or applications
$ ng generate application new-app
The Workspace Structure
• The workspace is built using the following schema:
dev-workspace/
... (workspace-wide config files)
projects/ (generated applications and libraries)
new-app/ --(an explicitly generated application)
... --(application-specific config)
e2e/ ----(corresponding e2e tests)
src/ ----(e2e tests source)
... ----(e2e-specific config)
src/ --(source and support files for application)
my-lib/ --(a generated library)
... --(library-specific config)
src/ --source and support files for library)
The Workspace angular.json File
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"my-lib": {
"projectType": "library",
"root": "projects/my-lib",
"sourceRoot": "projects/my-lib/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/my-lib/tsconfig.lib.json",
"project": "projects/my-lib/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/my-lib/tsconfig.lib.prod.json"
}
...
The Library Structure
• Once created, the library has the following structure:
projects
└── my-lib
├── README.md
├── karma.conf.js
├── ng-package.json
├── package.json
├── src
│ ├── lib
│ │ ├── my-lib.component.spec.ts
│ │ ├── my-lib.component.ts
│ │ ├── my-lib.module.ts
│ │ ├── my-lib.service.spec.ts
│ │ └── my-lib.service.ts
│ ├── public-api.ts
│ └── test.ts
├── tsconfig.lib.json
├── tsconfig.lib.prod.json
├── tsconfig.spec.json
└── tslint.json
Refactoring Parts of an App Into a Library
• To make a solution reusable
– needs to be adjusted
– so that it does not depend on app-specific code
• Declarations such as components and pipes
– should be designed as stateless
– meaning they don’t rely on or alter external variables
• Any observables components subscribe to internally
– should be cleaned up and disposed of during their lifecycle
• Components should expose their interactions
– through inputs for providing context
– outputs for communicating events to other components.
• Services should declare their own providers
– rather than declaring providers in the NgModule or a component
– allows the compiler to leave the service out of the bundle
– if it never gets injected into the application that imports the library
Refactoring Parts of an App Into a Library
• If you register global service providers
– or share providers across multiple NgModules
– use the forRoot() and forChild() patterns provided by RouterModule
• Check all internal dependencies
• For custom classes or interfaces
– used in either components or service
– check whether they depend on additional classes or interfaces
– these may also need to be migrated
• If library code depends on a service
– that service needs to be migrated.
• If library code or its templates depend on other libraries
– such a Angular Material, for instance
– you must configure your library with those dependencies.
Reusable Code and Schematics
• A library is packaged into an npm package
– for publishing and sharing
• This package can also include schematics
– instructions for generating / transforming code directly in your project
– the same way the CLI creates a generic skeleton app
– with ng generate component
• A schematic that is combined with a library can
– provide the Angular CLI with the information it needs
– to generate a component defined in that library
Integrating with the CLI
• A library can include schematics
– that allow it to integrate with the Angular CLI
• Include an installation schematic
– so that ng add can add your library to a project
• Include generation schematics in your library
– so that ng generate can scaffold your defined artifacts in a project
– components, services, tests, and so on
• Include an update schematic
– so that ng update can update your library’s dependencies
– and provide migrations for breaking changes in new releases
Building the Library
$ ng build my-lib
Building Angular Package
********************************************************************
It is not recommended to publish Ivy libraries to NPM repositories.
Read more here: https://v9.angular.io/guide/
ivy#maintaining-library-compatibility
********************************************************************
--------------------------------------------------------------------
Building entry point 'my-lib'
--------------------------------------------------------------------
Compiling TypeScript sources through ngc
Compiling @angular/core : es2015 as esm2015
Bundling to FESM2015
Bundling to FESM5
Bundling to UMD
Minifying UMD bundle
Writing package metadata
Built my-lib
--------------------------------------------------------------------
Built Angular Package
- from: /Users/ewan/dev/dev-workspace/projects/my-lib
- to: /Users/ewan/dev/dev-workspace/dist/my-lib
--------------------------------------------------------------------
Building the Library
$ ng test my-lib
Compiling @angular/animations : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/animations/browser : es2015 as esm2015
Compiling @angular/compiler/testing : es2015 as esm2015
Compiling @angular/core/testing : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/animations/browser/testing : es2015 as esm2015
Compiling @angular/forms : es2015 as esm2015
...
Compiling @angular/router/testing : es2015 as esm2015
10% building 2/2 modules 0 active06 05 2020 04:57:37.950:WARN [karma]:
No captured browser, open http://localhost:9876/
06 05 2020 04:57:37.952:INFO [karma-server]:
Karma v5.0.4 server started at http://0.0.0.0:9876/
...
TOTAL: 2 SUCCESS
TOTAL: 2 SUCCESS
$ ng lint my-lib
Linting "my-lib"...
All files pass linting.
Publishing a Library
• Use the Angular CLI and the npm package manager
– to build and publish your library as an npm package
– not recommended to publish Ivy libraries to NPM repositories
• Before publishing a library to NPM
– build it using the --prod flag
– will use the older compiler and runtime instead of Ivy.
– known as View Engine
$ ng build my-lib --prod
$ cd dist/my-lib
$ npm publish
Using Your Own Library in Apps
• Don't have to publish libraries to npm package manager
– If they are to be used it in your own apps
– but it does have to be built it first
$ ng build my-lib
• In your apps, import from the library by name:
import { myExport } from 'my-lib';
Angular Service Worker Introduction
• A service worker is a script that runs in the web browse
– manages caching for an application
• Functions as a network proxy
– intercept all outgoing HTTP requests made by the application
– can choose how to respond to them
– completely programmable and doesn't rely on server-specified
caching headers
• Unlike the other scripts that make up an application
– service worker is preserved after the user closes the tab
– next time application is loaded, the service worker loads first
• Even across a fast, reliable network
– round-trip delays can introduce significant latency
– when loading the application
– service workers can significantly improve the user experience
Service Workers in Angular
• Caching an application is like installing a native application
– application is cached as one unit, and all files update together
• Running application continues to run
– with the same version of all files
– does not suddenly start receiving cached files from a newer version
• When users refresh the application
– they see the latest fully cached version
– new tabs load the latest cached code.
• Updates happen in the background
– relatively quickly after changes are published
– previous version of the app served until an update is installed and
ready
• The service worker conserves bandwidth when possible
– resources are only downloaded if they've changed.
Adding a Service Worker to Project
• To set up the Angular service worker in a project
– use the CLI command
$ ng add @angular/pwa --project *project-name*
• The command completes the following actions:
– adds the @angular/service-worker package to your project.
– enables service worker build support in the CLI
– imports and registers the service worker in the app module.
– updates the index.html file
– installs icon files to support the installed Progressive Web App (PWA)
– creates service worker configuration file called ngsw-config.json
Adding a Service Worker to Project - Example
• To set up the Angular service worker in a project
– use the CLI command
$ ng add @angular/pwa --project angular-material
Installing packages for tooling via npm.
Installed packages for tooling via npm.
CREATE ngsw-config.json (620 bytes)
CREATE src/manifest.webmanifest (1356 bytes)
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
CREATE src/assets/icons/icon-72x72.png (792 bytes)
CREATE src/assets/icons/icon-96x96.png (958 bytes)
UPDATE angular.json (3819 bytes)
UPDATE package.json (1470 bytes)
UPDATE src/app/app.module.ts (1317 bytes)
UPDATE src/index.html (573 bytes)
✔ Packages installed successfully.
Adding a Service Worker to Project - Example
• The command completes the following actions:
– adds the @angular/service-worker package to your project.
$ ls node_modules/@angular/service-worker
README.md esm2015 ngsw-worker.js
bundles esm5 package.json
config fesm2015 safety-worker.js
config.d.ts fesm5 service-worker.d.ts
config.metadata.json ngsw-config.js service-worker.metadata.json
Adding a Service Worker to Project - Example
• The command completes the following actions:
– enables service worker build support in the CLI
angular.json
... other imports
"projects": {
"angular-material": {
"projectType": "application",
...
"configurations": {
"production": {
...
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
...
Adding a Service Worker to Project - Example
• The command completes the following actions:
– imports and registers the service worker in the app module
src/app/app.module.ts
... other imports
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
@NgModule({
// declarations:
imports: [
... other imports
ServiceWorkerModule.register('ngsw-worker.js’,
{ enabled: environment.production }),
],
// providers and bootstrap: [AppComponent],
entryComponents: [
PlayDialogComponent
],
})
export class AppModule { }
Adding a Service Worker to Project - Example
• The command completes the following actions:
– updates the index.html file
src/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular Material</title>
<base href="/">
...
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
</head>
<body>
<app-root></app-root>
<noscript>
Please enable JavaScript to continue using this application.
</noscript>
</body>
</html>
Adding a Service Worker to Project - Example
• The command completes the following actions:
– installs icon files to support the installed Progressive Web App (PWA)
– creates service worker configuration file called ngsw-config.json
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
CREATE src/assets/icons/icon-72x72.png (792 bytes)
CREATE src/assets/icons/icon-96x96.png (958 bytes)
Adding a Service Worker to Project - Example
• The command completes the following actions:
– creates service worker configuration file called ngsw-config.json
ngsw-config.json
{
"$schema":
"./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
...
Build the Project
• Finally, the project needs to be built
– the service worker cannot be used via ng serve
$ ng build --prod
Serving With http-server
• To serve the directory containing the web files
– with http-server
$ http-server -p 8080 -c-1 dist/angular-material
Simulating a Network Issue
• To simulate a network issue
– disable network interaction for your application
• In Chrome:
– select More Tools > Developer Tools
– (from the Chrome menu located at the top right corner
– go to the Network tab
– change the Online option to Offline
Simulating a Network Issue
• Now the app has no access to network interaction
– for applications that do not use the Angular service worker
– refreshing now would display Chrome's Internet disconnected page
– saying "There is no Internet connection".
• With the addition of an Angular service worker
– the application behavior changes
– on a refresh, the page loads normally
What is being Cached?
• All files the browser needs to render this app are cached
– the ngsw-config.json boilerplate configuration is set up
– to cache the specific resources used by the CLI
– namely:
• index.html
• favicon.ico
• Build artifacts (JS and CSS bundles)
• Anything under assets
• Images and fonts directly under the configured outputPath
Making Changes to the Application
• Open a new tab in the incognito browser
– this will keep the cache state alive
• Shut down http-server
• Make a change to the application
– watch the service worker install the update
• Edit:
– src/app/welcome/welcome.app.component.html
– change the text The Play Management System
– to The Play Management Platform
• Build and run the server again:
$ ng build --prod
$ http-server -p 8080 -c-1 dist/angular-material
Updating the Application in the Browser
• See how the browser and service worker
– handle the updated application
• Open http://localhost:8080 again in the same window
Updating the Application in the Browser
• Open http://localhost:8080 again in another window
Further Reading
• Many good articles that go into greater depth regarding:
• A Service Worker deep introduction
– https://developers.google.com/web/fundamentals/primers/service-
workers
• The App Shell
– https://angular.io/guide/app-shell
• Service Worker communication
– https://angular.io/guide/service-worker-communications
• Using Service Workers in production
– https://angular.io/guide/service-worker-communications
• Service Worker configuration
– https://angular.io/guide/service-worker-config