Slide 1

Slide 1 text

Introduction to Angular TimePlan Software

Slide 2

Slide 2 text

Lars Gyrup Brink Nielsen Co-organizer of AarhusJS Co-founder of This is Learning Open-source maintainer Published author GitHub Star Microsoft MVP Nx Champion Angular Hero of Education

Slide 3

Slide 3 text

Workshop modules 1. Local environment setup for Angular 2. Fundamentals of Angular declarables 3. Code lab 1 4. Angular service essentials 5. Code lab 2 6. Angular state management 7. Automated testing of Angular applications

Slide 4

Slide 4 text

Resources • Slides available at https://speakerdeck.com/layzee • Sample application solution available at https://github.com/LayZeeDK/ introduction-to-angular • Angular documentation available at https://angular.dev

Slide 5

Slide 5 text

Local environment setup for Angular

Slide 6

Slide 6 text

Node.js Node.js is a cross-platform JavaScript runtime using the V8 (Chromium) JavaScript engine. While mostly used for web servers, it is also used for JavaScript development tools, including the Angular CLI as well as the TypeScript and Angular compilers. Download Node.js from https://nodejs.org. Make sure to download and install a major version supported by the major version of Angular you are using. Refer to https://angular.dev/reference/versions. Use the node -v command to verify the version installed.

Slide 7

Slide 7 text

npm npm refers to: • A CLI-based package manager used to install versioned JavaScript packages from a private or public JavaScript package registry • The JavaScript package registry at https://registry.npmjs.com • The GitHub-owned npm, Inc. company that maintains the npm products • The JavaScript package explorer at https://npmjs.com The npm CLI is distributed with Node.js. NPM was originally an abbreviation of Node Package Manager. Use the npm -v command to verify the version installed.

Slide 8

Slide 8 text

Yarn Yarn is a CLI-based package manager compatible with JavaScript package registries. Yarn Classic refers to versions 1.x. It has a lax module resolution algorithm but is significantly slower than modern alternatives. Install the Yarn CLI using the npm CLI: npm install -g yarn or for Yarn Classic: npm install -g yarn@1 Use the yarn -v command to verify the version installed.

Slide 9

Slide 9 text

Angular CLI The Angular CLI is a Node.js-based development tool for Angular projects. Its main features include: • Code generation • Codemods and code migrations • Development web server • Task runner • Angular compilation Install the Angular CLI using the npm CLI: npm install -g @angular/cli or using the Yarn Classic CLI: yarn global add @angular/cli Use the ng version command to verify the version installed.

Slide 10

Slide 10 text

Visual Studio Code Visual Studio Code (VS Code) is a free cross-platform code editor by Microsoft. Angular CLI generates a workspace with recommended VS Code extensions. Download VS Code from https://code.visualstudio.com. Install the following VS Code extensions: • Angular Language Service • EditorConfig for VS Code • Error Lens • ESLint • GitLens (optional) • Prettier (optional)

Slide 11

Slide 11 text

Web browser Any desktop web browser with developer tools (press F12) will do. • Arc • Brave • Google Chrome • Microsoft Edge • Mozilla Firefox • Opera • Safari* • Vivaldi* *No Angular DevTools[1][2] web extension support.

Slide 12

Slide 12 text

Ex.1: New Angular workspace 1. Generate an Angular workspace ng new introduction-to-angular 2. Run the development server ng serve Or npm start Or yarn start 3. Open the Angular application http://localhost:4200 Or press O then Enter in the terminal

Slide 13

Slide 13 text

Ex.2: Configure the Angular CLI 1. Open angular.json 2. Add the following setting to display generated components as block HTML elements { "schematics": { "@schematics/angular:component": { "displayBlock": true } } }

Slide 14

Slide 14 text

Ex.3: Configure Prettier for Angular { "overrides": [ { "files": "*.html", "options": { "parser": "html" } }, { "files": "*.component.html", "options": { "parser": "angular" } } ] } 1. Create a .prettierrc JSON file in the root of the workspace 2. Add this configuration to support formatting Angular component templates with Prettier 3. Enable the Editor: Format On Save setting in VS Code Or use the Format Document command in VS Code

Slide 15

Slide 15 text

Ex.4: Add a format task { "name": "introduction-to-angular", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "format": "prettier --write ." }, "private": true, "dependencies": { "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", "@angular/compiler": "^19.0.0", "@angular/core": "^19.0.0", "@angular/forms": "^19.0.0", "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, "devDependencies": { "@angular-devkit/build-angular": "^19.0.1", "@angular/cli": "^19.0.1", "@angular/compiler-cli": "^19.0.0", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.4.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", 1. Add a format task to the scripts object in package.json 2. Specify the command prettier --write . 3. Run the task using the command npm run format Or yarn format

Slide 16

Slide 16 text

Fundamentals of Angular declarables

Slide 17

Slide 17 text

Angular component parts /- hello-world.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css' }) export class HelloWorldComponent {} <- hello-world.component.html -->

hello-world works!<-p> /- hello-world.component.css *- :host { display: block; } Generate your first component using the command ng generate component hello- world

Slide 18

Slide 18 text

Angular component parts /- hello-world.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css' }) export class HelloWorldComponent {} <- hello-world.component.html -->

hello-world works!<-p> /- hello-world.component.css *- :host { display: block; } An Angular component consist of: • A TypeScript class that represents the code-behind component model connecting user input to the application and application state to the user • A component template which represents the graphical user interface as HTML-/JavaScript-inspired Angular template syntax and is bound to the component model • A component stylesheet containing component-specific styles using CSS These parts can be inline in a single file or spread between two or more separate files.

Slide 19

Slide 19 text

Using a component /- app.component.ts import { Component } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent {} <- app.component.html -->

Welcome<-h1>

Slide 20

Slide 20 text

Component state /- hello-world.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { name = 'World'; } <- hello-world.component.html -->

Hello, {{ name }}!<-p> Component state is represented by properties on the component model. Component state is displayed to the user using template expressions, for example {{ name }}

Slide 21

Slide 21 text

Input properties /- hello-world.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; } A component can accept data from other components via properties that are marked with an @Input() decorator.

Slide 22

Slide 22 text

Input properties /- hello-world.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; } <- app.component.html -->

Welcome<-h1>

Slide 23

Slide 23 text

Output properties /- hello-world.component.ts import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'app-hello-world', imports: [], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; @Output() messageSent = new EventEmitter(); } <- hello-world.component.html -->

Hello, {{ name }}!<-p> Send message<-button> A component can output data to other components via properties that are marked with an @Output() decorator.

Slide 24

Slide 24 text

Output properties /- app.component.ts import { Component } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { message = ''; } <- app.component.html -->

Welcome<-h1> Received: {{ message }}<-p> Parent components tap into child component output properties via event listeners.

Slide 25

Slide 25 text

Template control flow <- app.component.html --> Received: {{ message }}<-p> } Angular has built-in template blocks for control flow. Conditionals use @if, @else-if, and @else blocks.

