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

Introduction to Angular - TimePlan Software

Introduction to Angular - TimePlan Software

A workshop for Angular beginners.

Presented at TimePlan Software, December 2024.

Lars Gyrup Brink Nielsen

December 10, 2024
Tweet

More Decks by Lars Gyrup Brink Nielsen

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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)
  9. 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.
  10. 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
  11. 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 } } }
  12. 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
  13. 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
  14. 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 --> <p>hello-world works!<-p> /- hello-world.component.css *- :host { display: block; } Generate your first component using the command ng generate component hello- world
  15. 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 --> <p>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.
  16. 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 --> <h1>Welcome<-h1> <app-hello-world /- To use a component as a child component: • Add the child component class to the Component.imports array of the parent component • Add an HTML tag to the parent component template that uses the element name specified by the child component’s selector
  17. 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 --> <p>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 }}
  18. 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.
  19. 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 --> <h1>Welcome<-h1> <app-hello-world name="TimePlan" /- Input properties are passed to child components via template bindings.
  20. 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<string>(); } <- hello-world.component.html --> <p>Hello, {{ name }}!<-p> <button (click)="messageSent.emit('Hi there')">Send message<-button> A component can output data to other components via properties that are marked with an @Output() decorator.
  21. 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 --> <h1>Welcome<-h1> <app-hello-world name="TimePlan" (messageSent)="message = $event" /- <p>Received: {{ message }}<-p> Parent components tap into child component output properties via event listeners.
  22. Template control flow <- app.component.html --> <app-hello-world name="TimePlan" (messageSent)="message =

    $event" /- @if (message) { <p>Received: {{ message }}<-p> } Angular has built-in template blocks for control flow. Conditionals use @if, @else-if, and @else blocks.
  23. 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 --> <h1>Welcome<-h1> <app-hello-world name="TimePlan" (messageSent)="receiveMessage($event)" /- <h2>Received messages<-h2> <app-message-list [messages]="receivedMessages" /- Angular has built-in template blocks for control flow. Loops use @for and @empty blocks.
  24. 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) { <p>Message #{{ $index + 1 }}: {{ message }}<-p> } @empty { <p><em>No messages received.<-em><-p> } Angular has built-in template blocks for control flow. Loops use @for and @empty blocks.
  25. 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.
  26. Control flow <- today.component.html --> <p> 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.
  27. 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 --> <app-hello-world name="TimePlan" (messageSent)="receiveMessage($event)" /- <h2>Today<-h2> <app-today /- <h2>Received messages<-h2> <app-message-list [messages]="messages" /- Angular has built-in template blocks for control flow. Switch statements use @switch, @case , and @default blocks.
  28. 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
  29. Angular pipes <- today.component.html --> <p> 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 }} <button (click)="refreshTime()">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
  30. Angular pipes <- today.component.html --> <p> 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" }} <button (click)="refreshTime()">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 (:)
  31. 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
  32. 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.
  33. 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
  34. 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 --> <p> Today is {{ now | weekday }} and the time is {{ now | date: "mediumTime" }} <button (click)="refreshTime()">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 }}
  35. 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.
  36. 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
  37. 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.
  38. 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.
  39. 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.
  40. 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.
  41. 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.
  42. 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<string>(); } To use a directive: 1. Add the directive class to Component.imports.
  43. 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<string>(); } <- hello-world.component.html --> <p appFontSize>Hello, {{ name }}!<-p> <button (click)="messageSent.emit('Hi there')">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.
  44. 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<string>(); } <- hello-world.component.html --> <p [appFontSize]="24">Hello, {{ name }}!<-p> <button (click)="messageSent.emit('Hi there')">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.
  45. 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 --> <p [appFontSize]="24">Hello, {{ name }}!<-p> <button (click)="messageSent.emit('Hi there')">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.
  46. 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.
  47. 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
  48. 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.
  49. 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.
  50. 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.
  51. Angular services <- app.component.html --> <app-hello-world name="TimePlan" (messageSent)="messageService.receiveMessage($event)" /- <h2>Today<-h2>

    <app-today /- <h2>Received messages<-h2> <app-message-list [messages]="messageService.messages" /- Use the injected MessageService instance in app.component.html.
  52. 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.
  53. 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.
  54. 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
  55. 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.
  56. 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.
  57. 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.
  58. Using the HttpClient /- joke-response.ts export interface JokeResponse {} Generate

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

    error: boolean; readonly category: string; readonly type: string; readonly joke: string; readonly flags: Record<string, boolean>; readonly id: string; readonly safe: boolean; readonly lang: string; } Add properties to JokeResponse. These are found at https://sv443.net/jokeapi/v2/.
  60. 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<JokeResponse>( '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.
  61. 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); joke?- string; } Add an optional joke property of type string to JokeComponent.
  62. 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode', ); joke?- string; constructor() {} } Add a constructor to JokeComponent.
  63. 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<JokeResponse>( '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.
  64. Using the HttpClient <- joke.component.html --> @if (joke) { <p>{{

    joke }}<-p> } @else { <p><em>Loading joke.--<-em><-p> } Add these template blocks to joke.component.html.
  65. 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.
  66. Using the HttpClient <- app.component.html --> <app-hello-world name="TimePlan" (messageSent)="messageService.receiveMessage($event)" /-

    <h2>Today<-h2> <app-today /- <h2>Joke<-h2> <app-joke /- <h2>Received messages<-h2> <app-message-list [messages]="messageService.messages" /- Use <app-joke /> in app.component.html.
  67. 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
  68. 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 --> <app-hello-world name="TimePlan" (messageSent)="messageService.receiveMessage($event)" /- Move the HelloWorldComponent from AppComponent to HelloPageComponent. Also bring the MessageService.
  69. 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 --> <h2>Today<-h2> <app-today /- Move the Today section from AppComponent to TodayPageComponent.
  70. 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 --> <h2>Received messages<-h2> <app-message-list [messages]="messageService.messages" /- Move the Received messages section from AppComponent to MessagePageComponent. Also bring the MessageService.
  71. 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.
  72. 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 --> <router-outlet /- Add RouterOutlet to Component.imports in app.component.ts. Add <router-outlet /> to app.component.html.
  73. Using Angular routes import { Routes } from '@angular/router'; export

    const routes: Routes = []; Open app.routes.ts.
  74. 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.
  75. Angular Router The application should now navigate to the hello

    URL path and display the HelloPageComponent.
  76. 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
  77. 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.
  78. 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.
  79. Navigation links <- navigation.component.html --> <nav> <ul> @for (route of

    routes; track route) { <li> <a [routerLink]="route.path" routerLinkActive="is-active"> {{ route.title }} <-a> <-li> } <-ul> <-nav> Add this navigation menu to navigation.component.html.
  80. Navigation links /- navigation.component.css *- :host { display: block; }

    .is-active { font-weight: bold; } <- navigation.component.html --> <nav> <ul> @for (route of routes; track route) { <li> <a [routerLink]="route.path" routerLinkActive="is-active"> {{ route.title }} <-a> <-li> } <-ul> <-nav> Create a CSS ruleset targeting the is-active HTML class in navigation.component.css.
  81. 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 --> <app-navigation /- <router-outlet /- Add NavigationComponent to Component.imports in app.component.ts. Use <app-navigation /> in app.component.html.
  82. Navigation links A navigation menu should be displayed at the

    top of the page. The active page should be highlighted in the navigation menu.
  83. 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.
  84. Programmating navigation <- hello-page.component.ts --> <app-hello-world name="TimePlan" (messageSent)="messageService.receiveMessage($event)" /- <button

    (click)="router.navigateByUrl('messages')">Open messages<-button> Call the Router.navigateByUrl method in hello- page.component.html.
  85. 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.
  86. 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
  87. 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<string> = from(['Ella', 'Noah', 'Lily']);
  88. RxJS Observable Pass a callback to an Observable.subscribe method to

    observe its next notification(s). import { from, Observable } from 'rxjs'; const name$: Observable<string> = from(['Ella', 'Noah', 'Lily']); name$.subscribe((name) => console.log(name)); /- -> "Ella" /- -> "Noah" /- -> "Lily"
  89. 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
  90. RxJS Observable An observable may end by emitting an error

    notification or a complete notification. An observable may end without emitting any next notifications.
  91. 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<string> = 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"
  92. RxJS operators RxJS 7.8 includes 90 operators. Operators are functions

    that can operate on the notifications, values, or timing of an obsevable.
  93. 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<Name> = 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; }
  94. 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<Name> = 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"
  95. 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<Name> = 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"
  96. 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<Name> = 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"
  97. 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<Name> = 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"
  98. 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<Name> = 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"
  99. 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<Name> = 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"
  100. 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<Name> = 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)
  101. 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<Name> = 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"
  102. 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<Name> = 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"
  103. 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<Name> = 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"
  104. 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<Name> = 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"
  105. Angular observables We already created an Observable in our Angular

    application. The HttpClient.get method returned an Observable<JokeResponse>. /- 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$.subscribe((response) => { this.joke = response.joke; }); } }
  106. 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), ) .subscribe(); } }
  107. 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<JokeResponse>( '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(); } }
  108. 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<JokeResponse>( '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(); } }
  109. 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.
  110. 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), takeUntilDestroyed(), ) .subscribe(); } }
  111. 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<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ); joke?- string; constructor() { this.jokeRequest$ .pipe( tap((response) => { this.joke = response.joke; }), takeUntilDestroyed(), ) .subscribe(); } }
  112. 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.
  113. 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<string> = this.http .get<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ) .pipe(map((response) => response.joke)); }
  114. 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<string> = this.http .get<JokeResponse>( 'https:--v2.jokeapi.dev/joke/Programming?safe-mode&type=single', ) .pipe(map((response) => response.joke)); }
  115. The AsyncPipe Replace the component template with these template blocks

    in joke.component.html. <- joke.component.html --> @let joke = joke$ | async; @if (joke) { <p>{{ joke }}<-p> } @else { <p><em>Loading joke.--<-em><-p> }
  116. RxJS Subject There are more than one type of RxJS

    Subject: • AsyncSubject • BehaviorSubject • ReplaySubject • Subject • WebsocketSubject import { Subject } from 'rxjs'; const name = new Subject<string>(); /- (No output)
  117. 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<string>(); const subscription: Subscription = name .pipe(map((n) => n.toUpperCase())) .subscribe((n) => console.log(n)); /- (No output)
  118. 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<string>(); 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"
  119. 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<string>(); 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"
  120. RxJS BehaviorSubject A BehaviorSubject is a subject that always requires

    and maintains a current value. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject<boolean>(false); /- (No output)
  121. 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<boolean>(false); isAuthenticated.subscribe((b) => console.log(b)); /- -> false
  122. 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<boolean>(false); console.log(isAuthenticated.value); /- -> false
  123. RxJS BehaviorSubject Notifications emitted after subscription time are also observed

    by subscribers. import { BehaviorSubject } from 'rxjs'; const isAuthenticated = new BehaviorSubject<boolean>(false); isAuthenticated.subscribe((b) => console.log(b)); isAuthenticated.next(true); /- -> false /- -> true
  124. 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<UserState>(initialState); userState.subscribe((state) => console.log(state)); /- -> { isAuthenticated: false, username: null } /- user-state.ts export interface UserState { readonly isAuthenticated: boolean; readonly username: string | null; }
  125. 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<UserState>(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 }
  126. Angular state management Generate a message state interface using the

    command ng generate interface message- state /- message-state.ts export interface MessageState {}
  127. Angular state management Add a messages property to MessageState. /-

    message-state.ts export interface MessageState { readonly messages: readonly string[]; }
  128. 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<MessageState>(initialState); messages: readonly string[] = []; receiveMessage(message: string) { this.messages = [.--this.messages, message]; } }
  129. 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<MessageState>(initialState); messages: readonly string[] = []; receiveMessage(message: string) { const state = this.#state.value; this.#state.next({ .--state, messages: [.--state.messages, message], }); } }
  130. 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<MessageState>(initialState); messages$: Observable<readonly string[]> = this.#state.pipe( map((state) => state.messages), ); receiveMessage(message: string) { const state = this.#state.value; this.#state.next({ .--state, messages: [.--state.messages, message], }); } }
  131. 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); }
  132. 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 --> <h2>Received messages<-h2> @let messages = messageService.messages$ | async; <app-message-list [messages]="messageService.messages" /-
  133. 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 --> <h2>Received messages<-h2> @let messages = (messageService.messages$ | async) ?- []; <app-message-list [messages]="messageService.messages" /-
  134. Angular state management Bind the messages template variable to the

    MessageListComponent.messages property. <- message-page.component.html --> <h2>Received messages<-h2> @let messages = (messageService.messages$ | async) ?- []; <app-message-list [messages]="messages" /-
  135. 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.
  136. 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', ); }); });
  137. 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', ); }); });
  138. 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', ); }); });
  139. 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 (<a> 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 (<p> 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] ); }); });
  140. 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<NavigationComponent>; 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(); }); });
  141. 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<JokeComponent>; 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(); }); });
  142. 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<JokeComponent>; 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(); }); });
  143. 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<JokePageComponent>; 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(); }); });
  144. Angular component tests using TestBed All your component tests should

    now be running and passing in the test browser.
  145. 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: [email protected]. 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
  146. 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 <html lang="en">…</html> - 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
  147. 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)
  148. 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
  149. 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.
  150. 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] });
  151. 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
  152. 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
  153. 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
  154. Workshop syllabus Automated testing of Angular applications • Angular component

    testing with TestBed • Playwright end-to-end testing
  155. 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
  156. 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
  157. 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