Further Angular
Further Angular
Angular
Mallon Associates
June 2020
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
filter()
Subscriber
What is Rxjs?
• 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
• 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';
}
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
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
• pipe() function:
– takes the functions to combine as its arguments
– returns a new function which runs the composed functions
– functions are run in sequence
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
...
Pipes
// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'rxjs-example';
}
• For example:
– custom events sending output data from child to a parent component
– Router and Forms modules listen for & respond to user-input events
• Supporting
– simple validation
– submission of data to URLs
• Reactive forms
– form model created programmatically
Reactive Forms
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
ReactiveFormsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Form Model
• 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('')
login() {
console.log(this.loginForm.value);
console.log(`Username: ${this.username}`);
console.log(`Password: ${this.password}`)
}
}
Reactive Form Example
• Validators class
– provides validation function provided as static methods
– including required, minLength, maxLength and pattern
– can be imported 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)])
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
@Component( {
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
username = new FormControl('usrname', Validators.required)
password = new FormControl( '',
[Validators.required, hasExclamation])
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
• 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
HTML Template
Using Pipes
Data Source
e.g. component
data or method
Pipe
Pipe
HTML Template
Predefined Pipes
• 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
@Component({
selector: 'app-root',
template: `<p>Total price: {{ price | currency }}</p>`
})
export class AppComponent {
price = 100.12348;
}
@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
@Component({
selector: 'app-root',
template: `
<p>Total price: {{ price | currency | lowercase }}</p>
`
})
export class AppComponent {
price = 100.12348;
}
Chaining Pipes
interface PipeTransform {
transform(value: any, ...args: any[]) : any
}
Custom Pipes
@Pipe( {
name: 'formatFileSize'
})
export class FormatFileSizePipe implements PipeTransform {
// keep up to 2 decimals
const formattedSize = Math.round( size * 100 ) / 100;
}
Using Custom Pipes
@NgModule({
declarations: [
AppComponent,
FormatFileSizePipe
],
imports: [ BrowserModule ],
bootstrap: [AppComponent]
})
export class AppModule { }
Using Custom Pipe
@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
• 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
@Pipe( {
name: 'logNumber',
pure: false
})
export class LogNumberPipe implements PipeTransform {
private lastNumber: number = 5;
private temp: number = this.lastNumber;
@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
• 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
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
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
• Can be based on
– predefined directive selection or css classes
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
App
Component
HelloList
Component
content
@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
@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
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
@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' );
}
ngAfterViewInit() {
console.log('afterViewInit');
}
ngOnDestroy() {
console.log('onDestroy');
}
}
Component Lifecycle Example
@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
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
• Built in directive
– used to modify the style attribute of an element
@Component({
selector: 'my-comp',
template: `
<template [myStructuralDirective]="somedata">
<p>
Under a structural directive.
</p>
</template>`
})
Structural Directives
@Component({
selector: 'app-directive-example',
template: `
<p *structuralDirective="somedata">
Under a structural directive.
</p>
`
})
• 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
@NgModule( {
declarations: [
AppComponent,
ChangeColorDirective
],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
Defining a Custom Directive
@Component({
selector: 'app-root',
template: `<p changecolor>{{message}}</p>`
})
export class AppComponent {
message = 'Hello!';
}
Defining a Custom Directive
<p [changecolor]="blue">{{message}}</p>
Passing information
@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
• An EventEmitter
– is an instance variable decorated with @Output
– an instance of EventEmitter<type>()
– defined in @angular/core
• 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
<body>
<div>
<p>
<ul>
<li>...</li>
• 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
App
Component
Component Component
One Two
child routes
@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
@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
@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
@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;
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.
• 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
• Application structure
App Module
providers
App
Component
comp-one comp-two
ActivateGuard
Component Component
One Two
DeactivateGuard
passed as parameter
Implementing Access Control Guards
@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
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
@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
canDeactivate() {
return this.checked;
}
checkboxChanged(checked) {
this.checked = checked;
}
}
Implementing Access Control Guards
• Deactivation blocked
Secondary Routes
• 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
• 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
• 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
Two Phases
Change
Event Phase Detection
Phases
App
Component
ToDo ToDo
Component Component
Change Detection
ToDo ToDo
Component Component
Change Detection
ToDo ToDo
Component Component
Change Detection
App
Component
1 3
2
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
todos:List<ToDo>
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
ToDo ToDo
Component Component {{todo.text}} {{todo.text}}
todo.done todo.done
ToDo_ ToDo_
ChangeDetector ChangeDetector
Change Detection and Two-Way Bindings
Event Binding
View Template Component
Property Binding
• Default
– change detector's mode will be set to CheckAlways
• OnPush
– change detector's mode will be set to CheckOnce
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
OnPush
Admin_Change ToDoList_ChangeDetector
Admin ToDoList
Detector
Component Component
• NgZone service
– makes several observables available
– useful for determining state of Angular's Zone
In the 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
@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
• Supported by
– ngZone.runOutsideAngular
ngZone.runOutsideAngular(this.<someMethod>.bind(this));
Executing Outside the Zone
@Injectable()
export class ExecutorService {
private interval: any;
onZoneUnstable() {
console.log( 'Angular zone is unstable' );
}
Executing Outside the Zone
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
@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
• A third-party package
– used on Angular projects to facilitate the development process
• Provides reutilization of common components e.g.:
– cards
– inputs
– data tables
• 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
– 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
• 3 categories
– common behaviours
– components
– testing
• Documentation is at:
– https://material.angular.io/cdk/categories
Common Behaviors
Components
Testing
Example Application
$ ng new angular-material
$ cd angular-material
import 'hammerjs';
Adding Material Icons
<!doctype html>
<html lang="en">
<head>
<!-- ... other tags ... -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons”
rel="stylesheet">
</head>
</html>
Importing Material Components
@NgModule({
imports: [],
exports: []
})
@NgModule({
// declarations property
imports: [
BrowserModule,
BrowserAnimationsModule,
MaterialModule,
],
@NgModule({
imports: [
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatListModule,
],
exports: [
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatListModule,
]
})
export class MaterialModule {}
Angular Material and Flexbox
@NgModule({
// ... declarations property
imports: [
// .. other imports
FlexLayoutModule,
],
<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
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
• Update app.module.ts
– if not already there
// .. further imports
@NgModule({
/// declarations
imports: [
// .. other imports
AppRoutingModule,
],
@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']
})
...
Updating the Dashboard
src/app/dashboard/dashboard.component.ts
...
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
...
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-table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
The Dashboard.
The Dashboard.
Enabling Data Input
<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
@Component({
selector: 'app-play-dialog',
templateUrl: './play-dialog.component.html',
styleUrls: ['./play-dialog.component.css']
})
export class PlayDialogComponent {
playPost = {
title: '',
playid: 0
};
...
The Dialog Component
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
• 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
• 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
<h3>Binding innerHTML</h3>
<p>Bound value:</p>
<p class="e2e-inner-html-interpolated">{{htmlSnippet}}</p>
• To enable:
– requires configuration of the web server
– to return an appropriate Content-Security-Policy HTTP header
Use the Offline Template Compiler
• To prevent this
– use a templating language that automatically escapes values
– prevents XSS vulnerabilities on the server
• To prevent this
– mark the URL value as a trusted URL
– using the bypassSecurityTrustUrl call:
• 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
• By default
– Protractor uses the Jasmine test framework for its testing interface
webdriver-manager update
Setup
/**
* @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
– 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';
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';
$ ng e2e
...
workspace-project App
✓ should display welcome message
• Configured routing:
...
...
A Second Set of Tests
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';
beforeEach(() => {
browser.get('/');
});
afterEach(async () => {
...
A Second Set of Tests
$ ng e2e
...
workspace-project App
✓ greeting component should display message
✓ user component should display message
• Common locators:
// Find an element using a css selector
by.css('.myclass')
var el = element(locator);
element.all(by.css('.selector')).then(function(elements) {
// elements is an array of ElementFinders.
});
element(by.css('some-css'));
– a list of elements:
element.all(by.css('some-css'));
element(by.css('some-css')).element(by.tagName('tag-within-css'));
element(by.css('some-css')).all(by.tagName('tag-within-css'));
Organizing Tests and Refactoring
• 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
$ cd my-workspace
• Or applications
$ ng generate application new-app
The Workspace Structure
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
$ 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
$ cd dist/my-lib
$ npm publish
Using Your Own Library in Apps
$ ng build my-lib
$ 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
@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
...
Build the Project
$ ng build --prod
Serving With http-server
• 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