Slide 26

Slide 26 text

Template control flow /- app.component.ts import { Component } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; import { TodayComponent } from './today/today.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent, MessageListComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { receivedMessages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } } <- app.component.html -->

Welcome<-h1> Received messages<-h2>

Slide 27

Slide 27 text

Template control flow /- message-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-message-list', imports: [], templateUrl: './message-list.component.html', styleUrl: './message-list.component.css', }) export class MessageListComponent { @Input() messages: readonly string[] = []; } <- message-list.component.html --> @for (message of messages; track $index) {

Message #{{ $index + 1 }}: {{ message }}<-p> } @empty {

No messages received.<-em><-p> } Angular has built-in template blocks for control flow. Loops use @for and @empty blocks.

Slide 28

Slide 28 text

Control flow /- today.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-today', imports: [], templateUrl: './today.component.html', styleUrl: './today.component.css', }) export class TodayComponent { today = new Date(); } Angular has built-in template blocks for control flow. Switch statements use @switch, @case , and @default blocks.

Slide 29

Slide 29 text

Control flow <- today.component.html -->

Today is @switch (today.getDay()) { @case (0) { Sunday } @case (1) { Monday } @case (2) { Tuesday } @case (3) { Wednesday } @case (4) { Thursday } @case (5) { Friday } @case (6) { Saturday } @default { a new day } } <-p> Angular has built-in template blocks for control flow. Switch statements use @switch, @case , and @default blocks.

Slide 30

Slide 30 text

Control flow /- app.component.ts import { Component } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; import { MessageListComponent } from './message-list/message-list.component'; import { TodayComponent } from './today/today.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent, MessageListComponent, TodayComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { messages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } } <- app.component.html --> Today<-h2> Received messages<-h2>

Slide 31

Slide 31 text

Angular pipes /- today.component.ts import { DatePipe } from '@angular/common'; import { Component } from '@angular/core'; @Component({ selector: 'app-today', imports: [DatePipe], templateUrl: './today.component.html', styleUrl: './today.component.css', }) export class TodayComponent { now = new Date(); refreshTime() { this.now = new Date(); } } Component templates can use Angular pipes to process values, for example to format a date-time. 1. In the component model, add the pipe class to Component.imports

Slide 32

Slide 32 text

Angular pipes <- today.component.html -->

Today is @switch (now.getDay()) { @case (0) { Sunday } @case (1) { Monday } @case (2) { Tuesday } @case (3) { Wednesday } @case (4) { Thursday } @case (5) { Friday } @case (6) { Saturday } @default { a new day } } and the time is {{ now | date }} Refresh time<-button> <-p> Component templates can use Angular pipes to process values, for example to format a date-time. 1. In the component model, add the pipe class to Component.imports 2. In the template, add a pipe (|) operator followed by the pipe’s name

Slide 33

Slide 33 text

Angular pipes <- today.component.html -->

Today is @switch (now.getDay()) { @case (0) { Sunday } @case (1) { Monday } @case (2) { Tuesday } @case (3) { Wednesday } @case (4) { Thursday } @case (5) { Friday } @case (6) { Saturday } @default { a new day } } and the time is {{ now | date: "mediumTime" }} Refresh time<-button> <-p> Component templates can use Angular pipes to process values, for example to format a date-time. 1. In the component model, add the pipe class to Component.imports 2. In the template, add a pipe (|) operator followed by the pipe’s name 3. Optionally, add pipe parameters separated by colon (:)

Slide 34

Slide 34 text

Custom Angular pipes /- weekday.pipe.ts import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'weekday', }) export class WeekdayPipe implements PipeTransform { transform(value: unknown, .--args: unknown[]): unknown { return null; } } Generate your first Angular pipe using the command ng generate pipe weekday

Slide 35

Slide 35 text

Custom Angular pipes /- weekday.pipe.ts import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'weekday', }) export class WeekdayPipe implements PipeTransform { transform(value: Date) { let weekday = ''; switch (value.getDay()) { case 0: weekday = 'Sunday'; break; case 1: weekday = 'Monday'; break; case 2: weekday = 'Tuesday'; break; case 3: weekday = 'Wednesday'; break; case 4: weekday = 'Thursday'; break; case 5: weekday = 'Friday'; break; case 6: weekday = 'Saturday'; break; default: weekday = 'a new day'; break; } return weekday; } } Generate your first Angular pipe using the command ng generate pipe weekday For this example, we move the weekday switch statement from today.component.html to WeekdayPipe.transform and convert it to JavaScript.

Slide 36

Slide 36 text

Custom Angular pipes /- today.component.ts import { DatePipe } from '@angular/common'; import { Component } from '@angular/core'; import { WeekdayPipe } from '.-/weekday.pipe'; @Component({ selector: 'app-today', imports: [DatePipe, WeekdayPipe], templateUrl: './today.component.html', styleUrl: './today.component.css', }) export class TodayComponent { now = new Date(); refreshTime() { this.now = new Date(); } } To use the custom pipe: 1. In today.component.ts, add WeekdayPipe to Component.imports

Slide 37

Slide 37 text

Custom Angular pipes /- today.component.ts import { DatePipe } from '@angular/common'; import { Component } from '@angular/core'; import { WeekdayPipe } from '.-/weekday.pipe'; @Component({ selector: 'app-today', imports: [DatePipe, WeekdayPipe], templateUrl: './today.component.html', styleUrl: './today.component.css', }) export class TodayComponent { now = new Date(); refreshTime() { this.now = new Date(); } } <- today.component.html -->

Today is {{ now | weekday }} and the time is {{ now | date: "mediumTime" }} Refresh time<-button> <-p> To use the custom pipe: 1. In today.component.ts, add WeekdayPipe to Component.imports 2. In today.component.html, add {{ now | weekday }}

Slide 38

Slide 38 text

Angular directives Angular directives are like Angular components without styles and templates. • They have input properties • They have output properties • They can access their host DOM element • They have host listeners and host bindings • They have lifecycle hooks • They usually have HTML attribute selectors instead of HTML element selectors An Angular directive is commonly used to manipulate the Document Object Model (DOM), that is HTML elements in the browser.

Slide 39

Slide 39 text

Custom Angular directives /- font-size.directive.ts import { Directive } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { constructor() {} } Generate your first Angular directive using the command ng generate directive font- size

Slide 40

Slide 40 text

Custom Angular directives /- font-size.directive.ts import { Directive, HostBinding, Input } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { @Input() appFontSize; } Add an input property named the same as the selector, in this case appFontSize, to accept an input value.

Slide 41

Slide 41 text

Custom Angular directives /- font-size.directive.ts import { Directive, HostBinding, Input } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { @Input() appFontSize: number; } Annotate the expected type, in this case number.

Slide 42

Slide 42 text

