Upgrade to Pro — share decks privately, control downloads, hide ads and more …

angular2-slides-v4.pdf

 angular2-slides-v4.pdf

Jussi Pohjolainen

November 25, 2020
Tweet

More Decks by Jussi Pohjolainen

Other Decks in Technology

Transcript

  1. React vs Angular React • Pure React is just for

    UI • But you can add third-party libraries • JavaScript preference • JSX for templates • One-way databinding Angular 2 -> • In addition for UI you will have lot of other libraries for AJAX, services, modules ... • TypeScript preference • Strong opinions how app should be structured • HTML for templates with add-ons from Angular • Two-way databinding • Learning curve can be steep
  2. AngularJS vs Angular 2 -> • No scope, uses hierarchical

    components: • Simpler expression syntax, [ ] for property binding, ( ) for event binding • Modularity – core functionality in modules, lighter and faster • Modern Browser only – reduce need for browser compability workarounds • TypeScript Recommendation
  3. TypeScript • TypeScript is a superset of JavaScript and ES6

    • Created by Microsoft • Enhances the language with type annotations • Strong recommendation for Angular • Browsers do not understand TypeScript, but it is possible to compile TypeScript to JavaScript • So in Angular2 you will end up with files ending with .ts rather than .js. In the end the .ts files are compiled to .js • You can implement also React, Express, Vue.js in TypeScript
  4. Install • To install use npm • npm install -g

    typescript • tsc -version • Then code with *.ts files • And compile • tsc *.ts • Which will in the end compile to *.js
  5. Example of types function repeat(mystring : string, amount : number)

    : string { let returnValue : string = "" for(let i=0; i<amount; i++) { returnValue = returnValue + mystring } return returnValue } Cannot pass not string Return value is string
  6. Running in Browser <!DOCTYPE html> <html> <head> <title>Title</title> <meta charset="UTF-8"

    /> <style media="screen"></style> <script src="dist/bundle.js"></script> </head> <body> <p id="changethis">Loading...</p> </body> </html>
  7. Watch - mode By running in watch mode (-w) it

    detects changes in ts file and compiles it
  8. Types • Now when trying to invoke • p.innerHTML =

    repeat(5, 5) • Where the function is declared • function repeat(mystring : string, amount : number) : string • You will get an error: • Argument of type '5' is not assignable to parameter of type 'string'
  9. TypeScript types • boolean • number • string • array

    • tuple • enum • any • void • null • undefined • never
  10. Tuple: Define the types in array let tuple: [string, number,

    boolean] = ['h', 5, true] tuple[0] = 'hello' tuple[1] = 5 tuple[2] = true
  11. Enum enum Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}

    let today : Day = Day.Monday console.log(today) // 0
  12. Any: type can change let variable : any variable =

    1 variable = "hello" let array : any[] = [1, 'hello', true]
  13. void function doIt() : void { console.log('I am not returning

    anything') let variable : void variable = null variable = undefined } Not really useful, only null or undefined values are accepted
  14. let vs var vs const • var • scope either

    global or function • let • block scoping • const • block scoping, cannot change
  15. Function type function sum(a: number, b: number) : number {

    return a + b } let f : ((a : number, b : number) => number) = sum
  16. Function type function sum(a: number, b: number, f: ((result :

    number) => void)) : void { f(a + b) } sum(5,5, result => console.log(result))
  17. JavaScript function buildName(firstName, lastName) { if (lastName) return firstName +

    " " + lastName; else return firstName; } let result1 = buildName("Bob"); let result2 = buildName("Bob", "Adams", "Sr."); let result3 = buildName("Bob", "Adams") console.log(result1) console.log(result2) console.log(result3) Two arguments You can pass several...
  18. TypeScript function buildName(firstName : string, lastName : string) : string

    { if (lastName) return firstName + " " + lastName; else return firstName; } let result1 = buildName("Bob"); let result2 = buildName("Bob", "Adams", "Sr."); let result3 = buildName("Bob", "Adams") console.log(result1) console.log(result2) console.log(result3) We will now have problems...
  19. TypeScript function buildName(firstName : string, lastName? : string) : string

    { if (lastName) return firstName + " " + lastName; else return firstName; } let result1 = buildName("Bob"); // let result2 = buildName("Bob", "Adams", "Sr."); let result3 = buildName("Bob", "Adams") console.log(result1) // console.log(result2) console.log(result3) You can now use 1 – 2 arguments! Will be undefined..
  20. Rest arguments function buildName(firstName: string, ...restOfName: string[]) { return firstName

    + " " + restOfName.join(" "); } // employeeName will be "Joseph Samuel Lucas MacKinzie" let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
  21. Example interface Person { name: string; hello: () => void;

    } let jack: Person = { name: "jack", hello: function (): void { console.log(this.name); }, }; jack.hello(); let f = jack.hello; f(); We will get undefined
  22. Example interface Person { name: string; hello: (this: Person) =>

    void; } let jack: Person = { name: "jack", hello: function (this: Person): void { console.log(this.name); }, }; jack.hello(); let f = jack.hello; f(); this must be typeof Person, otherwise fail Won't compile..
  23. readonly function display(person : Person) : string { person.firstname =

    "Tina" return `firstname = ${person.firstname}, lastname = ${person.lastname}` } interface Person { readonly firstname : string readonly lastname : string } Cannot change!
  24. Function in interface interface MyFunc { (msg: string) : void

    } function display(f : MyFunc) : void { f('hello') } function printToConsole(msg : string) : void { console.log(msg) } display(printToConsole)
  25. Class Types interface Flyable { fly() : void } class

    Bird implements Flyable { fly() { console.log('Bird is flying') } } class Airplane implements Flyable { fly() { console.log('Airplane is flying') } } function flySomething(something : Flyable) : void { something.fly() } flySomething(new Airplane()) flySomething(new Bird()) Must have the fly() method We can pass bird and airplane
  26. Class and Inheritance class Person { private name : string

    private age : number constructor(name : string, age : number) { if(name.length >= 2 && age >= 0 && age <= 150) { this.name = name this.age = age } } public sleep() { console.log(`${this.name} sleeps`) } } class Programmer extends Person { private salary : number constructor(name : string, age : number, salary : number) { super(name, age) if(salary >= 0) { this.salary = salary } } public codeApps() { console.log('code apps') } }
  27. Setters and Getters (ES5 Target required) class Person { private

    _name : string = "" get name(): string { return this._name; } set name(newName : string) { if(newName.length >= 2) { this._name = newName } else { throw new TypeError('Name must be at least 2 chars') } } } // tsc -target ES5 -w src/index.ts --outFile dist/bundle.js let tina = new Person() tina.name = "T" console.log(tina.name) Invoking the set method
  28. Static class Circle { public static PI : number =

    3.14 private _radius : number set radius(newRadius : number) { if(newRadius > 0) { this._radius = newRadius } else { throw new TypeError('radius must be > 0') } } get radius() : number { return this._radius } calculateArea() : number { return Circle.PI * c.radius * c.radius } } let c = new Circle() c.radius = 5 console.log(c.calculateArea()) console.log("area = " + Circle.PI * c.radius * c.radius) Common variable to all objects. Class variable
  29. Abstract Class abstract class Shape { protected _xPos : number

    protected _yPos : number abstract calculateArea() : number } class Circle extends Shape { private radius : number calculateArea() : number { return Math.PI * this.radius * this.radius } } class Rectangle extends Shape { private width : number private heigth : number calculateArea() : number { return this.width * this.heigth } }
  30. Generics class List<T> { private list: T[] = [] public

    add (element: T) { this.list.push(element) } public removeEveryOther() { let newList : T[] = [] for(let i=0; i<this.list.length; i = i + 2) { newList.push(this.list[i]) } this.list = newList } public printAll() { for(let item of this.list) { console.log(item) } } } let mylist = new List<Number>() mylist.add(1); mylist.add(2); mylist.add(3); mylist.add(4); mylist.removeEveryOther() mylist.printAll() Let's define the T - type at runtime
  31. Modules • Starting from ES2015, JavaScript has module support, altough

    Browser support is weak • TypeScript shares the module support from ES2015 • Modules are executed in their own scope, not in global scope • Variables, functions etc are visible to other modules using export • Module can use other module's "stuff" using import • Any file containing top-level either import or export is considered to be module
  32. Importing import { Person } from "./person"; let jackObject :

    Person = {name: "jack", age: 40}; console.log(jackObject);
  33. Import: Renaming import { Person as Henkilo } from "./person";

    let jackObject : Henkilo = {name: "jack", age: 40}; console.log(jackObject);
  34. Multiexport interface Person { name: string, age: number } interface

    Movie { director: Person, title: string } export { Person, Movie };
  35. Multi-import import { Person, Movie } from "./person"; let jackObject

    : Person = {name: "jack", age: 40}; let movieObject : Movie = {director: jackObject, title: "My Movie"}
  36. Default Export Option 1 interface Person { name: string, age:

    number } export default Person; Option 2 export default interface Person { name: string, age: number }
  37. Code Generation for Modules • Compiler will develop different code

    depending on module target specification • Options: CommonJS, require.js, UMD, SystemJS, ES6
  38. Quickstart • Angular CLI - command line app • To

    install globally • npm install -g @angular/cli • ng --version • Create skeleton project • ng new my-app --strict • Run your app • ng serve –o Enables strict mode, additional typescript / js checking
  39. Concepts • Applications are written using HTML templates that may

    contain Angular markup • <h1>{{ title }}</h1> • These templates are managed by Application components • Business logic can be wrapped inside of services • All of these are sandboxed into modules • So module contains components (that contain templates) and services • The app is launched by bootstrapping a root module
  40. Component • Component controls a fragment of the screen called

    a view.. which is html • Core building block of Angular 2 apps • Reusable piece of UI that is depicted by a custom html tag • Views? • Navigation links • Content in the web page • Component's application logic is written inside of a class • Class interacts with the view through an API of properties and methods • Angular creates, updates and destroys components when user moves through the app • These classes may have lifecycle hooks / methods
  41. HTML Template and Component import { Component } from '@angular/core';

    @Component({ selector: 'custom-element', template: `<h1>Hello {{name}}</h1>` }) export class AppComponent { name = 'Angular'; } HTML template. Usually in separate html file When angular finds <custom-element> </custom-element> use the template instead Component that manages the view
  42. HTML Template and Component – sepate files import { Component

    } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; } Now template and css in separate files
  43. Module Files app.component.ts the component app.component.html the view app.component.css the

    styling app.component.spec.ts unit testing app.module.ts boxing the components into module
  44. Modules • Angular apps are modular • Angular modules or

    NgModules • Every app has at least one module: the root module, named AppModule • In large scale app you will end up with several feature modules • Module is a class with @NgModule decorator • Decorators are functions that modify JavaScript Classes • You will use both standard EcmaScript module- and Angular module system
  45. NgModule({ }) • Takes single object that describes the module

    • declarations • The view classes that belong to module: component, directive or pipe view class • imports • Other modules that are needed by component templates declared in this module • bootstrap • The main application view; root component, that hosts all other app views. Only root module should set this.
  46. app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule

    } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [], bootstrap: [AppComponent] }) export class AppModule { } By using JavaScript Module system we import modules. This is standard JS Declare all the components that are part of this module. Can be also pipes and directives AppComponent will host all other views. This is done only in the root module All components can now use BrowserModule. You can add here forms, routing, http...
  47. main.ts -> main.js import { enableProdMode } from '@angular/core'; import

    { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err)); Sets AppModule to be the main module of our app
  48. <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>MyApp</title> <base href="/">

    <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root></app-root> </body> </html> Custom element which angular will change dynamically Not a single import for javascript? <script> tags are injected by runtime here
  49. Lab

  50. Several Components import { Component } from '@angular/core'; @Component({ selector:

    'my-app', template: `<h1>{{title}}</h1> <planet-list></planet-list>` }) export class AppComponent { title : string = "Planets"; } Custom element again! This needs to be defined in other component
  51. app/planet-list/planet-list-component.ts import { Component } from '@angular/core'; interface Planet {

    name: string; diameter: number; population: number; } @Component({ selector: 'planet-list', template: `<ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>` }) export class PlanetListComponent { planets: Planet[] = [ {'name": 'Yavin IV', 'diameter': 10200, population: 1000}, {'name": 'Alderaan', 'diameter': 12500, population: 2000000000}]; } Create directory for each component Iterate all the planets
  52. app/planet-list/planet.ts export interface Planet { name: string; diameter: number; population:

    number; } Let's put the interface into separate file for better practice and reusage
  53. app/planet-list/planet-list-component.ts import { Component } from '@angular/core'; import { Planet

    } from './planet'; @Component({ selector: 'planet-list', template: `<ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>` }) export class PlanetListComponent { planets: Planet[] = [ {"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}]; } Let's import the interface
  54. Final modification to the module import { NgModule } from

    '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { PlanetListComponent } from './planet-list/planet-list.component'; import { AppComponent } from './app.component'; @NgModule({ imports: [ BrowserModule ], declarations: [ AppComponent, PlanetListComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } The components that belong to this module
  55. Angular CLI • You can create new components and stuff

    by using Angular CLI • ng g component my-new-component • Creates several files and modifies also your module!
  56. Lab

  57. import { Component } from '@angular/core'; import { Planet }

    from './planet'; @Component({ selector: 'planet-list', template: `<ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul> <p *ngIf="planets.length > 3">Too many planets</p>` }) export class PlanetListComponent { planets: Planet[] = [ {"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}, {"name": "Hoth", "diameter": 80, population: 20000}, {"name": "Dagobah", "diameter": 8900, population: 3000}]; } *ngFor for displaying array Template Syntax *ngIf for conditional statement
  58. Template Syntax • Template expression produces a value • {{

    1 + 1 }} • Many JavaScript expressions are allowed, but not all • assignments (=) • new • chaining expressions (;) • incrementing, decrementing (++, --) • Expression context is typically the component instance • {{ title }} refers to properties of the instance of the component • Should not have visible side effects, quick execution, simplicity ...
  59. Examples of Data Binding • One way • <img [src]="heroImageUrl">

    • <img bind-src="heroImageUrl"> • <img src="{{heroImageUrl}}"> • One way event • <button (click)="onSave()">Save</button> • <button on-click="onSave()">Save</button> • Two way binding in Forms • <input [(ngModel)]="name"> All the same
  60. Binding Data Between View and Component import { Component }

    from '@angular/core'; @Component({ selector: 'my-app', // <my-app/> template: `<form> <input type="text" placeholder="name" name="nameOfTheTextField" [(ngModel)]="personName" /> </form> <p>name = {{personName}}</p>` }) export class AppComponent { personName = 'kalle'; }
  61. Importing new Module import { NgModule } from '@angular/core'; import

    { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
  62. Example import { Component } from '@angular/core'; @Component({ selector: 'my-app',

    template: `<form> <input type="number" placeholder="mass" name="massText" [(ngModel)]="mass" /> <input type="number" placeholder="height" name="heightText" [(ngModel)]="height" /> </form> <p>bmi = {{mass / (height * height)}}</p>` }) export class AppComponent { } View handles the logic of calculating bmi
  63. Example import { Component } from '@angular/core'; @Component({ selector: 'my-app',

    template: `<form> <input type="number" placeholder="mass" name="massText" [(ngModel)]="mass" /> <input type="number" placeholder="height" name="heightText" [(ngModel)]="height" /> </form> <p>bmi = {{calculateBmi()}}</p>` }) export class AppComponent { mass : number = 0; height : number = 0; calculateBmi() : number { return this.mass / (this.height*this.height); } } Component handles the logic of calculating bmi
  64. Event Example import { Component } from '@angular/core'; @Component({ selector:

    'my-app', template: `<form> <input type="number" placeholder="mass" name="massText" [(ngModel)]="mass" /> <input type="number" placeholder="height" name="heightText" [(ngModel)]="height" /> <button (click)="calculateBmi()">Calculate Bmi</button> </form> <p>bmi = {{bmi}}</p>`, }) export class AppComponent { mass : number = 0; height : number = 0; bmi : number = 0; calculateBmi() : void { this.bmi = this.mass / (this.height*this.height); } } When clicking the button, call the method Method updates the bmi
  65. Lab

  66. Master Component import { Component } from '@angular/core'; @Component({ selector:

    'my-selector', template: `<ul><point [xPos]="0" [yPos]="0"></point> <point [xPos]="5" [yPos]="20"></point></ul>` }) export class AppComponent { } Passing Data to component
  67. Child Component import { Component } from '@angular/core'; import {

    Input } from '@angular/core'; @Component({ selector: 'point', template: `<li>xPos = {{xPos}}, yPos = {{yPos}}</li>` }) export class PointComponent { @Input() xPos: number @Input() yPos: number } Injecting the values given
  68. Iterating import { Component } from '@angular/core'; @Component({ selector: 'my-selector',

    template: `<ul> <point *ngFor="let point of points" [xPos]="point.xPos" [yPos]="point.yPos"></point> </ul>` }) export class AppComponent { points : Object[] = [{"xPos": 0, "yPos": 0}, {"xPos": 2, "yPos": 2}] } Iterating!
  69. Passing Variables import { Component } from '@angular/core'; @Component({ selector:

    'my-selector', template: `<monster [name]="thename"></monster>` }) export class AppComponent { thename : string = "The Monster" } Passing Data to component using variable
  70. Passing Variables import { Component } from '@angular/core'; @Component({ selector:

    'my-selector', template: `<monster [name]="'The Monster'"></monster>` }) export class AppComponent { } Passing Data to component without variable
  71. Parent: Parent listening to Children import { Component } from

    '@angular/core'; @Component({ selector: 'my-selector', template: `<ul> <point (onDeleteClicked)="clicked($event)" [xPos]="0" [yPos]="0"></point> </ul>` }) export class AppComponent { clicked(event: string) { console.log(event); // delete clicked } } Child triggers an event that the Parent catches. The onDeleteClicked is something that must be implemented in child
  72. Child: Parent listening to Children import { Component } from

    '@angular/core'; import { Input } from '@angular/core'; import { Output } from '@angular/core'; import { EventEmitter } from '@angular/core'; @Component({ selector: 'point', template: `<li>xPos = {{xPos}}, yPos = {{yPos}} <button (click)="delete()">Delete</button></li>` }) export class PointComponent { @Input() xPos: number @Input() yPos: number @Output() onDeleteClicked = new EventEmitter<string>(); delete() { this.onDeleteClicked.emit('delete clicked') } } When delete button is pressed, send the event The parent listenes to this Triggering the event
  73. Component Lifecycle • constructor • ngOnChanges • If simple input

    changes • ngOnInit • When component is initialized • ngDoCheck • If "complicated" input changes • ngOnDestroy • When component is destroyed
  74. Example App Component import { Component } from '@angular/core'; @Component({

    selector: 'my-selector', template: `<button (click)="toggle()">Toggle Component</button> <lifecyclecomponent *ngIf="isVisible"></lifecyclecomponent>` }) export class AppComponent { isVisible : boolean = true toggle() { this.isVisible = !this.isVisible } } Button will toggle the nameform component ON and OFF
  75. Basic Lifecycle hooks import { Component } from '@angular/core'; import

    { OnInit } from '@angular/core'; import { OnDestroy } from '@angular/core'; @Component({ selector: 'life-cycle', template: `<p>Testing lifecycle</p>` }) export class LifeCycleComponent implements OnInit, OnDestroy { ngOnInit() { console.log("init") } ngOnDestroy() { console.log("destroy") } } Interfaces are voluntary here, you do not need them
  76. ngOnInit and Destroy • To perform complex initializations shortly after

    construction • To set up component after Angular sets input properties • So if for example your component fetches data from server do NOT do this in constructor • In Destroy you can do clean ups if necessary
  77. ngOnChanges – Example using Primitive Type @Component({ selector: 'app-parent', template:

    `<h2>Parent</h2> <button (click)="updateUser()">Update</button> <app-child [user]="name"></app-child>` }) export class ParentComponent { name = 'tina'; updateUser(): void { this.name = 'jack'; } }
  78. ngOnChanges – Example using Primitive Type @Component({ selector: 'app-child', template:

    '<p>{{user}}</p>' }) export class ChildComponent implements OnChanges { @Input() user = ''; ngOnChanges(): void { console.log('name changed'); } } Triggers event when input changes. user here is primitive type!
  79. ngOnChanges – Reference type import { Component, OnInit } from

    '@angular/core'; @Component({ selector: 'app-parent', template: `<h2>Parent</h2> <button (click)="updateUser()">Update</button> <app-child [user]="user"></app-child>` }) export class ParentComponent { user = { name: 'tina' }; updateUser(): void { this.user.name = 'jack'; } }
  80. ngOnChanges – Reference type @Component({ selector: 'app-child', template: '<p>{{user.name}}</p>' })

    export class ChildComponent implements OnChanges { @Input() user = { name: '' }; ngOnChanges(): void { console.log('name changed'); } } This is not called! ngOnChanges won't detect changes inside of reference type!
  81. ngDoCheck - example @Component({ selector: 'app-child', template: '<p>{{user.name}}</p>' }) export

    class ChildComponent implements DoCheck { @Input() user = { name: '' }; ngDoCheck(): void { console.log('name changed'); } } Detects also reference type updates
  82. Lab

  83. CSS

  84. Component Styles • Use standard CSS • You can use

    component styles • More modular approach to regular stylesheets import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: '<h1>Hello</h1>', styles: ['h1 { color: RGB(255,0,0) }'] }) export class AppComponent { title = 'app'; }
  85. Overview • Attribute directives: used in attributes in elements •

    <p doSomething>...</p> • Structural directives: change the structure of the view • ngFor, ngIf ...
  86. Generating directive ng g directive dosomething create src/app/dosomething.directive.spec.ts (244 bytes)

    create src/app/dosomething.directive.ts (151 bytes) update src/app/app.module.ts (484 bytes)
  87. Module import { DosomethingDirective } from './dosomething.directive'; @NgModule({ declarations: [

    AppComponent, ChildComponent, DosomethingDirective ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
  88. Directive import { Directive } from '@angular/core'; @Directive({ selector: '[appDosomething]'

    }) export class DosomethingDirective { constructor() { } } Defining selector (attribute name) Does not do anything yet.
  89. Usage import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: '<h1 appDosomething>Hello</h1>', }) export class AppComponent { title = 'app'; } Using the directive
  90. Implementing Logic import { Directive, ElementRef } from '@angular/core'; @Directive({

    selector: '[appDosomething]' }) export class DosomethingDirective { constructor(el: ElementRef) { el.nativeElement.style.backgroundColor = 'yellow'; } } Importing reference to the element that has the attribute Native DOM - element
  91. import { Directive, ElementRef, HostListener } from '@angular/core'; @Directive({ selector:

    '[appDosomething]' }) export class DosomethingDirective { private el: ElementRef; constructor(el: ElementRef) { this.el = el; } @HostListener('mouseenter') onMouseEnter() { this.highlight('yellow'); } @HostListener('mouseleave') onMouseLeave() { this.highlight(null); } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; } } @HostListener let's you subscribe to events of the host DOM element
  92. TypeScript Constructor Initializing attribute export class DosomethingDirective { private el:

    ElementRef; constructor(el: ElementRef) { this.el = el; } } Shorter syntax export class DosomethingDirective { constructor(private el: ElementRef) {} }
  93. Input import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: '<h1 appDosomething [color]="someColor">Hello</h1>' }) export class AppComponent { title = 'app'; someColor = 'red'; } You can give input to the directive
  94. Receiving Input import { Directive, ElementRef, HostListener, Input } from

    '@angular/core'; @Directive({ selector: '[appDosomething]' }) export class DosomethingDirective { @Input() color: string; ... Using @Input to get the color
  95. Cleaner Syntax import { Component } from '@angular/core'; @Component({ selector:

    'app-root', // template: '<h1 appDosomething [color]="someColor">Hello</h1>' template: '<h1 [appDosomething]="someColor">Hello</h1>' }) export class AppComponent { title = 'app'; someColor = 'red'; } The directive name and the input in the same
  96. Receiving Input import { Directive, ElementRef, HostListener, Input } from

    '@angular/core'; @Directive({ selector: '[appDosomething]' }) export class DosomethingDirective { @Input() appDosomething: string; ... Change the input name to selector. Can be confusing name?
  97. Using Alias import { Directive, ElementRef, HostListener, Input } from

    '@angular/core'; @Directive({ selector: '[appDosomething]' }) export class DosomethingDirective { @Input('appDosomething') color: string; ... Now better variable name
  98. Lab

  99. Structural Directives • Structural directives are responsible for HTML layout

    • Reshape DOM • Apply the directive to host element, just like in attributes • <div *ngIf="something">...</div> • The asterisk * precedes the structural directive as a convenience notation • Three common built-in structural directives • ngIf, ngFor, ngSwitch
  100. ngSwitch import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<div [ngSwitch]="day"> <p *ngSwitchCase="1">January</p> <p *ngSwitchCase="2">February</p> <p *ngSwitchDefault>Some other month</p> </div>`}) export class AppComponent { day = 1; } Will display "January"
  101. *ngIf import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<div *ngIf="display">Is this displayed?</div>` }) export class AppComponent { display = true; } The *ngIf is translated to something else!
  102. *ngIf => ng-template import { Component } from '@angular/core'; @Component({

    selector: 'app-root', template: `<ng-template [ngIf]="display"> <div>Is this displayed?</div> </ng-template>` }) export class AppComponent { display = true; } The end result is like this. *ngFor and *ngSwitch follows the same pattern.
  103. *ngFor import { Component } from '@angular/core'; interface Person {

    name: string; } @Component({ selector: 'app-root', template: ` <div *ngFor="let person of persons"> {{person.name}} </div>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}]; } Iterating the list. We have more options here also...
  104. *ngFor: predefined variable index import { Component } from '@angular/core';

    interface Person { name: string; } @Component({ selector: 'app-root', template: ` <div *ngFor="let person of persons; let i=index;"> {{i}} {{person.name}} </div>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}]; } 0,1, ...
  105. *ngFor: predefined variable odd import { Component } from '@angular/core';

    interface Person { name: string; } @Component({ selector: 'app-root', template: ` <div *ngFor="let person of persons; let i=index; let o=odd;"> {{i}} {{o}} {{person.name}} </div>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}, {name: 'paul'}, {name: 'sara'}]; } true, false, true, false ...
  106. *ngFor uses the template import { Component } from '@angular/core';

    interface Person { name: string; } @Component({ selector: 'app-root', template: `<ng-template ngFor let-person [ngForOf]="persons" let-i="index" let-o="odd"> <div>{{i}} {{person.name}}</div> </ng-template>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}, {name: 'paul'}, {name: 'sara'}]; } The *ngFor is compiled to this, but as you can see, *ngFor is much cleaner syntax and preferred
  107. Animations • Angular animations are built on top of standard

    Web Animations API • It helps if you know the Animations API when working with Angular animations • Angular animations have a specific modules • import { BrowserAnimationsModule } from '@angular/platform- browser/animations'; • To build a animation you can have states • When state changes, animation occurs
  108. State? export class LightbulbComponent { private _state = 'on'; toggle():

    boolean { this._state = (this._state === 'on') ? 'off' : 'on'; return false; } get state() { return this._state; } } State can be whatever. We can attach "listeners" to this. When state changes animation occurs
  109. import { BrowserModule } from '@angular/platform-browser'; import { NgModule }

    from '@angular/core'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { LightbulbComponent } from './lightbulb/lightbulb.component'; @NgModule({ declarations: [ AppComponent, LightbulbComponent ], imports: [ BrowserModule, BrowserAnimationsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } Import the animation's module
  110. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; Define animation when state changes
  111. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; Display just "On" or "Off" inside of p
  112. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; Changes the state
  113. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; Trigger lightBulbState and use "On" or "Off"..
  114. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; The lightBulbState is defined here
  115. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; When state is on, use this style
  116. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; When state is off, use this style
  117. import { Component } from '@angular/core'; import { trigger, state,

    style, animate, transition } from '@angular/animations'; @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" (click)="toggle()">{{state}}</p>', styles: ['p { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on => off', animate('500ms ease-in')), transition('off => on', animate('500ms ease-out')) ]) ] }) export class LightbulbComponent { private _state = 'on'; What kind of transitions to use when changing the state
  118. @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" href="" (click)="toggle()">{{state}}</p>', styles: ['p

    { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on <=> off', animate('500ms linear')) ]) ] }) If the same kind of animate, we can use <=> See http://easings.net/ for details about easy-in etc
  119. Using wildcards * • Match any state using * •

    on => * • * => off • * => *
  120. To define enter and leave: use void state • When

    element enters the view use • void => * • When element leaves the view use • * => void
  121. @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" href="" (click)="toggle()">{{state}}</p>', styles: ['p

    { display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('void => *', [...]), transition('* => void', [...]), ]) ] }) Instead of declaring the styles in states, now you declare them in the transition itself...
  122. @Component({ selector: 'app-lightbulb', template: '<p [@lightBulbState]="state" href="" (click)="toggle()">{{state}}</p>', styles: ['p

    { display: inline-block; }'], animations: [ trigger('lightBulbState', [ transition(':enter', [...]), transition(':leave', [...]), ]) ] }) You can also use a cleaner syntax here :enter == void => * :leave == * => void
  123. @Component({ selector: 'app-lightbulb', template: `<a (click)="toggle()">Toggle</a> <p *ngIf="state" [@lightBulbState]="state">Lightbulb!</p>`, styles:

    ['p { display: inline-block; }', 'a { display: block; }'], animations: [ trigger('lightBulbState', [ transition(':enter', [ style({transform: 'translateX(-200%)'}), animate(500) ]), transition(':leave', [ animate(500, style({opacity: 0, transform: 'translateX(500%)'})) ]) ]) ] }) export class LightbulbComponent { private _state = true; ... Will toggle the lightbulb off and on of the view state is now boolean because of the *ngIf When entering 1) set lightbulb off the screen 2)Animate it to original position When leaving, animate it to right and set opacity to 0 (invisible)
  124. Routing • Change HTML dynamically when URL changes • Entering

    URLs • Clicking links • Forward and back • Display particular component for a given URL • Use @Angular/Router library package • Then configure (which view is displayed on what URL)
  125. import { BrowserModule } from '@angular/platform-browser'; import { NgModule }

    from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { AddComponent } from './add/add.component'; import { ViewComponent } from './view/view.component'; const appRoutes: Routes = [ { path: 'add', component: AddComponent }, { path: 'view',component: ViewComponent }, ]; @NgModule({ declarations: [ AppComponent, AddComponent, ViewComponent ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes, { enableTracing: true }) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } Configuring Just for debugging
  126. app.component.html <h1> Welcome to {{title}}! </h1> <nav> <a routerLink="/add" routerLinkActive="active">Add</a>

    <a routerLink="/view" routerLinkActive="active">View</a> </nav> <div> <router-outlet></router-outlet> </div> Depending on the URL different components are injected after the router- outlet Creating links adds "active" CSS class so you can style the link differently
  127. @Component({ selector: 'app-root', template: `<h1> Welcome to {{title}}! </h1> <nav>

    <a routerLink="/add" routerLinkActive="thisischosen">Add</a> <a routerLink="/view" routerLinkActive="thisischosen">View</a> </nav> <div> <router-outlet></router-outlet> </div>`, styles: ['.thisischosen { background-color: lightgray; }'] }) export class AppComponent { css class
  128. wild card const appRoutes: Routes = [ { path: 'add',

    component: AddComponent }, { path: 'view', component: ViewComponent }, { path: '**', component: NotfoundComponent } ]; If any other url, then..
  129. Redirect const appRoutes: Routes = [ { path: 'add', component:

    AddComponent }, { path: 'view', component: ViewComponent }, { path: 'jussi', redirectTo: '/add', pathMatch: 'full' }, { path: '**', component: NotfoundComponent } ]; If path is http://localhost:4200/jussi the redirect to http://localhost:4200/add
  130. Redirect const appRoutes: Routes = [ { path: 'add', component:

    AddComponent }, { path: 'view', component: ViewComponent }, { path: 'jussi', redirectTo: '/add', pathMatch: 'prefix' }, { path: '**', component: NotfoundComponent } ]; If path is http://localhost:4200/jussi/jaska the redirect to http://localhost:4200/add
  131. Redirect const appRoutes: Routes = [ { path: 'add', component:

    AddComponent }, { path: 'view', component: ViewComponent }, { path: '', redirectTo: '/add', pathMatch: 'full' }, { path: '**', component: NotfoundComponent } ]; If path is http://localhost:4200/ then redirect to http://localhost:4200/add
  132. import { NgModule } from '@angular/core'; import { RouterModule, Routes

    } from '@angular/router'; import { AddComponent } from './add/add.component'; import { ViewComponent } from './view/view.component'; import { NotfoundComponent } from './notfound/notfound.component'; const appRoutes: Routes = [ { path: 'add', component: AddComponent }, { path: 'view', component: ViewComponent }, { path: '', redirectTo: '/add', pathMatch: 'full' }, { path: '**', component: NotfoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } ) ], exports: [ RouterModule ] }) export class AppRoutingModule {} Separate the routing stuff into it's own module: app-routing.module.ts
  133. import { BrowserModule } from '@angular/platform-browser'; import { NgModule }

    from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AddComponent } from './add/add.component'; import { ViewComponent } from './view/view.component'; import { NotfoundComponent } from './notfound/notfound.component'; @NgModule({ declarations: [ AppComponent, AddComponent, ViewComponent, NotfoundComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } Import it to the main module
  134. Lab

  135. View Module • The View part of the app displays

    • 1) List of posts • 2) When clicking a post a detailed information about the post • in routes • http://localhost:4200/view • http://localhost:4200/view/1 • We can define own module and own routing module for this • Can be overkill in small applications
  136. view/view.module.ts import { NgModule } from '@angular/core'; import { HttpClientModule

    } from '@angular/common/http'; import { CommonModule } from '@angular/common'; import { ViewComponent } from './view.component'; import { ViewDetailComponent } from './view-detail.component'; import { ViewRoutingModule } from './view-routing.module'; @NgModule({ imports: [ CommonModule, HttpClientModule, ViewRoutingModule ], declarations: [ ViewComponent, ViewDetailComponent ] }) export class ViewModule {} The view module has it's own routing Just the view components
  137. app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule

    } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AddComponent } from './add/add.component'; import { NotfoundComponent } from './notfound/notfound.component'; import { ViewModule } from './view/view.module'; @NgModule({ declarations: [ AppComponent, AddComponent, NotfoundComponent ], imports: [ BrowserModule, ViewModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } Importing the module
  138. view/view-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule,

    Routes } from '@angular/router'; import { ViewComponent } from './view.component'; import { ViewDetailComponent } from './view-detail.component'; const mySubRoutes: Routes = [ { path: 'view', component: ViewComponent }, { path: 'view/:id', component: ViewDetailComponent } ]; @NgModule({ imports: [ // FOR CHILD! RouterModule.forChild(mySubRoutes) ], exports: [ RouterModule ] }) export class ViewRoutingModule { } "Sub routing" Instead of forRoot, use forChild!
  139. app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule,

    Routes } from '@angular/router'; import { AddComponent } from './add/add.component'; // import { ViewComponent } from './view/view.component'; import { NotfoundComponent } from './notfound/notfound.component'; const appRoutes: Routes = [ { path: 'add', component: AddComponent }, // { path: 'view', component: ViewComponent }, { path: '', redirectTo: '/add', pathMatch: 'full' }, { path: '**', component: NotfoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } ) ], exports: [ RouterModule ] }) export class AppRoutingModule {} We can take away the view part from the main routing module
  140. view/view.component.ts import { Component, OnInit } from '@angular/core'; import {

    HttpClient } from '@angular/common/http'; import { Post } from './view-post'; @Component({ selector: 'app-view', template: `<ul><li *ngFor="let post of posts"><a routerLink="{{post.id}}">{{post.title}}</a></li></ul>` }) export class ViewComponent implements OnInit { posts: Post[] = []; constructor(private http: HttpClient) { } ngOnInit() { this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts').subscribe(jsonObject => { this.posts = jsonObject; }); } } Displays a list of posts. When clicking, change the url
  141. view/view-detail.component.ts import { Component, OnInit } from '@angular/core'; import {

    Router, ActivatedRoute, Params } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { Post } from './view-post'; @Component({ selector: 'app-view-detail', template: `<h2>{{post.title}}</h2><p>{{post.body}}</p>` }) export class ViewDetailComponent implements OnInit { post: Post = {id: undefined, title: '', body: '', userId: undefined}; constructor( private activatedRoute: ActivatedRoute, private http: HttpClient) {} ngOnInit() { // subscribe to "url" this.activatedRoute.params.subscribe((params: Params) => { // when changes, get id url value const id = params['id']; // Fetch content this.http.get<Post>('https://jsonplaceholder.typicode.com/posts/' + id).subscribe(jsonObject => { this.post = jsonObject; }); }); } }
  142. import { Component, OnInit } from '@angular/core'; import { ActivatedRoute,

    Params } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { Post } from './view-post'; @Component({ selector: 'app-view-detail', template: `<h2>{{post.title}}</h2><p>{{post.body}}</p>` }) export class ViewDetailComponent implements OnInit { post: Post = {id: undefined, title: '', body: '', userId: undefined}; constructor( private activatedRoute: ActivatedRoute, private http: HttpClient) {} ngOnInit() { // subscribe to "url" this.activatedRoute.params.subscribe((params: Params) => { // when changes, get id url value const id = params['id']; // Fetch content this.http.get<Post>('https://jsonplaceholder.typicode.com/posts/' + id).subscribe(jsonObject => { this.post = jsonObject; }); }); } } Getting the id value from url
  143. import { Component, OnInit } from '@angular/core'; import { Router,

    ActivatedRoute, Params } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { Post } from './view-post'; @Component({ selector: 'app-view-detail', template: `<h2>{{post.title}}</h2><p>{{post.body}}</p><button (click)="back()" href="">Back</button>` }) export class ViewDetailComponent implements OnInit { post: Post = {id: undefined, title: '', body: '', userId: undefined}; constructor( private router: Router, private activatedRoute: ActivatedRoute, private http: HttpClient) {} back() { this.router.navigate(['view']); } ngOnInit() { // subscribe to "url" this.activatedRoute.params.subscribe((params: Params) => { // when changes, get id url value const id = params['id']; // Fetch content this.http.get<Post>('https://jsonplaceholder.typicode.com/posts/' + id).subscribe(jsonObject => { this.post = jsonObject; }); }); } } Navigating
  144. import { Component, OnInit } from '@angular/core'; import { Router,

    ActivatedRoute, Params } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { Post } from './view-post'; @Component({ selector: 'app-view-detail', template: `<h2>{{post.title}}</h2><p>{{post.body}}</p><button (click)="back()" href="">Back</button>` }) export class ViewDetailComponent implements OnInit { post: Post = {id: undefined, title: '', body: '', userId: undefined}; constructor( private router: Router, private activatedRoute: ActivatedRoute, private http: HttpClient) {} back() { this.router.navigate(['view', {id: this.post.id}]); } ngOnInit() { // subscribe to "url" this.activatedRoute.params.subscribe((params: Params) => { // when changes, get id url value const id = params['id']; // Fetch content this.http.get<Post>('https://jsonplaceholder.typicode.com/posts/' + id).subscribe(jsonObject => { this.post = jsonObject; }); }); } } Giving the id back: http://localhost:4200/view;id=1
  145. view.component.ts import { Component, OnInit } from '@angular/core'; import {

    HttpClient } from '@angular/common/http'; import { Post } from './view-post'; import { ActivatedRoute, Params } from '@angular/router'; @Component({ selector: 'app-view', template: `<ul><li *ngFor="let post of posts" [class.selected]="post.id == selectedId"> <a routerLink="{{post.id}}">{{post.title}}</a></li></ul>`, styles: ['.selected { background-color: lightgray; }'] }) export class ViewComponent implements OnInit { posts: Post[] = []; constructor(private http: HttpClient, private activatedRoute: ActivatedRoute) { } private selectedId: number; ngOnInit() { this.activatedRoute.params.subscribe((params: Params) => { const id = params['id']; this.selectedId = id; }); this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts').subscribe(jsonObject => { this.posts = jsonObject; }); } } Get the value
  146. Example const appRoutes: Routes = [ { path: 'modify', component:

    ModifyComponent, children: [ {path: '', redirectTo: 'add', pathMatch: 'full'}, {path: 'add', component: AddComponent}, {path: 'delete', component: DeleteComponent} ]} ]; If url is modify/add then display ModifyComponent and inside of this display AddComponent
  147. App- Component (Main) import { Component } from '@angular/core'; @Component({

    selector: 'app-root', template: `<h1> Welcome to {{title}}! </h1> <nav> <a routerLink="/modify" routerLinkActive="thisischosen">Modify</a> <a routerLink="/view" routerLinkActive="thisischosen">View</a> </nav> <div> <router-outlet></router-outlet> </div>`, styles: ['.thisischosen { background-color: lightgray; }'] }) export class AppComponent { title = 'app'; } if modify/ and view/ then change the components
  148. Modify - Component import { Component, OnInit } from '@angular/core';

    @Component({ selector: 'app-modify', template: `<h3>Modify</h3> <nav> <a routerLink="add" routerLinkActive="selected">Add</a> | <a routerLink="delete" routerLinkActive="selected">Delete</a> </nav> <router-outlet></router-outlet>`, styles: ['.selected { background-color: lightgray; }'] }) export class ModifyComponent implements OnInit { constructor() { } ngOnInit() { } } Another <router-outlet>! If modify/add or modify/delete Notice the difference between /add and add
  149. Route Guards • Route Guards tells to the router if

    requested route is permitted or not • Decisision is just true or false comparison from a class that implements guard interface
  150. Simple Guard Service import { Injectable } from '@angular/core'; import

    { CanActivate } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { canActivate() { console.log('Called'); return true; } } true => can access, false => cannot access
  151. Setting up const mySubRoutes: Routes = [ { path: 'modify',

    component: ModifyComponent, canActivate: [AuthGuard], children: [ {path: '', redirectTo: 'add', pathMatch: 'full'}, {path: 'add', component: AddComponent}, {path: 'delete', component: DeleteComponent} ]}, ]; modify now protected under AuthGuard. Remember to add AuthGuard to providers to the module
  152. Pipes • To display value transformations use pipes • Built-in

    pipes • currency, date, percent, uppercase, lowercase ... • Usage • {{ something | pipe }} • For example • {{ title | uppercase }}
  153. Pipe usage: uppercase import { Component } from '@angular/core'; @Component({

    selector: 'my-selector', template: `<h1>{{title | uppercase}}</h1> <planet-list></planet-list>` }) export class AppComponent { title : string = 'Planets' } PLANETS
  154. Pipe usage import { Component } from '@angular/core'; @Component({ selector:

    'my-selector', template: `<h1>{{title}}</h1> <p>Today is {{ todayDate | date:'dd.mm.yyyy'}} <planet-list></planet-list>` }) export class AppComponent { title : string = 'Planets' todayDate : Date = new Date() } Displays date in the following format
  155. Custom Pipe import { Component } from '@angular/core'; import {

    Planet } from './planet'; @Component({ selector: 'planet-list', template: `<ul> <li *ngFor="let planet of planets | myfilter: 'Alderaan'"> {{planet.name}} </li> </ul>` }) export class PlanetListComponent { planets: Planet[] = [ {"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}]; } Display only planets that contain 'Alderaan'
  156. filter.pipe.ts import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'myfilter'})

    export class MyFilterPipe implements PipeTransform { transform(items: any[], filter: string): any { // If no filter text given if(!filter) { return items } let newItems : any[] = [] for(let itemObject of items) { for(let key in itemObject) { // Make regex to the value let value : string = String(itemObject[key]) if(value.match(filter)) { newItems.push(itemObject) } } } return newItems } } items here contains the planets. The filter contains "Alderaan". Returns a array containing only planets that has "Alderaan
  157. Updating the Module.. import { BrowserModule } from '@angular/platform-browser'; import

    { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PlanetListComponent } from './planet-list/planet-list.component'; import { MyFilterPipe } from './filter.pipe'; @NgModule({ declarations: [ AppComponent, PlanetListComponent, MyFilterPipe ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
  158. Usage import { Component } from '@angular/core'; import { Planet

    } from './planet'; @Component({ selector: 'planet-list', template: `<br><input type="text" placeholder="filter" [(ngModel)]="filterText" /> <ul> <li *ngFor="let planet of planets | myfilter: filterText"> {{planet.name}} </li> </ul>` }) export class PlanetListComponent { planets: Planet[] = [ {"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}]; filterText: string = "" } Filttering according to user input
  159. Service • Angular service is a class that encapulates some

    sort of functionality and provides a service to the rest of the application • In AngularJS 1.X you had service, factory, provider but no more. Just class.
  160. Simple Service: planet.service.ts import { Injectable } from '@angular/core'; import

    { Planet } from './planet'; @Injectable() export class PlanetService { fetch() : Planet[] { return [{"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}]; } } Now the planets are coming from a service
  161. Component: planet-list.component.ts import { Component } from '@angular/core'; import {

    Planet } from '../planet'; import { PlanetService } from '../planet.service'; @Component({ selector: 'planet-list', template: ` <ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>`, providers: [PlanetService] }) export class PlanetListComponent { planets: Planet[] = [] constructor(planetService : PlanetService) { this.planets = planetService.fetch(); } } When creating the object from PlaneListComponent Angular will inject the service The Service is available only in this component. Declare providers in main component or even module if you want it to be available throughout your app.
  162. Component: planet-list.component.ts import { Component } from '@angular/core'; import {

    Planet } from '../planet'; import { PlanetService } from '../planet.service'; @Component({ selector: 'planet-list', template: ` <ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>`, providers: [PlanetService] }) export class PlanetListComponent { planets: Planet[] = [] constructor(planetService : PlanetService) { this.planets = planetService.fetch(); } } We can remove this and add it to module
  163. module: app.module.ts import { NgModule } from '@angular/core'; import {

    BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { PlanetListComponent } from './planet-list/planet-list.component'; import { AppComponent } from './app.component'; import { PlanetService } from './planet.service'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, PlanetListComponent ], providers: [ PlanetService ], bootstrap: [ AppComponent ] }) export class AppModule { } Now the service available throughout your app
  164. Lab

  165. Connecting to Backend • Front-end apps communicate with backend services

    using HTTP • Two different APIs: XMLHttpRequest and Fetch API • Angular provides HttpClient • Build on top of XMLHttpRequest • HttpClient is implemented in HttpClientModule which needs to be imported first • Once installed you can inject the service
  166. import { BrowserModule } from '@angular/platform-browser'; import { NgModule }

    from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { PlanetListComponent } from './planet-list/planet-list.component'; @NgModule({ declarations: [ PlanetListComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [PlanetListComponent] }) export class AppModule { } Now you can inject HttpClient in all declared components
  167. import { Component } from '@angular/core'; import { Planet }

    from './planet'; import { HttpClient } from '@angular/common/http'; import { OnInit } from '@angular/core' @Component({ selector: 'planet-list', template: `<ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>` }) export class PlanetListComponent implements OnInit { public planets: Planet[] = []; private http: HttpClient constructor(http: HttpClient) { this.http = http } ngOnInit(): void { this.http.get('https://swapi.co/api/planets/').subscribe(jsonObject => { // {"count": 61, "next": "", "results": [...]} this.planets = jsonObject['results']; }); } } Import HttpClient Service Injects automatically the HttpClient AJAX call to backend
  168. ngOnInit(): void { this.http.get('https://swapi.co/api/planets/') .subscribe(jsonObject => { console.log(typeof jsonObject) //

    this.planets = jsonObject['results'] this.planets = jsonObject.results }); } It is object Will complain because it may not have results property
  169. Backend JSON: https://swapi.co/api/planets/ { "count": 61, "next": "https://swapi.co/api/planets/?page=2", "previous": null,

    "results": [ { "name": "Alderaan", "rotation_period": "24", "orbital_period": "364", "diameter": "12500", "climate": "temperate", "gravity": "1 standard", "terrain": "grasslands, mountains", "surface_water": "40", ...
  170. import { ItemResponse } from './itemresponse'; ngOnInit(): void { this.http.get<ItemResponse>('https://swapi.co/api/planets/')

    .subscribe(jsonObject => { this.planets = jsonObject.results }); } Define the type of given json object. This is an interface that we have to implement It should now work
  171. ItemResponse Interface import { Planet } from './planet' export interface

    ItemResponse { count: number; next: string; previous: string; results: [Planet] } Planet - array
  172. Getting Headers ngOnInit(): void { this.http.get<ItemResponse>('https://swapi.co/api/planets/', {observe: 'response'}).subscribe(response => {

    // application/json console.log(response.headers.get('Content-type')) // 200 console.log(response.status) this.planets = response.body.results; }); } Pass configuration object HttpResponse<ItemResponse>
  173. HTTP POST • HTTP Post is easy too • http.post(url,

    body).subscribe((result) => {}) • You can also give configuration object • http.post(url, body, {observe: 'response'}).subscribe((response) => {}) • And use interfaces • http.post<SomeInterface>(url, body).subscribe((result) => {})
  174. import { Component } from '@angular/core'; import { HttpClient }

    from '@angular/common/http' interface LocationResponse { id: number, latitude: number, longitude: number } @Component({ selector: 'add-location', template: `<input type="text" placeholder="latitude" name="latitudeText" [(ngModel)]="latitude" /> <input type="text" placeholder="longitude"name="longitudeText" [(ngModel)]="longitude" /> <button (click)="sendToServer()">Send</button> <p>{{msg}}</p>` }) export class AddLocationComponent { latitude: number longitude: number msg: string = "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ).subscribe(response => { if(response.status === 201) { this.msg = `Saved with id = ${response.body.id}` } }) } } Expecting this data from backend Sending Json object to backend If we got 201 CREATED then display it in UI
  175. Error Handling • If request fails or getting 4xx results?

    • Add error handler • http.post(url).subscribe(successFunc, errFunc) • You will get argument of type HttpErrorMessage to the errFunc • The errFunc is called if backend responds with 4xx, 5xx • Also if frontend creates exception
  176. export class AddLocationComponent { latitude: number longitude: number msg: string

    = "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe(this.success, this.error) } error(err: HttpErrorResponse) { console.log('error' + err.error.message) } success(response : HttpResponse<LocationResponse>) { let locationObject : LocationResponse = response.body console.log(locationObject.id) console.log(locationObject.latitude) console.log(locationObject.longitude) } } Calls functions
  177. export class AddLocationComponent { latitude: number longitude: number msg: string

    = "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe(this.success, this.error) } error(err: HttpErrorResponse) { this.msg = "Some problem with saving data." } success(response : HttpResponse<LocationResponse>) { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` } } Modify the UI Modify the UI Binded to view Does not work!
  178. Working Example sendToServer() { let body = {"latitude": this.latitude, "longitude":

    this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe((response) => { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` }) } How come the anonymous arrow function work? It uses closures...
  179. Under the Engine... sendToServer() { let body = {"latitude": this.latitude,

    "longitude": this.longitude} let _this = this this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe((response) => { let locationObject : LocationResponse = response.body _this.msg = `Saved with an id of ${locationObject.id}` }) } _this is still in memory because closure
  180. export class AddLocationComponent { latitude: number longitude: number msg: string

    = "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe(this.success, this.error) } error(err: HttpErrorResponse) { this.msg = "Some problem with saving data." } success(response : HttpResponse<LocationResponse>) { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` } } this does NOT refer to AddLocationComponent object anymore
  181. export class AddLocationComponent { latitude: number longitude: number msg: string

    = "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe(this.success.bind(this), this.error.bind(this)) } error(err: HttpErrorResponse) { this.msg = "Some problem with saving data." } success(response : HttpResponse<LocationResponse>) { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` } } Function binding to the rescue!
  182. Material Design • Material Design is a design language developed

    by Google • Most of Google's mobile apps had applied this style • Gmail, Youtube, Docs.. • Also Web interfaces of these apps! • Google has developed prebuilt UI components for Angular that follows this Material Design • Although it's possible to override the styling of these • In addition of providing look and feel you will have basic implementation of various UI components
  183. Angular Material • Material Design Components for Angular • https://material.angular.io

    • Fast and Consistent • Versatile • Optimized for Angular
  184. Getting Started • Install Angular Material and Angular CDK (Component

    Development Kit) • npm install --save @angular/material @angular/cdk • Include BrowserAnimationsModule in your app • imports: [BrowserAnimationsModule] • Include Modules from Angular Material • import {MatButtonModule, MatCheckboxModule} from '@angular/material'; • ...imports: [MatButtonModule, MatCheckboxModule] • Include a theme in styles.css • @import "~@angular/material/prebuilt-themes/indigo-pink.css"; • Install gesture support (needed for couple of the components) • npm install --save hammerjs • Add Material Icons in index.html • <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  185. Using Components • See documentation about the components • https://material.angular.io/components/categories

    • The documentation is really basic, rely on the examples given! • Different Components for • Form, Navigation, Layout, Buttons, Popups & Modals, Data table
  186. Example Template <h3>Raised Buttons</h3> <div class="button-row"> <button mat-raised-button>Basic</button> <button mat-raised-button

    color="primary">Primary</button> <button mat-raised-button color="accent">Accent</button> <button mat-raised-button color="warn">Warn</button> <button mat-raised-button disabled>Disabled</button> </div>
  187. User Input and $event • To bind to a DOM

    event • <button (click)="onClickMe()">Click me!</button> • Using $event • <input (keyup)="onKey($event)"> • The corresponding event object is given to the onKey – method • The properties of the $event object depends on the type of DOM event • Mouse event is different than key event
  188. $event import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { console.log(event); } } Base class for all the DOM Events Either MouseEvent or KeyboardEvent
  189. $event import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { console.log(event.target); } } Either <input ..> or <p>..
  190. $event import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { console.log(event.target.value); } } Angular will warn about this. The value is not present in event.target
  191. import { Component } from '@angular/core'; @Component({ selector: 'app-root', template:

    `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { const givenTarget: EventTarget = event.target; if (givenTarget instanceof HTMLInputElement) { const input: HTMLInputElement = givenTarget; console.log(input.value); } else if (givenTarget instanceof HTMLElement) { console.log(givenTarget.textContent); } } } Will output "Click Me" Will output given text Interface that HTMLInputElement and HTMLElement implemets
  192. import { Component } from '@angular/core'; @Component({ selector: 'app-root', template:

    `<input (keyup)="doSomething($event)" placeholder="Write">` }) export class AppComponent { doSomething(event: KeyboardEvent) { console.log(event.key); // const givenTarget: HTMLInputElement = event.target as HTMLInputElement; // console.log(givenTarget.value); } } If we use KeyboardEvent directly, then we can use it's properties..
  193. Template Reference Variable import { Component } from '@angular/core'; @Component({

    selector: 'app-root', template: `<input #donaldduck (keyup)="0" placeholder="Write"> {{donaldduck.value}}` }) export class AppComponent { /* doSomething(event: KeyboardEvent) { const givenTarget: HTMLInputElement = event.target as HTMLInputElement; console.log(givenTarget.value); }*/ } Create new variable that refers to the <input>. No need for the event function! Must have something in here
  194. keyup.enter import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<input (keyup.enter)="enterIsPressed($event)" placeholder="Write"> {{value}}` }) export class AppComponent { value = ''; enterIsPressed(event: KeyboardEvent) { const givenTarget: HTMLInputElement = event.target as HTMLInputElement; this.value = givenTarget.value; } } Angular's own event When enter is pressed, display the results
  195. key.enter and Template Ref Variable import { Component } from

    '@angular/core'; @Component({ selector: 'app-root', template: `<input #donaldduck (keyup.enter)="enterIsPressed(donaldduck.value)" placeholder="Write"> {{value}}` }) export class AppComponent { value = ''; enterIsPressed(value: string) { this.value = value; } } Send a string A lot simpler code now
  196. blur import { Component } from '@angular/core'; @Component({ selector: 'app-root',

    template: `<input #donaldduck (keyup.enter)="enterIsPressed(donaldduck.value)" placeholder="Write" (blur)="enterIsPressed(donaldduck.value)"> {{value}}` }) export class AppComponent { value = ''; enterIsPressed(value: string) { this.value = value; } } If user is leaving the input without typing enter, do exactly the same
  197. import { Component } from '@angular/core'; @Component({ selector: 'app-root', template:

    `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label> <input type="text" id="name" name="user_name"> </div> <div> <label for="mail">E-mail:</label> <input type="email" id="mail" name="user_mail" required> </div> <div> <button type="submit">Save</button> </div> </fieldset> </form>`, styles: [''] }) export class AppComponent { }
  198. interface Person { name?: string; email: string; } @Component({ selector:

    'app-root', template: `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label> <input type="text" id="name" name="user_name" [(ngModel)]="userInput.name"> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" [ngModel]="userInput.email" required> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>` }) export class AppComponent { userInput: Person = {name: '', email: ''}; get debug() { return JSON.stringify(this.userInput); } fill() { this.userInput.name = 'Kalle'; this.userInput.email = '[email protected]'; } } Simple interface for user input. Name is optional [(ngModel)] => two-way binding [ngModel] => one-way binding email is not visible when user gives input email is visible in the UI
  199. @Component({ selector: 'app-root', template: `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label>

    <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> <div>{{nameElement.className}}</div> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement [(ngModel)]="userInput.email" required> <div>{{emailElement.className}}</div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>` }) Variable the refers to the current <input>-element Display styles (classes) of the input
  200. Tracking Changes State Class if true Class if false The

    control has been visited. ng-touched ng-untouched The control's value has changed. ng-dirty ng-pristine The control's value is valid. ng-valid ng-invalid
  201. @Component({ selector: 'app-root', template: `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label>

    <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.className.includes('ng-valid')"> E-mail is required </div> </div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>` }) Will toggle css hide or show.
  202. @Component({ selector: 'app-root', template: `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label>

    <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.className.includes('ng-valid')"> E-mail is required </div> </div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>`, styles: ['.ng-valid { border-left: 20px solid green; }', '.ng-invalid { border-left: 20px solid red; }'] }) Styling
  203. @Component({ selector: 'app-root', template: `<form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label>

    <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.className.includes('ng-valid')"> E-mail is required </div> </div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>`, styles: ['.ng-valid[required] { border-left: 20px solid green; }', '.ng-invalid:not(form) { border-left: 20px solid red; }'] }) Styling
  204. Modification <form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label> <input type="text" id="name"

    name="user_name" #nameElement [(ngModel)]="userInput.name"> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.className.includes('ng-valid') || emailElement.className.includes('ng-pristine')"> E-mail is required </div> <div><code>{{emailElement.className}}</code></div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form> Now error message is hidden if form is loaded for the first time
  205. Modification <form> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label> <input type="text" id="name"

    name="user_name" #nameElement [(ngModel)]="userInput.name"> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement="ngModel" [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.valid || emailElement.pristine"> E-mail is required </div> <div><code>{{emailElement.className}}</code></div> </div> <div> <button type="submit">Save</button><button (click)="fill()" type="button">Fill</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form> Now emailElement type is NgModel class (directive) And it has several properties
  206. Form Validation <form (ngSubmit)="onSubmit()" #formElement> <fieldset><legend>Person Information</legend> <div> <label for="name">Name:</label>

    <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement="ngModel" [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.valid || emailElement.pristine"> E-mail is required </div> <div><code>{{emailElement.className}}</code></div> </div> <div> <button type="submit">Save</button> </div> <div>User input: <code>{{debug}}</code></div> <div>{{formElement.className}}</div> </fieldset> </form> Will have ng-valid if all of the form items are valid
  207. NgForm and Form Validation <form (ngSubmit)="onSubmit()" #formElement="ngForm"> <fieldset><legend>Person Information</legend> <div>

    <label for="name">Name:</label> <input type="text" id="name" name="user_name" #nameElement [(ngModel)]="userInput.name"> </div> <div> <label for="mail">E-mail:</label> <input type="text" id="mail" name="user_mail" #emailElement="ngModel" [(ngModel)]="userInput.email" required> <div [hidden]="emailElement.valid || emailElement.pristine"> E-mail is required </div> <div><code>{{emailElement.className}}</code></div> </div> <div> <button type="submit" [disabled]="!formElement.form.valid">Save</button> </div> <div>User input: <code>{{debug}}</code></div> </fieldset> </form>`, Will disable and enable the button if whole form is valid or not
  208. Template Driven Forms and Validation • The valid of input

    control is determined by using standard HTML5 • Is valid if text given • <input type="text" ... required> • Is valid if text follows the regex • <input type="text" pattern="[a-z0-9+-]+@[a-z0-9.-]+\.[a-z]{2,3}$" ... required> • Is valid if given 6 chars • <input type="text" minlength="6" maxlength="6" ... required> • See • https://developer.mozilla.org/en- US/docs/Learn/HTML/Forms/Form_validation
  209. Template vs Reactive Template • Easy to use • For

    simple scenarios • Two way databinding • Minimal Component code • Unit testing can be hard Reactive • More flexible • For more complex scenarios • No automatic databinding • More component code, less HTML • Easier unit testing
  210. Module 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 ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } Import the needed module
  211. FormControl import { Component } from '@angular/core'; import { FormControl

    } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <label>Name:</label> <input type="text" [formControl]="name">` }) export class AppComponent { name = new FormControl(); persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; } Link the input to the formcontrol
  212. FormGroup import { Component } from '@angular/core'; import { FormControl,

    FormGroup } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form>` }) export class AppComponent { personForm = new FormGroup({name: new FormControl(), age: new FormControl()}); persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; } Link the the whole form to FormGroup Because input is not standalone, instead of using [formControl] use formControlName
  213. Form Data Model import { Component } from '@angular/core'; import

    { FormControl, FormGroup } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form> <p>Form value: {{ personForm.value | json }}</p>` }) export class AppComponent { personForm = new FormGroup({name: new FormControl(), age: new FormControl()}); persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; } Use .value to get the form data. The json pipe here transforms it to string
  214. FormBuilder import { Component } from '@angular/core'; import { FormControl,

    FormGroup, FormBuilder } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form> <p>Form value: {{ personForm.value | json }}</p>` }) export class AppComponent { persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; // personForm = new FormGroup({name: new FormControl(), age: new FormControl()}); personForm: FormGroup; constructor(private fb: FormBuilder) { this.personForm = fb.group({name: '', age: ''}); } } Use FormBuilder to quickly create the FormGroup
  215. Simple validation import { Component } from '@angular/core'; import {

    FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form> <p>Form value: {{ personForm.value | json }}</p> <p>Form status: {{ personForm.status | json }}</p>` }) export class AppComponent { persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; personForm: FormGroup; constructor(private fb: FormBuilder) { this.personForm = fb.group({name: ['', Validators.required], age: ''}); } } Pass an array of values. First the default value and then a validator Will output valid or invalid depending if name was given.
  216. Using Compose and Several Validators export class AppComponent { persons:

    Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; personForm: FormGroup; constructor(private fb: FormBuilder) { this.personForm = fb.group({name: ['', Validators.required], age: ['', Validators.compose([Validators.required, Validators.min(0), Validators.max(150)])]}); } }
  217. Custom Validator import { Component } from '@angular/core'; import {

    FormControl, FormGroup, FormBuilder, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; function forbiddenNameValidator(control: AbstractControl): ValidationErrors | null { const value = control.value; if (value === 'Jussi') { return {'error': 'wrong name'}; } else { return null; } } Will receive the control If value is wrong, return error object, type is ValidationError which basically means a object with one key value pair and value is string return null if validation was success
  218. Usage export class AppComponent { personForm: FormGroup; constructor(private fb: FormBuilder)

    { this.personForm = fb.group({name: ['', forbiddenNameValidator], age: ['', Validators.compose([Validators.required, Validators.min(0), Validators.max(150)])]}); } }
  219. Arguments function validateNameByRegexValidator(nameRegExp: RegExp) { function forbiddenNameValidator(control: AbstractControl): ValidationErrors |

    null { const value = control.value; if (nameRegExp.test(value)) { return {'error': 'wrong name'}; } else { return null; } } return forbiddenNameValidator; }
  220. Usage export class AppComponent { persons: Person[] = [{name: 'jack',

    age: 30}, {name: 'tina', age: 20}]; personForm: FormGroup; constructor(private fb: FormBuilder) { this.personForm = fb.group({name: ['', validateNameByRegexValidator(/^Jussi$/i)], age: ['', Validators.compose([Validators.required, Validators.min(0), Validators.max(150)])]}); } }
  221. Properties Property Description myControl.value the value of a FormControl. myControl.status

    the validity of a FormControl. Possible values: VALID, INVALID, PENDING, or DISABLED. myControl.pristine true if the user has not changed the value in the UI. Its opposite is myControl.dirty. myControl.untouched true if the control user has not yet entered the HTML control and triggered its blur event. Its opposite is myControl.touched.
  222. @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form

    [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form> <p>name = {{personForm.get('name').value}}</p> <p>name status = {{personForm.get('name').status}}</p> <p>name pristine = {{personForm.get('name').pristine}}</p> <p>name dirty = {{personForm.get('name').dirty}}</p> <p>name untouched = {{personForm.get('name').untouched}}</p> <p>name touched = {{personForm.get('name').touched}}</p>` })
  223. setValue and Mapping class Person { name = ''; age:

    number = null; } export class AppComponent { personForm: FormGroup; userInput: Person = new Person(); constructor(private fb: FormBuilder) { this.personForm = fb.group({name: ['', validateNameByRegexValidator(/^Jussi$/i)], age: ['', Validators.compose([Validators.required, Validators.min(0), Validators.max(150)])]}); this.personForm.setValue({name: this.userInput.name, age: this.userInput.age}); } } Mapping user given values to userInput object
  224. Dynamic Forms • If you have a lot of forms

    that change during the lifecycle of your apps, you could consider dynamic forms • Dynamic forms are just reactive forms that are built dynamically
  225. import { Component, OnInit } from '@angular/core'; import { QuestionBase

    } from './question-base'; import { QuestionControlService } from './question-control-service'; import { FormGroup } from '@angular/forms'; import { TextboxQuestion } from './question-textbox'; @Component({ selector: 'app-root', template: `<form [formGroup]="form"> <div *ngFor="let question of questions"> <app-question [question]="question" [form]="form"></app-question> </div> <button type="submit" [disabled]="!form.valid">Save</button> </form>` }) export class AppComponent implements OnInit { questions: QuestionBase[] = []; form: FormGroup; constructor(private qcs: QuestionControlService) { this.questions.push(new TextboxQuestion({key: 'name', label: 'Name', textboxtype: 'string', required: true}), new TextboxQuestion({key: 'age', label: 'Age', textboxtype: 'number', required: false})); } ngOnInit() { this.form = this.qcs.toFormGroup(this.questions); } } For each question, create form element questions - array Add different questions. Both TextboxQuestion and QuestionBase are classes that one must implement
  226. import { Component, OnInit } from '@angular/core'; import { QuestionBase

    } from './question-base'; import { QuestionControlService } from './question-control-service'; import { FormGroup } from '@angular/forms'; import { TextboxQuestion } from './question-textbox'; @Component({ selector: 'app-root', template: `<form [formGroup]="form"> <div *ngFor="let question of questions"> <app-question [question]="question" [form]="form"></app-question> </div> <button type="submit" [disabled]="!form.valid">Save</button> </form>` }) export class AppComponent implements OnInit { questions: QuestionBase[] = []; form: FormGroup; constructor(private qcs: QuestionControlService) { this.questions.push(new TextboxQuestion({key: 'name', label: 'Name', textboxtype: 'string', required: true}), new TextboxQuestion({key: 'age', label: 'Age', textboxtype: 'number', required: false})); } ngOnInit() { this.form = this.qcs.toFormGroup(this.questions); } } Reactive Form, used for validation Service that transforms the questions into FormGroup Array to FormGroup
  227. QuestionBase // Label to be shown in UI label?: string;

    // textbox, slider, ...? controlType?: string; // key to differentiate the form elements key?: string; // Is it required or not? required?: boolean; TextboxQuestion controlType: string = "textbox" // numeric, text...? textboxtype: string;
  228. export interface Options { // Label to be shown in

    UI label?: string; // textbox, slider, ...? controlType?: string; // key to differentiate the form elements key?: string; // Is it required or not? required?: boolean; } /** * Base class for all the form input elements */ export class QuestionBase { label: string; controlType: string; key: string; required: boolean; constructor(options: Options) { this.label = options.label; this.controlType = options.controlType; this.key = options.key; this.required = options.required; } }
  229. import { QuestionBase, Options } from './question-base'; /** * TextBox

    - element */ export class TextboxQuestion extends QuestionBase { // Will be textbox, overrides the base classes control type controlType = 'textbox'; // Is it numeric, text ..? textboxtype: string; constructor(options: Object) { super(options); this.textboxtype = options['textboxtype']; } }
  230. import { Injectable } from '@angular/core'; import { FormControl, FormGroup,

    Validators } from '@angular/forms'; import { QuestionBase } from './question-base'; /** * Service that transforms questions to FormGroup. */ @Injectable() export class QuestionControlService { /** * Transforms questions-array to FormGroup. * * @param questions list of questions. * @return FormGroup containing the questions. */ toFormGroup(questions: QuestionBase[] ): FormGroup { const group: any = {}; for (const question of questions) { if (question.required) { group[question.key] = new FormControl('', Validators.required); } else { group[question.key] = new FormControl(); } } // return new FormGroup({name: new FormControl(), age: new FormControl()}) return new FormGroup(group); } } Creates on object that contains FormControls Returns the FormGroup
  231. import { Component, OnInit, Input } from '@angular/core'; import {

    FormGroup } from '@angular/forms'; import { TextboxQuestion } from '../question-textbox'; import { QuestionBase } from '../question-base'; import { Form } from '@angular/forms'; @Component({ selector: 'app-question', template: `<div [formGroup]="form"><label>{{question.label}}</label> <input [type]="question.textboxtype" [formControlName]="question.key">{{question.key}}</div>` }) export class QuestionComponent implements OnInit { @Input() question: QuestionBase; @Input() form: FormGroup; constructor() { } ngOnInit() { } } For each question create input. You could do ngSwich here and create different input elements by the type of the question formControlName requires that formGroup is present in the component