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

angular2-slides-v4.pdf

 angular2-slides-v4.pdf

D9e65f4b0af059ae9ba243c8c2265e4f?s=128

Jussi Pohjolainen

November 25, 2020
Tweet

Transcript

  1. Angular - platform Jussi Pohjolainen

  2. Different Libraries React 2 AngularJS Angular 2 -> JavaScript TypeScript

    Vue ... ... TypeScript Recommendation
  3. 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
  4. 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
  5. TypeScript

  6. 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
  7. 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
  8. 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
  9. Let's compile the index.ts to bundle.js

  10. 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>
  11. Watch - mode By running in watch mode (-w) it

    detects changes in ts file and compiles it
  12. 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'
  13. Lab 01 + 02: Install and Hello World

  14. TypeScript types • boolean • number • string • array

    • tuple • enum • any • void • null • undefined • never
  15. Array let list1 : number[] = [1,2,3] let list2 :

    Array<number> = [1,2,3]
  16. Tuple: Define the types in array let tuple: [string, number,

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

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

    1 variable = "hello" let array : any[] = [1, 'hello', true]
  19. 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
  20. never function error() : never { throw new Error("this will

    never return a type") }
  21. let vs var vs const • var • scope either

    global or function • let • block scoping • const • block scoping, cannot change
  22. Lab 03: Basic Types

  23. Functions

  24. Function type function sum(a: number, b: number) : number {

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

    number) => void)) : void { f(a + b) } sum(5,5, result => console.log(result))
  26. 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...
  27. 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...
  28. 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..
  29. Default parameter function buildName(firstName: string, lastName = "Smith") { //

    ... }
  30. 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");
  31. Lab 04: Functions

  32. Keyword this

  33. Outputting global object function doIt() { console.log(this); } doIt();

  34. Restrict that this must be defined tsc --noImplicitThis index.ts The

    type of this must be defined!
  35. Defining type function doIt(this: type) { console.log(this); } doIt();

  36. 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
  37. 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..
  38. Lab 06 + 07 + 08 + 09: this keyword

  39. Interfaces and Classes

  40. 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!
  41. 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)
  42. 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
  43. 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') } }
  44. Visibility • Just like in other languages • public (default)

    • private • protected
  45. 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
  46. 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
  47. 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 } }
  48. 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
  49. Lab 10 + 11

  50. 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
  51. Exporting: person.ts export interface Person { name: string, age: number

    }
  52. Exporting interface Person { name: string, age: number } export

    { Person };
  53. Rename: Exporting interface Person { name: string, age: number }

    export { Person as Henkilo };
  54. Importing import { Person } from "./person"; let jackObject :

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

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

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

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

    export default Person;
  59. Default Export Option 1 interface Person { name: string, age:

    number } export default Person; Option 2 export default interface Person { name: string, age: number }
  60. Default import import Person from "./person"; let jackObject : Person

    = {name: "jack", age: 40};
  61. Code Generation for Modules • Compiler will develop different code

    depending on module target specification • Options: CommonJS, require.js, UMD, SystemJS, ES6
  62. Lab 12: Modules

  63. Angular Quick Start

  64. 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
  65. 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
  66. https://angular.io/guide/architecture

  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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.
  73. 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...
  74. 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
  75. <!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
  76. Lab

  77. Several Components

  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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!
  84. Lab

  85. Displaying Data

  86. 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
  87. 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 ...
  88. Data Binding

  89. 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
  90. 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'; }
  91. Result ngModel is not available by default!

  92. 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 { }
  93. Result

  94. 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
  95. 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
  96. 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
  97. Lab

  98. Component Interaction

  99. 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
  100. 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
  101. 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!
  102. 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
  103. 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
  104. 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
  105. 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
  106. Lifecycle Hooks

  107. Component Lifecycle • constructor • ngOnChanges • If simple input

    changes • ngOnInit • When component is initialized • ngDoCheck • If "complicated" input changes • ngOnDestroy • When component is destroyed
  108. 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
  109. 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
  110. 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
  111. 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'; } }
  112. 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!
  113. 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'; } }
  114. 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!
  115. ngDoCheck() • Detect and act upon changes that Angular can't

    or won't detect of its own.
  116. 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
  117. Lab

  118. CSS

  119. 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'; }
  120. Attribute Directives

  121. Overview • Attribute directives: used in attributes in elements •

    <p doSomething>...</p> • Structural directives: change the structure of the view • ngFor, ngIf ...
  122. 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)
  123. Module import { DosomethingDirective } from './dosomething.directive'; @NgModule({ declarations: [

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

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

    template: '<h1 appDosomething>Hello</h1>', }) export class AppComponent { title = 'app'; } Using the directive
  126. 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
  127. 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
  128. 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) {} }
  129. 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
  130. 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
  131. 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
  132. 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?
  133. Using Alias import { Directive, ElementRef, HostListener, Input } from

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

  135. Structural Directives

  136. 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
  137. 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"
  138. *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!
  139. *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.
  140. *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...
  141. *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, ...
  142. *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 ...
  143. *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
  144. Animations

  145. 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
  146. 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
  147. 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
  148. 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
  149. 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
  150. 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
  151. 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"..
  152. 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
  153. 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
  154. 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
  155. 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
  156. @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
  157. Using wildcards * • Match any state using * •

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

    element enters the view use • void => * • When element leaves the view use • * => void
  159. @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...
  160. @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
  161. @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)
  162. End Result

  163. Routing

  164. 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)
  165. 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
  166. 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
  167. @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
  168. wild card const appRoutes: Routes = [ { path: 'add',

    component: AddComponent }, { path: 'view', component: ViewComponent }, { path: '**', component: NotfoundComponent } ]; If any other url, then..
  169. 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
  170. 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
  171. 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
  172. 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
  173. 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
  174. Lab

  175. More Modules

  176. 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
  177. http://localhost:4200/view

  178. http://localhost:4200/view/3

  179. 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
  180. 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
  181. 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!
  182. 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
  183. 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
  184. 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; }); }); } }
  185. 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
  186. 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
  187. 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
  188. 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
  189. Child Routing

  190. Example modify/ and view/ modify/add and modify/delete

  191. 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
  192. 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
  193. 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
  194. Route Guards

  195. 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
  196. 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
  197. 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
  198. Pipes

  199. Pipes • To display value transformations use pipes • Built-in

    pipes • currency, date, percent, uppercase, lowercase ... • Usage • {{ something | pipe }} • For example • {{ title | uppercase }}
  200. 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
  201. 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
  202. 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'
  203. 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
  204. 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 { }
  205. 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
  206. Services

  207. 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.
  208. 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
  209. 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.
  210. 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
  211. 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
  212. Lab

  213. HttpClient Consuming Real Data

  214. 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
  215. 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
  216. 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
  217. 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
  218. 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", ...
  219. 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
  220. ItemResponse Interface import { Planet } from './planet' export interface

    ItemResponse { count: number; next: string; previous: string; results: [Planet] } Planet - array
  221. Planet Interface export interface Planet { name: string; diameter: number;

    population: number; }
  222. 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>
  223. 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) => {})
  224. None
  225. 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
  226. 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
  227. 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
  228. 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!
  229. 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...
  230. 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
  231. 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
  232. 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!
  233. Angular Material Components

  234. 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
  235. Angular Material • Material Design Components for Angular • https://material.angular.io

    • Fast and Consistent • Versatile • Optimized for Angular
  236. 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">
  237. 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
  238. 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>
  239. Angular Forms in Detail

  240. 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
  241. $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
  242. $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>..
  243. $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
  244. 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
  245. 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..
  246. 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
  247. 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
  248. 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
  249. 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
  250. Forms Template Driven and Reactive Forms

  251. Template Driven Forms

  252. 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 { }
  253. 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 = 'kalle@tamk.fi'; } } 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
  254. @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
  255. 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
  256. Result Allowing empty input This was required

  257. @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.
  258. @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
  259. Result

  260. @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
  261. Result

  262. Result: when writing the email

  263. Result Error message is displayed when opening the app

  264. 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
  265. From Documentation

  266. 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
  267. 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
  268. NgForm

  269. 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
  270. Validation

  271. 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
  272. Reactive Forms

  273. 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
  274. 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
  275. 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
  276. 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
  277. 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
  278. 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
  279. 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.
  280. Standard Validators

  281. 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)])]}); } }
  282. 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
  283. 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)])]}); } }
  284. 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; }
  285. 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)])]}); } }
  286. 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.
  287. @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>` })
  288. 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
  289. Dynamic Forms

  290. 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
  291. 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
  292. 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
  293. 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;
  294. 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; } }
  295. 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']; } }
  296. 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
  297. 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