Custom Angular directives /- font-size.directive.ts import { Directive, HostBinding, Input } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { @Input({ required: true }) appFontSize! number; } Add the required: true setting to the @Input decorator and use the non-null assertion operator (!) to prevent errors.

Slide 43

Slide 43 text

Custom Angular directives /- font-size.directive.ts import { Directive, HostBinding, Input } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { @Input({ required: true }) @HostBinding('style.fontSize.px') appFontSize!- number; } Use a @HostBinding() decorator to change properties of the host DOM element, for example style.fontSize.

Slide 44

Slide 44 text

Custom Angular directives /- font-size.directive.ts import { Directive, HostBinding, Input } from '@angular/core'; @Directive({ selector: '[appFontSize]', }) export class FontSizeDirective { @Input({ required: true }) @HostBinding('style.fontSize.px') appFontSize!- number; } .px is a special unit-specifying suffix for style property bindings.

Slide 45

Slide 45 text

Using Angular directives /- hello-world.component.ts import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FontSizeDirective } from '.-/font-size.directive'; @Component({ selector: 'app-hello-world', imports: [FontSizeDirective], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; @Output() messageSent = new EventEmitter(); } To use a directive: 1. Add the directive class to Component.imports.

Slide 46

Slide 46 text

Using Angular directives /- hello-world.component.ts import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FontSizeDirective } from '.-/font-size.directive'; @Component({ selector: 'app-hello-world', imports: [FontSizeDirective], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; @Output() messageSent = new EventEmitter(); } <- hello-world.component.html -->

Hello, {{ name }}!<-p> Send message<-button> To use a directive: 1. Add the directive class to Component.imports. 2. Use the attribute selector on a DOM element in the component template.

Slide 47

Slide 47 text

Using Angular directives /- hello-world.component.ts import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FontSizeDirective } from '.-/font-size.directive'; @Component({ selector: 'app-hello-world', imports: [FontSizeDirective], templateUrl: './hello-world.component.html', styleUrl: './hello-world.component.css', }) export class HelloWorldComponent { @Input() name = 'World'; @Output() messageSent = new EventEmitter(); } <- hello-world.component.html -->

Hello, {{ name }}!<-p> Send message<-button> To use a directive: 1. Add the directive class to Component.imports. 2. Use the attribute selector on a DOM element in the component template. 3. Optionally bind input properties and/or output properties.

Slide 48

Slide 48 text

Angular component styles /- hello-world.component.css *- :host { display: block; } button { background-color: purple; color: white; border: none; &:hover { background-color: rebeccapurple; } &:active { background-color: blueviolet; } } <- hello-world.component.html -->

Hello, {{ name }}!<-p> Send message<-button> Component styles only apply to DOM elements mentioned in the component template. A simple selector like button is often enough to target a DOM element. In this case, there is no need to add HTML/CSS classes.

Slide 49

Slide 49 text

Code lab 1

Slide 50

Slide 50 text

Code lab 1 Create a simple Angular application, for example: • A counter • A calculator • A todo list Or follow these slides to create the sample application. Available at https://speakerdeck.com/layzee.

Slide 51

Slide 51 text

Angular service essentials

Slide 52

Slide 52 text

Angular services /- message.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MessageService { constructor() {} } Angular services are usually class-based. Generate your first Angular service using the command ng generate service message

Slide 53

Slide 53 text

Angular services /- message.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MessageService { constructor() {} } Singleton Angular services are provided by passing the providedIn: 'root' option to the @Injectable decorator.

Slide 54

Slide 54 text

Angular services /- app.component.ts import { Component, inject } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; import { MessageListComponent } from './message-list/message-list.component'; import { MessageService } from './message.service'; import { TodayComponent } from './today/today.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent, TodayComponent, MessageListComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { messageService = inject(MessageService); messages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } } A class-based Angular service is injected into a component, directive, pipe, or service by passing the service class to the inject function.

Slide 55

Slide 55 text

Angular services /- message.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MessageService { messages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } } /- app.component.ts import { Component, inject } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; import { MessageListComponent } from './message-list/message-list.component'; import { MessageService } from './message.service'; import { TodayComponent } from './today/today.component'; @Component({ selector: 'app-root', imports: [HelloWorldComponent, TodayComponent, MessageListComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { messageService = inject(MessageService); } Extract component state from AppComponent to MessageService.

Slide 56

Slide 56 text

Angular services <- app.component.html --> Today<-h2> Received messages<-h2>

Slide 57

Slide 57 text

Angular HttpClient For HTTP requests and responses, Angular offers the HttpClient service.

Slide 58

Slide 58 text

Providing the HttpClient /- app.config.ts import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ], }; To use the HttpClient service, first provide it in your AppConfig by using the provideHttpClient provider function.

Slide 59

Slide 59 text

Providing the HttpClient /- app.config.ts import { provideHttpClient, withFetch } from '@angular/common/http'; import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withFetch()), ], }; It’s recommended to use the modern fetch API instead of the classic XMLHttpRequest API as the HTTP engine for the HttpClient service. To do this, pass the withFetch function to provideHttpClient.

Slide 60

Slide 60 text

Using the HttpClient /- joke.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent {} Generate a joke component using the command ng generate component joke

Slide 61

Slide 61 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); } Inject the HttpClient by using the inject function.

Slide 62

Slide 62 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); } Inject the HttpClient by using the inject function.

Slide 63

Slide 63 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); } Call the HttpClient.get method to prepare a GET HTTP request.

Slide 64

Slide 64 text

Using the HttpClient /- joke-response.ts export interface JokeResponse {} Generate a joke response interface using the command ng generate interface joke- response

Slide 65

Slide 65 text

Using the HttpClient /- joke-response.ts export interface JokeResponse { readonly error: boolean; readonly category: string; readonly type: string; readonly joke: string; readonly flags: Record; readonly id: string; readonly safe: boolean; readonly lang: string; } Add properties to JokeResponse. These are found at https://sv443.net/jokeapi/v2/.

Slide 66

Slide 66 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); } Add JokeResponse as the type parameter for the HttpClient.get method. This specifies the type of the HTTP response body when parsed as JSON.

Slide 67

Slide 67 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); joke?- string; } Add an optional joke property of type string to JokeComponent.

Slide 68

Slide 68 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); joke?- string; constructor() {} } Add a constructor to JokeComponent.

Slide 69

Slide 69 text

Using the HttpClient /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); joke?- string; constructor() { this.jokeRequest$.subscribe((response) => { this.joke = response.joke; }); } } In the constructor, call the subscribe method on JokeComponent.jokeRequest$. Pass a callback that accepts the response and assigns JokeResponse.joke to JokeComponent.joke.

Slide 70

Slide 70 text

Using the HttpClient <- joke.component.html --> @if (joke) {

{{ joke }}<-p> } @else {

Loading joke.--<-em><-p> } Add these template blocks to joke.component.html.

Slide 71

Slide 71 text

Using the HttpClient /- app.component.ts import { Component, inject } from '@angular/core'; import { HelloWorldComponent } from './hello-world/hello-world.component'; import { JokeComponent } from './joke/joke.component'; import { MessageListComponent } from './message-list/message-list.component'; import { MessageService } from './message.service'; import { TodayComponent } from './today/today.component'; @Component({ selector: 'app-root', imports: [ HelloWorldComponent, TodayComponent, MessageListComponent, JokeComponent, ], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { messageService = inject(MessageService); } Add JokeComponent to Component.imports in app.component.ts.

Slide 72

Slide 72 text

Using the HttpClient <- app.component.html --> Today<-h2> Joke<-h2> Received messages<-h2> in app.component.html.

Slide 73

Slide 73 text

Angular Router The Angular Router is used to activate components based on the current URL.

Slide 74

Slide 74 text

Angular Router Split our application into pages, each matching a different URL. Generate page components using the following commands ng generate component hello- page ng generate component today- page ng generate component joke- page ng generate component message- page

Slide 75

Slide 75 text

Angular Router /- hello-page.compoennt.ts import { Component, inject } from '@angular/core'; import { HelloWorldComponent } from '.-/hello-world/hello-world.component'; import { MessageService } from '.-/message.service'; @Component({ selector: 'app-hello-page', imports: [HelloWorldComponent], templateUrl: './hello-page.component.html', styleUrl: './hello-page.component.css', }) export class HelloPageComponent { messageService = inject(MessageService); } <- hello-page.component.ts -->

Slide 76

Slide 76 text

Angular Router /- today-page.component.ts import { Component } from '@angular/core'; import { TodayComponent } from '.-/today/today.component'; @Component({ selector: 'app-today-page', imports: [TodayComponent], templateUrl: './today-page.component.html', styleUrl: './today-page.component.css', }) export class TodayPageComponent {} <- today-page.component.html -->

Today<-h2>

Slide 77

Slide 77 text

Angular Router /- message-page.component.ts import { Component, inject } from '@angular/core'; import { MessageListComponent } from '.-/message-list/message-list.component'; import { MessageService } from '.-/message.service'; @Component({ selector: 'app-message-page', imports: [MessageListComponent], templateUrl: './message-page.component.html', styleUrl: './message-page.component.css', }) export class MessagePageComponent { messageService = inject(MessageService); } <- message-page.component.html -->

Received messages<-h2>

Slide 78

Slide 78 text

Angular Router /- app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', imports: [], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent {} <- app.component.html --> AppComponent should now be empty.

Slide 79

Slide 79 text

Using Angular routes /- app.component.ts import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent {} <- app.component.html --> to app.component.html.

Slide 80

Slide 80 text

Using Angular routes import { Routes } from '@angular/router'; export const routes: Routes = []; Open app.routes.ts.

Slide 81

Slide 81 text

Configuring Angular routes import { Routes } from '@angular/router'; import { HelloPageComponent } from './hello-page/hello-page.component'; import { JokePageComponent } from './joke-page/joke-page.component'; import { MessagePageComponent } from './message-page/message-page.component'; import { TodayPageComponent } from './today-page/today-page.component'; export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'hello' }, { title: 'Hello', path: 'hello', component: HelloPageComponent }, { title: 'Joke', path: 'joke', component: JokePageComponent }, { title: 'Messages', path: 'messages', component: MessagePageComponent }, { title: 'Today', path: 'today', component: TodayPageComponent }, ]; Configure these routes.

Slide 82

Slide 82 text

Angular Router The application should now navigate to the hello URL path and display the HelloPageComponent.

Slide 83

Slide 83 text

Navigation links /- navigation.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-navigation', imports: [], templateUrl: './navigation.component.html', styleUrl: './navigation.component.css', }) export class NavigationComponent {} Generate a navigation component using the command ng generate component navigation

Slide 84

Slide 84 text

Navigation links /- navigation.component.ts import { Component } from '@angular/core'; import { routes } from '.-/app.routes'; @Component({ selector: 'app-navigation', imports: [], templateUrl: './navigation.component.html', styleUrl: './navigation.component.css', }) export class NavigationComponent { routes = routes.filter((route) => route.path !-- ''); } Filter out the default route from the routes constant and assign the rest to a NavigationComponent.routes property.

Slide 85

Slide 85 text

Navigation links /- navigation.component.ts import { Component } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { routes } from '.-/app.routes'; @Component({ selector: 'app-navigation', imports: [RouterLink, RouterLinkActive], templateUrl: './navigation.component.html', styleUrl: './navigation.component.css', }) export class NavigationComponent { routes = routes.filter((route) => route.path !-- ''); } Add the RouterLink and RouterLinkActive directives to Component.imports in navigation.component.ts.

Slide 86

Slide 86 text

Navigation links <- navigation.component.html -->

Slide 87

Slide 87 text

Navigation links /- navigation.component.css *- :host { display: block; } .is-active { font-weight: bold; } <- navigation.component.html -->

Slide 88

Slide 88 text

Navigation links /- app.component.ts import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { NavigationComponent } from './navigation/navigation.component'; @Component({ selector: 'app-root', imports: [RouterOutlet, NavigationComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent {} <- app.component.html --> in app.component.html.

Slide 89

Slide 89 text

Navigation links A navigation menu should be displayed at the top of the page. The active page should be highlighted in the navigation menu.

Slide 90

Slide 90 text

Programmating navigation Use the Router service for programmatic navigation.

Slide 91

Slide 91 text

Programmating navigation /- hello-page.compoennt.ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; import { HelloWorldComponent } from '.-/hello-world/hello-world.component'; import { MessageService } from '.-/message.service'; @Component({ selector: 'app-hello-page', imports: [HelloWorldComponent], templateUrl: './hello-page.component.html', styleUrl: './hello-page.component.css', }) export class HelloPageComponent { messageService = inject(MessageService); router = inject(Router); } Inject the Router service in HelloPageComponent.

Slide 92

Slide 92 text

Programmating navigation <- hello-page.component.ts --> Open messages<-button> Call the Router.navigateByUrl method in hello- page.component.html.

Slide 93

Slide 93 text

Code lab 2

Slide 94

Slide 94 text

Code lab 2 Create a simple Angular application that loads data from a web API, for example • JokeAPI (https://sv443.net/jokeapi/v2/) • PokeAPI (https://pokeapi.co/) • TODO API (https://api.nstack.in/) • JSONPlaceholder (https://jsonplaceholder.typicode.com/) Or follow these slides to continue the sample application. Available at https://speakerdeck.com/layzee.

Slide 95

Slide 95 text

Angular state management

Slide 96

Slide 96 text

RxJS RxJS (Reactive Extensions for JavaScript) is a library for asynchronous programming. RxJS extends The Observer Pattern and includes Observable, operators, schedulers, as well as Subject and friends. Similar libraries are available for other programming languages, platforms, and frameworks, for example: • RxJava • Rx.NET • RxPHP

Slide 97

Slide 97 text

RxJS Observable An RxJS Observable represents a stream of events. Most observables are lazy. They do nothing until they are observed. import { from, Observable } from 'rxjs'; const name$: Observable = from(['Ella', 'Noah', 'Lily']);

Slide 98

Slide 98 text

RxJS Observable Pass a callback to an Observable.subscribe method to observe its next notification(s). import { from, Observable } from 'rxjs'; const name$: Observable = from(['Ella', 'Noah', 'Lily']); name$.subscribe((name) => console.log(name)); /- -> "Ella" /- -> "Noah" /- -> "Lily"

Slide 99

Slide 99 text

RxJS Observable An observable can emit three different types of notifications. • Zero to many next notifications with a data value • Zero to one error notification with an error value • Zero to one complete notification with no value

Slide 100

Slide 100 text

RxJS Observable An observable may end by emitting an error notification or a complete notification. An observable may end without emitting any next notifications.

Slide 101

Slide 101 text

RxJS Observable Pass an observer object to an Observable.subscribe method to subscribe to one or more types of notifications. import { from, Observable } from 'rxjs'; const name$: Observable = from(['Ella', 'Noah', 'Lily']); name$.subscribe({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission') }); /- -> "Ella" /- -> "Noah" /- -> "Lily" /- -> "End of transmission"

Slide 102

Slide 102 text

RxJS operators RxJS 7.8 includes 90 operators. Operators are functions that can operate on the notifications, values, or timing of an obsevable.

Slide 103

Slide 103 text

RxJS operators Zero to many operators can be passed to an Observable.pipe method.

Slide 104

Slide 104 text

The map operator A common operator is map. Its callback is passed the value of every next notification. Its return value is the replacement value for this next notification as observed by subscribers. import { from, map, Observable } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), ); name$.subscribe({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> { name: "Lily", firstLetter: "L" } /- -> "End of transmission” /- name.ts export interface Name { readonly name: string; readonly firstLetter: string; }

Slide 105

Slide 105 text

The filter operator Another common operator is filter. Its callback is passed the value of every next notification. When it returns true, the next notification is forwarded to subscribers. When it returns false, the next notification is ignored and never observed by subscribers. import { filter, from, map, Observable } from 'rxjs’; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), ); name$.subscribe({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 106

Slide 106 text

The tap operator The tap operator represents side effects triggered by observable notifications. We could and should place our browser console side effects in a tap operation. Similar to the Observable.subscribe method, the tap operator accepts either: • A single callback for next notifications • An observer object with one or more notification handlers import { filter, from, map, Observable, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }) ); name$.subscribe(); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 107

Slide 107 text

Subscriptions Now that we haven’t passed any arguments to the Observable.subscribe method, what’s its purpose? While the tap operator describes side effects, they are not activated until we subscribe to the observable. import { filter, from, map, Observable, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }) ); name$.subscribe(); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 108

Slide 108 text

Subscriptions Additionally, the Observable.subscribe method returns a Subscription. import { filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap((value) => {}) ); const subscription: Subscription = name$.subscribe(); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 109

Slide 109 text

Subscriptions Additionally, the Observable.subscribe method returns a Subscription. import { filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap((value) => {}) ); const subscription: Subscription = name$.subscribe(); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 110

Slide 110 text

Subscriptions At any time, we can cancel a subscription by calling its Subscription.unsubscribe method. In this example, it has no effect since the observable emits its next notifications synchronously and we observe their values synchronously. import { filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap((value) => {}) ); const subscription: Subscription = name$.subscribe(); subscription.unsubscribe(); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 111

Slide 111 text

Asynchronous observables Convert our observable to an asynchronous observable by using the delay operator. In this case, no values are output because we unsubscribe before 100 milliseconds have passed. import { delay, filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( delay(100), map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }), ); const subscription: Subscription = name$.subscribe(); subscription.unsubscribe(); /- (No output)

Slide 112

Slide 112 text

Asynchronous observables If we unsubscribe after 100 milliseconds have passed, the next notification values are still output in the browser console. import { delay, filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily']).pipe( delay(100), map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }), ); const subscription: Subscription = name$.subscribe(); setTimeout(() => { subscription.unsubscribe(); }, 200); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 113

Slide 113 text

Distinct notifications To prevent duplicate next notifications from being emitted, we can use for the distinctUntilChanged operator. In this case, we still receive the duplicate value because the operator compares by reference by default. The two 'Ella' strings are mapped to Name data structures by the map operator, each a new object reference so they both pass the distinctUntilChanged operator. import { delay, distinctUntilChanged, filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Ella', 'Noah', 'Lily']).pipe( delay(100), map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), distinctUntilChanged(), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }), ); const subscription: Subscription = name$.subscribe(); setTimeout(() => { subscription.unsubscribe(); }, 200); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 114

Slide 114 text

Distinct notifications If we place the distinctUntilChanged operator before the map operator in the observable pipeline, only one Ella name is emitted. import { delay, distinctUntilChanged, filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Ella', 'Noah', 'Lily']).pipe( delay(100), distinctUntilChanged(), map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }) ); const subscription: Subscription = name$.subscribe(); setTimeout(() => { subscription.unsubscribe(); }, 200); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> "End of transmission"

Slide 115

Slide 115 text

Distinct notifications Notice, however, that only the previous next notification’s value is considered when determining whether a next notification is distinct. import { delay, distinctUntilChanged, filter, from, map, Observable, Subscription, tap } from 'rxjs'; import { Name } from './name'; const name$: Observable = from(['Ella', 'Noah', 'Lily', 'Ella']).pipe( delay(100), distinctUntilChanged(), map((name) => ({ name, firstLetter: name[0], })), filter((name) => name.firstLetter !-- 'L'), tap({ next: (name) => console.log(name), error: (error) => console.error(error), complete: () => console.log('End of transmission'), }) ); const subscription: Subscription = name$.subscribe(); setTimeout(() => { subscription.unsubscribe(); }, 200); /- -> { name: "Ella", firstLetter: "E" } /- -> { name: "Noah", firstLetter: "N" } /- -> { name: "Ella", firstLetter: "E" } /- -> "End of transmission"

Slide 116

Slide 116 text

Angular observables We already created an Observable in our Angular application. The HttpClient.get method returned an Observable. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$.subscribe((response) => { this.joke = response.joke; }); } }

Slide 117

Slide 117 text

Angular observables Move the property assignment side effect to a tap operation. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { tap } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), ) .subscribe(); } }

Slide 118

Slide 118 text

Angular observables Store the Subscription in the JokeComponent.jokeSubscription property. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { Subscription, tap } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; jokeSubscription?- Subscription; constructor() { this.jokeSubscription = this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), ) .subscribe(); } }

Slide 119

Slide 119 text

Angular observables Use the ngOnDestroy lifecycle hook to unsubscribe from the subscription. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject, OnDestroy } from '@angular/core'; import { Subscription, tap } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ /- (.--) }) export class JokeComponent implements OnDestroy { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; jokeSubscription?- Subscription; constructor() { this.jokeSubscription = this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), ) .subscribe(); } ngOnDestroy() { this.jokeSubscription?-unsubscribe(); } }

Slide 120

Slide 120 text

Angular observables Unsubscribing from an Observable created by the HttpClient service causes the HTTP request to be aborted. In our example it happens when a user navigates away from the component before an HTTP response is received.

Slide 121

Slide 121 text

Angular observables Because unsubscribing from an observable in the ngOnDestroy hook is a common pattern in Angular applications, we can use the takeUntilDestroyed operator from the @angular/core/rxjs-interop package. Now we do not need to manage the subscription manually. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { tap } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ /- (.--) }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), takeUntilDestroyed(), ) .subscribe(); } }

Slide 122

Slide 122 text

Angular observables To prevent memory leaks and long- running side effects, the takeUntilDestroyed operator should always be the last operator in an observable pipeline. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { tap } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ /- (.--) }) export class JokeComponent { http = inject(HttpClient); jokeRequest$ = this.http.get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), takeUntilDestroyed(), ) .subscribe(); } }

Slide 123

Slide 123 text

The AsyncPipe Rather than having to maintain an observable, a plain value property, and manage a subscription, the AsyncPipe enables us to use an observable directly in a component template.

Slide 124

Slide 124 text

The AsyncPipe Leave only the HttpClient and this joke$ property in JokeComponent. /- joke.component.ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { map, Observable } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); joke$: Observable = this.http .get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ) .pipe(map((response) => response.joke)); }

Slide 125

Slide 125 text

The AsyncPipe Add AsyncPipe to Component.imports in joke.component.ts. /- joke.component.ts import { AsyncPipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { map, Observable } from 'rxjs'; import { JokeResponse } from '.-/joke-response'; @Component({ selector: 'app-joke', imports: [AsyncPipe], templateUrl: './joke.component.html', styleUrl: './joke.component.css', }) export class JokeComponent { http = inject(HttpClient); joke$: Observable = this.http .get( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ) .pipe(map((response) => response.joke)); }

Slide 126

Slide 126 text

The AsyncPipe Replace the component template with these template blocks in joke.component.html. <- joke.component.html --> @let joke = joke$ | async; @if (joke) {

{{ joke }}<-p> } @else {

Loading joke.--<-em><-p> }

Slide 127

Slide 127 text

RxJS Subject There are more than one type of RxJS Subject: • AsyncSubject • BehaviorSubject • ReplaySubject • Subject • WebsocketSubject import { Subject } from 'rxjs'; const name = new Subject(); /- (No output)

Slide 128

Slide 128 text

RxJS Subject RxJS Subjects are Observables in that they have Subject.subscribe and Subject.pipe methods. import { map, Subject, Subscription } from 'rxjs'; const name = new Subject(); const subscription: Subscription = name .pipe(map((n) => n.toUpperCase())) .subscribe((n) => console.log(n)); /- (No output)

Slide 129

Slide 129 text

RxJS Subject Subjects have a method for each notification type: • Subject.next(value) • Subject.error(error) • Subject.complete() These methods allow us to directly control their notifications. import { map, Subject, Subscription } from 'rxjs'; const name = new Subject(); const subscription: Subscription = name .pipe(map((n) => n.toUpperCase())) .subscribe((n) => console.log(n)); name.next('Ella'); name.next('Noah'); name.next('Lily'); name.complete(); /- -> "ELLA" /- -> "NOAH" /- -> "LILY"

Slide 130

Slide 130 text

Late subscription When we subscribe after a notification is emitted from a Subject instance, we miss that notification. import { map, Subject, Subscription } from 'rxjs'; const name = new Subject(); name.next('Ella'); const subscription: Subscription = name .pipe(map((n) => n.toUpperCase())) .subscribe((n) => console.log(n)); name.next('Noah'); name.next('Lily'); name.complete(); /- -> "NOAH" /- -> "LILY"

Slide 131

Slide 131 text

RxJS BehaviorSubject A BehaviorSubject is a subject that always requires and maintains a current value. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject(false); /- (No output)

Slide 132

Slide 132 text

RxJS BehaviorSubject Because BehaviorSubject maintains a current value, we always receive a value on subscription. Either the initial value or the value of the latest next notification. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject(false); isAuthenticated.subscribe((b) => console.log(b)); /- -> false

Slide 133

Slide 133 text

RxJS BehaviorSubject For imperative APIS, a BehaviorSubject’s current value can be accesed using its value property. This requires no subscription. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject(false); console.log(isAuthenticated.value); /- -> false

Slide 134

Slide 134 text

RxJS BehaviorSubject Notifications emitted after subscription time are also observed by subscribers. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject(false); isAuthenticated.subscribe((b) => console.log(b)); isAuthenticated.next(true); /- -> false /- -> true

Slide 135

Slide 135 text

RxJS state management Observables, including subjects can emit object values in next notifications. This makes BehaviorSubject a good fit for maintaining the current state. import { BehaviorSubject } from 'rxjs’; import { UserState } from './user-state'; const initialState: UserState = { isAuthenticated: false, username: null, }; const userState = new BehaviorSubject(initialState); userState.subscribe((state) => console.log(state)); /- -> { isAuthenticated: false, username: null } /- user-state.ts export interface UserState { readonly isAuthenticated: boolean; readonly username: string | null; }

Slide 136

Slide 136 text

RxJS state management In this example, we simulate an authentication flow and observe user state changes. import { BehaviorSubject } from 'rxjs'; import { UserState } from './user-state'; function logIn(username: string, password: string) { return fetch('https:--example.com', { mode: 'no-cors' }).then(() => { userState.next({ isAuthenticated: true, username, }); }); } function logOut() { return fetch('https:--example.com', { mode: 'no-cors' }).then(() => { userState.next(initialState); }); } const initialState: UserState = { isAuthenticated: false, username: null, }; const userState = new BehaviorSubject(initialState); userState.subscribe((state) => console.log(state)); logIn('satya', 'N@dell@').then(() => logOut()); /- -> { isAuthenticated: false, username: null } /- -> { isAuthenticated: true, username: "satya" } /- -> { isAuthenticated: false, username: null }

Slide 137

Slide 137 text

Angular state management Generate a message state interface using the command ng generate interface message- state /- message-state.ts export interface MessageState {}

Slide 138

Slide 138 text

Angular state management Add a messages property to MessageState. /- message-state.ts export interface MessageState { readonly messages: readonly string[]; }

Slide 139

Slide 139 text

Angular state management Assign a BehaviorSubject as a private #state field in MessageService. /- message.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { MessageState } from './message-state'; const initialState: MessageState = { messages: [], }; @Injectable({ providedIn: 'root', }) export class MessageService { #state = new BehaviorSubject(initialState); messages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } }

Slide 140

Slide 140 text

Angular state management Emit a next notification to the MessageService.#state subject from the MessageService.receiveMessage method. /- message.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { MessageState } from './message-state'; const initialState: MessageState = { messages: [], }; @Injectable({ providedIn: 'root', }) export class MessageService { #state = new BehaviorSubject(initialState); messages: readonly string[] = []; receiveMessage(message: string) { const state = this.#state.value; this.#state.next({ .--state, messages: [.--state.messages, message], }); } }

Slide 141

Slide 141 text

Angular state management Replace the MessageService.messages property with a MessageService.messages$ observable property. /- message.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, map, Observable } from 'rxjs'; import { MessageState } from './message-state'; const initialState: MessageState = { messages: [], }; @Injectable({ providedIn: 'root', }) export class MessageService { #state = new BehaviorSubject(initialState); messages$: Observable = this.#state.pipe( map((state) => state.messages), ); receiveMessage(message: string) { const state = this.#state.value; this.#state.next({ .--state, messages: [.--state.messages, message], }); } }

Slide 142

Slide 142 text

Angular state management Add AsyncPipe to Component.imports in message- page.component.ts. /- message-page.component.ts import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { MessageListComponent } from '.-/message-list/message-list.component'; import { MessageService } from '.-/message.service'; @Component({ selector: 'app-message-page', imports: [MessageListComponent, AsyncPipe], templateUrl: './message-page.component.html', styleUrl: './message-page.component.css', }) export class MessagePageComponent { messageService = inject(MessageService); }

Slide 143

Slide 143 text

Angular state management Add a @let template block where messageService.messages$ is passed through the AsyncPipe and assigned to a local messages template variable in message-page.component.html. <- message-page.component.html -->

Received messages<-h2> @let messages = messageService.messages$ | async;

Slide 144

Slide 144 text

Angular state management Use an empty array ([]) as the default value. This is necessary because the AsyncPipe initially emits a null value. <- message-page.component.html -->

Received messages<-h2> @let messages = (messageService.messages$ | async) ?- [];

Slide 145

Slide 145 text

Angular state management Bind the messages template variable to the MessageListComponent.messages property. <- message-page.component.html -->

Received messages<-h2> @let messages = (messageService.messages$ | async) ?- [];

Slide 146

Slide 146 text

Code lab 3

Slide 147

Slide 147 text

Code lab 3 Use RxJS state management for one of your previous code lab applications. Or make the time in the Today page update every second automatically using RxJS state management. Or follow these slides to continue the sample application. Available at https://speakerdeck.com/layzee.

Slide 148

Slide 148 text

Automated testing of Angular applications

Slide 149

Slide 149 text

Angular component tests using TestBed Run your Angular component tests using the command ng test

Slide 150

Slide 150 text

AppComponent tests Resolve the type issue in app.component.spec.ts by deleting the second test, that is the second time a callback is passed to the it function. /- app.component.spec.ts import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('h1')?-textContent).toContain( 'Hello, introduction-to-angular', ); }); });

Slide 151

Slide 151 text

AppComponent tests To resolve dependency injection issues for the AppComponent tests, provide the Angular Router with the actual application routes. /- app.component.spec.ts import { TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app.component'; import { routes } from './app.routes'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], providers: [provideRouter(routes)], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('h1')?-textContent).toContain( 'Hello, introduction-to-angular', ); }); });

Slide 152

Slide 152 text

AppComponent tests To prevent the Angular Router from navigating in the test browser, provide Angular Location service test doubles by calling the provideLocationMocks provider function. /- app.component.spec.ts import { provideLocationMocks } from '@angular/common/testing'; import { TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app.component'; import { routes } from './app.routes'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], providers: [provideRouter(routes), provideLocationMocks()], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('h1')?-textContent).toContain( 'Hello, introduction-to-angular', ); }); });

Slide 153

Slide 153 text

AppComponent tests To make the second AppComponent test pass: 1. Update the test description 2. Use auto change detection in the component test by calling the ComponentFixture.autoDetectCh anges method 3. Click the first link ( HTML element) 4. Wait for the Router and Angular’s change detection to stabilize in the component test by calling the ComponentFixture.whenStable method and awaiting its returned Promise 5. Assert that the first paragraph (

HTML element) contains the text Hello, TimePlan! /- app.component.spec.ts import { provideLocationMocks } from '@angular/common/testing'; import { TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app.component'; import { routes } from './app.routes'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], providers: [provideRouter(routes), provideLocationMocks()], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it('should render a greeting', async () => { /- [1] const fixture = TestBed.createComponent(AppComponent); fixture.autoDetectChanges(); /- [2] const compiled = fixture.nativeElement as HTMLElement; compiled.querySelector('a')?-click(); /- [3] await fixture.whenStable(); /- [4] expect(compiled.querySelector('p')?-textContent).toContain( /- [5] 'Hello, TimePlan!', /- [5] ); }); });

Slide 154

Slide 154 text

NavigationCompo nent tests To resolve dependency injection issues for the NavigationComponent tests, provide the missing services like in the AppComponent tests. /- navigation.component.spec.ts import { provideLocationMocks } from '@angular/common/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { routes } from '.-/app.routes'; import { NavigationComponent } from './navigation.component'; describe('NavigationComponent', () => { let component: NavigationComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NavigationComponent], providers: [provideRouter(routes), provideLocationMocks()], }).compileComponents(); fixture = TestBed.createComponent(NavigationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });

Slide 155

Slide 155 text

JokeComponent tests To resolve dependency injection issues for the JokeComponent tests, provide the HttpClient. /- joke.component.spec.ts import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { JokeComponent } from './joke.component'; describe('JokeComponent', () => { let component: JokeComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [JokeComponent], providers: [provideHttpClient()], }).compileComponents(); fixture = TestBed.createComponent(JokeComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });

Slide 156

Slide 156 text

JokeComponent tests To prevent the HttpClient from making actual HTTP requests from the test browser, provide HttpClient dependency test doubles by calling the provideHttpClientTesting provider function. To mock HTTP responses in a component tests, use the HttpTestingController service. Documentation at https://angular.dev/guide/http/testing. /- joke.component.spec.ts import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { JokeComponent } from './joke.component'; describe('JokeComponent', () => { let component: JokeComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [JokeComponent], providers: [provideHttpClient(), provideHttpClientTesting()], }).compileComponents(); fixture = TestBed.createComponent(JokeComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });

Slide 157

Slide 157 text

JokePageCompone nt tests To resolve dependency injection issues for the JokePageComponent tests, provide the missing services like in the JokeComponent tests. /- joke-page.component.spec.ts import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { JokePageComponent } from './joke-page.component'; describe('JokePageComponent', () => { let component: JokePageComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [JokePageComponent], providers: [provideHttpClient(), provideHttpClientTesting()], }).compileComponents(); fixture = TestBed.createComponent(JokePageComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });

Slide 158

Slide 158 text

Angular component tests using TestBed All your component tests should now be running and passing in the test browser.

Slide 159

Slide 159 text

End-to-end tests using Playwright Add Playwright as your end-to-end testing framework using the command ng e2e and picking Playwright from the list. Press Y then Enter in all prompts. Cannot find "e2e" target for the specified project. You can add a package that implements these capabilities. For example: Playwright: ng add playwright-ng-schematics Cypress: ng add @cypress/schematic Nightwatch: ng add @nightwatch/schematics WebdriverIO: ng add @wdio/schematics Puppeteer: ng add @puppeteer/ng-schematics Would you like to add a package with "e2e" capabilities now? Playwright Determining Package Manager › Using package manager: npm Searching for compatible package version › Found compatible package version: playwright-ng-schematics@1.2.2. Loading package information from registry Confirming installation Installing package Install Playwright browsers (can be done manually via 'npx playwright install')? yes Adding @playwright/test 1.49.1 CREATE playwright.config.ts (1933 bytes) CREATE e2e/example.spec.ts (208 bytes) UPDATE angular.json (3099 bytes) UPDATE package.json (1212 bytes) UPDATE .gitignore (698 bytes) Packages installed successfully. Installing browsers... Downloading Chromium 131.0.6778.33 (playwright build v1148) from https://playwright.azureedge.net/builds/chromium/1148/chromium-win64.zip 136.9 MiB [====================] 100% 0.0s Chromium 131.0.6778.33 (playwright build v1148) downloaded to C:\Users\lars\AppData\Local\ms- playwright\chromium-1148 Downloading Chromium Headless Shell 131.0.6778.33 (playwright build v1148) from https://playwright.azureedge.net/builds/chromium/1148/chromium-headless-shell-win64.zip 87.7 MiB [====================] 100% 0.0s Chromium Headless Shell 131.0.6778.33 (playwright build v1148) downloaded to C:\Users\lars\AppData\Local\ms-playwright\chromium_headless_shell-1148 Downloading Firefox 132.0 (playwright build v1466) from https://playwright.azureedge.net/builds/firefox/1466/firefox-win64.zip 85.8 MiB [====================] 100% 0.0s Firefox 132.0 (playwright build v1466) downloaded to C:\Users\lars\AppData\Local\ms- playwright\firefox-1466 Downloading Webkit 18.2 (playwright build v2104) from https://playwright.azureedge.net/builds/webkit/2104/webkit-win64.zip 52.7 MiB [====================] 100% 0.0s Webkit 18.2 (playwright build v2104) downloaded to C:\Users\lars\AppData\Local\ms- playwright\webkit-2104 Downloading FFMPEG playwright build v1010 from https://playwright.azureedge.net/builds/ffmpeg/1010/ffmpeg-win64.zip 1.3 MiB [====================] 100% 0.0s FFMPEG playwright build v1010 downloaded to C:\Users\lars\AppData\Local\ms-playwright\ffmpeg-1010

Slide 160

Slide 160 text

End-to-end tests using Playwright Run the generated end-to-end test in example.spec.ts in headless mode in your terminal using the command ng e2e Running 3 tests using 3 workers 1) [webkit] › example.spec.ts:3:5 › has title ─────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveTitle(expected) Locator: locator(':root') Expected pattern: /MyApp/ Received string: "Hello" Call log: - expect.toHaveTitle with timeout 5000ms - waiting for locator(':root') 8 × locator resolved to … - unexpected value "Hello" 5 | 6 | // Expect a title "to contain" a substring. > 7 | await expect(page).toHaveTitle(/MyApp/); | ^ 8 | }); 9 | at D:\projects\sandbox\introduction-to-angular\e2e\example.spec.ts:7:22

Slide 161

Slide 161 text

End-to-end tests using Playwright An HTML test report should open on http://localhost:9323 because the tests fails in all 3 browsers: • Chromium • Firefox • WebKit (Safari)

Slide 162

Slide 162 text

End-to-end tests using Playwright Run the generated end-to-end test in example.spec.ts in UI mode in your terminal using the command ng e2e --ui

Slide 163

Slide 163 text

End-to-end tests using Playwright Double-click the test (has title) or test file (example.spec.ts) to run it

Slide 164

Slide 164 text

End-to-end tests using Playwright Click a Watch button or the Watch all button to run end-to-end test(s) in watch mode.

Slide 165

Slide 165 text

End-to-end tests using Playwright To make the generated end-to-end test pass, open example.spec.ts and make the following changes. 1. Change the test description 2. Change the comment 3. Add a Playwright Locator for the greeting 4. Assert that the greeting contains the text Hello, TimePlan! /- example.spec.ts import { expect, test } from '@playwright/test'; test('has greeting', async ({ page }) => { /- [1] await page.goto('/'); /- Expect a greeting "to contain" a substring. /- [2] const greeting = page.getByText('hello,'); /- [3] await expect(greeting).toContainText('Hello, TimePlan!'); /- [3][4] });

Slide 166

Slide 166 text

End-to-end tests using Playwright The end-to-end test should now pass.

Slide 167

Slide 167 text

End-to-end tests using Playwright To run end-to-end tests in multiple browsers in Playwright UI mode, follow these steps: 1. Expand the Filter accordion below the Playwright logo 2. Check the box next to each browser you want to test in 3. Double click a test or test file to run it in the specified browsers Or expand a test accordian to list each browser individually then double click a browser name

Slide 168

Slide 168 text

Conclusion

Slide 169

Slide 169 text

Workshop syllabus Local environment setup for Angular • Node.js • npm/Yarn Classic • Angular CLI • Visual Studio Code Fundamentals of Angular declarables • Angular component models • Angular component templates • Angular component styles • Angular pipes • Angular directives

Slide 170

Slide 170 text

Workshop syllabus Angular service essentials • Angular services • Angular dependency injection • Angular HttpClient • Anguar Router Angular state management • RxJS Observable • RxJS BehaviorSubject • Common RxJS operators • Angular AsyncPipe

Slide 171

Slide 171 text

Workshop syllabus Automated testing of Angular applications • Angular component testing with TestBed • Playwright end-to-end testing

Slide 172

Slide 172 text

What we did not cover • Application bootstrapping • Change detection algorithms • Lifecycle hooks • Component view/content queries • Deferrable views • Lazy loading • Dynamic component rendering • Non-class-based services and providers • HTTP interceptors • Route parameters

Slide 173

Slide 173 text

What we did not cover • Angular Signals • Angular Forms • Angular Internationalization (i18n) • Angular Progressive Web Apps (PWA) • Angular Server-Side Rendering (SSR) • The ng update command • The ng add command • CI/CD workflows • File and folder structure • Architecture

Slide 174

Slide 174 text

Recommended reading Et billede, der indeholder tekst, skærmbillede, plakat, grafisk design Automatisk genereret beskrivelse Et billede, der indeholder tekst, tøj, plakat, Kostumedesign Automatisk genereret beskrivelse