Angular - ILLYAKO/mywiki GitHub Wiki

Angular

https://github.com/ILLYAKO/angular-tour-of-heroes
https://angular.io/tutorial/tour-of-heroes
https://angular.io/guide/forms
https://angular.io/guide/forms-overview

0. Prerequisites:

  • Node.js
  • npm package manager

1. Angular CLI

Angular CLI is used to create projects, generate application and library code, and perform a variety of ongoing development tests such as testing, bundling, and deployment.

npm install -g @angular/cli

2. Create a workspace and initial application

A collection of Angular projects (that is, applications and libraries ) powered by the Angular CLI that are typically co-located in a single source-control repository (git).

ng new angular-tour-of-heroes

3. Run the application

3.1 Navigate to the workspace folder, such as angular-tour-of-heroes

cd angular-tour-of-heroes

3.2 Run the "serve" command

ng serve --open

The ng serve command launches the server, watches your files, and rebuilds the app as you make changes to those files. The --open (or just -o) option automatically opens your browser to http://localhost:4200/.

You should see the Welcome Angular page.

4. Angular components

The page you see is the application shell. The shell is controlled by an Angular component named AppComponent. Components are the fundamental building blocks of Angular applications. They display data on the screen, listen to user input, and take action based on that input.
Components should focus on presenting data and delegating data access to a service.

4.1. AppComponent

The src/app directory is the starter application.

AppComponent file:

  • app.component.ts The component class code, written in TypeScript.
  • app.component.html The component template, is written in HTML.
  • app.component.css The component's private CSS styles.

4.1.1. Change the application title

4.1.1.1

Change title = 'Tour of Heroes' in app.component.ts (class title property)

4.1.1.2

Delete the default template and add <h1>{{title}}</h1> in app.component.html (template)

The double curly braces are Angular's interpolation binding syntax. This interpolation binding presents the component's title property value inside the HTML header tag.

4.1.1.3

Add application-wide styles in src/styles.css

/* Application-wide Styles */
h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
}
h2, h3 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[type="text"], button {
  color: #333;
  font-family: Cambria, Georgia, serif;
}
button {
  background-color: #eee;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  color: black;
  font-size: 1.2rem;
  padding: 1rem;
  margin-right: 1rem;
  margin-bottom: 1rem;
  margin-top: 1rem;
}
button:hover {
  background-color: black;
  color: white;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}

/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

4.2. New component

4.2.1. Create a new component

Use ng generate to create a new component named 'heroes'.

ng generate component heroes

ng generate creates a new directory, src/app/heroes, and generates the three files of the HeroesComponent along with a test file.

The Component symbol from the Angular core library and annotate the component class with @Component. @Component is a decorator function that specifies the Angular metadata for the component.

ng generate created three metadata properties:

  • selector The component's CSS element selector.
  • templateUrl is The location of the component's template file.
  • styleUrls The location of the component's private CSS styles.

Always export the component class so you can import it elsewhere.

4.2.1. Add a property to the new component (heroes.component.ts).

hero = 'Windstorm';

4.2.2. Show property in the template (heroes.component.html).

<h2>{{hero}}</h2>

4.2.3. Show component view (src/app/app.component.html)

To display the new component, it must be added to the template of the shell AppComponent
<app-heroes></app-heroes>

4.2. Create interface

4.2.1. Create interface (src/app/hero.ts)

Create a Hero interface in its own file in the src/app directory.

export interface Hero {
  id: number;
  name: string;
}
4.2.1. Import interface to the class (src/app/heroes/heroes.component.ts)

Import the Hero interface to HerroesComponent class. Refactor the component's hero property to be of type Hero. Initialize it with an ``idof1` and the name `Windstorm`.

import { Component } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent {
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };
}

4.3. Show the object (heroes.component.html (HeroesComponent template))

4.3.1. Show the hero object

Update the binding in the template to announce the hero's name and show both id and name in a details display

<h2>{{hero.name}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>
4.3.1. Format with the UppercasePipe (src/app/heroes/heroes.component.html)

https://angular.io/guide/pipes <h2>{{hero.name | uppercase}} Details</h2>

4.4. Input text box (src/app/heroes/heroes.component.html (HeroesComponent's template))

The text box should display the property and update that property as the user types.
To automate that data flow, set up a two-way data binding between the <input> form element and the hero.name property.

4.4.1 Two-way binding (src/app/heroes/heroes.component.html (HeroesComponent's template))

The data flows from the component class out to the screen and from the screen back to the class. [(ngModel)] is Angular's two-way data binding syntax.

<div>
  <label for="name">Hero name: </label>
  <input id="name" [([ngModel](https://angular.io/api/forms/NgModel))]="hero.name" placeholder="name">
</div>

4.5. Import FormsModule (app.module.ts)

Open app.module.ts and import the FormsModule symbol from the @angular/forms library.
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
Add FormsModule to the imports array in @NgModule. The imports array contains the list of external modules that the application needs.
imports: [ BrowserModule, FormsModule ],

4.6. Display a selection list

4.6.1. Create mock heroes (src/app/mock-heroes.ts)

Create a file called mock-heroes.ts in the src/app/ directory. Define a HEROES constant as ana array of ten heroes and export it.

import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 12, name: 'Dr. Nice' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr. IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];
4.6.2. Displaying heroes (src/app/heroes/heroes.component.ts)

Open the HeroesComonent class file and import the mock HEROES. import { HEROES } from '../mock-heroes';
Define a component property called heroes to expose the HEROES array for binding

export class HeroesComponent {

  heroes = HEROES;
}
4.6.3. List heroes with *ngFor(heroes.component.html)
<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <button type="button">
      <span class="badge">{{hero.id}}</span>
      <span class="name">{{hero.name}}</span>
    </button>
  </li>
</ul>
4.6.4. Add an *ngFor to the <li> to iterate through the list of heroes

The *ngFor is Angular's repeater directive. It repeats the host element for each element in a list. Don't forget to put the asterisk * in front of ngFor. It's a critical part of the syntax.

<li *ngFor="let hero of heroes">

4.7. Define private styles for a specific component (src/app/heroes/heroes.component.ts (@Component))

You define private styles either inline in the @Component.styles array or as style sheet files identified in the @Component.styleUrls array.

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

Styles and style sheets identified in @Component metadata are scoped to that specific component. The heroes.component.css styles apply only to the HeroesComponent and don't affect the outer HTML or the HTML in any other component.

4.8. Viewing details

4.8.1. Add a click event binding (heroes.component.html (template excerpt))
<li *ngFor="let hero of heroes">
  <button type="button" (click)="onSelect(hero)">
  <!-- ... -->
4.8.2. Add the click event handler. (src/app/heroes/heroes.component.ts (onSelect))

Rename the component's hero property to SelectHero but don't assign any value to it since there is no selected hero when the application starts. Add the following onSelect() method, which assigns the clicked hero from the template to the component's selectHero.

selectedHero?: Hero;
onSelect(hero: Hero): void {
  this.selectedHero = hero;
}
4.8.3. Add a details section
<div *ngIf="selectedHero">
  <h2>{{selectedHero.name | uppercase}} Details</h2>
  <div>id: {{selectedHero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
  </div>
</div>

Add the *ngIf directive to the <div> that wraps the hero details. This directive tells Angular to render the section only when the selectedHero is defined after it has been selected by clicking on a hero.

4.8.4. Style the selected hero (heroes.component.html (toggle the 'selected' CSS class))

Angular's class binding can add and remove a CSS class conditionally.

[class.selected]="hero === selectedHero"

4.9. Create Detail Component

4.9.1. Make the HeroDetailComponent

Use this ng generate command to create a new component named hero-detail. ng generate component hero-detail

4.9.2. Write the template

Remove the hero detail from the HeroComponent and add it to the HeroDetailComponent template.

<div *ngIf="hero">

  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="hero.name" placeholder="name">
  </div>

</div>
4.9.3. Add the @Input() hero property (src/app/hero-detail/hero-detail.component.ts)
4.9.3.1. Import Hero to HeroDetatilComponent

import { Hero } from '../hero';

4.9.3.2 Amend the @angular/core import statement to include the Input symbol.

import { Component, Input } from '@angular/core';

4.9.3.3 Add a hero property, preceded by the @Input() decorator.

@Input() hero?: Hero;

4.9.3.4. Show the HeroDetailComponent (heroes.component.html (HeroDetail binding))

The two components have a parent/child relationship. The parent, HeroesComponent, controls the child, HeroDetailComponent by sending it a new hero to display whenever the user selects a hero from the list.
[hero]="selectedHero" is an Angular property binding.
<app-hero-detail [hero]="selectedHero"></app-hero-detail>

5. Angular services

Services are a great way to share information among classes that don't know each other.

5.1. New Services

5.1.1. Create the HeroService

ng generate service hero

The new service imports the Angular Injectable symbol and annotates the class with the @Injectable() decorator. This marks the class as one that participates in the dependency injection system. The HeroService class is going to provide an injectable service, and it can also have its own injected dependencies.

5.1.2. Get data (src/app/hero.service.ts)

The HeroService could get hero data from anywhere such as a web service, local storage, or a mock data source.

5.1.2.1. Import the Hero and HEROS
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
5.1.2.2. Add a getHeroes method to return the mock heroes.
getHeroes(): Hero[] {
  return HEROES;
}
5.1.2.3. Provide the HeroService

The HeroService should be available to the dependency injection system before Angular can inject into the HeroComponent by registering a provider. A provider is something that can create or deliver a service. In this case, it instantiates the HeroService class to provide the service.
To make sure that the HeroService can provide this service, register it with the injector. The injector is the object that chooses and injects the provider where the application requires it.
By default, ng generate service registers a provider with the root injector for your service by including provider metadata, that's provideIn: 'root' in the @Injectable() decorator.

@Injectable({
  providedIn: 'root',
})
5.1.2.4. Update HeroComponent (src/app/heroes/heroes.component.ts (import HeroService))

import { HeroService } from '../hero.service';
heroes: Hero[] = [];

5.1.2.5. Inject the HeroService (src/app/heroes/heroes.component.ts)

constructor(private heroService: HeroService) {}

5.1.2.6. Create a method to retrieve the heroes from the service. (src/app/heroes/heroes.component.ts)
getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}
5.1.2.7. Call getHeroes() in ngOnInit()

Call getHeroes() indide the ngOnInit lifecycle hook and let Angular call ngOnInit() at an appropriate time after constructing a HeroesComponent instance.
Reserve the constructor for minimal initialization such as wiring constructor parameters to properties. The constructor shouldn't do anything. It certainly should call a function that makes HTTP requests to a remote server as real data service would.

ngOnInit(): void {
  this.getHeroes();
}
5.1.2. Observable data (src/app/heroes/heroes.component.ts)

Observable is one of the key classes in the RxJS library

HeroService.getHeroes() must have an asynchronous signature.
In this tutorial, HeroService.getHeroes() returns an Observable so that it can use the Angular HttpClient.get method to fetch the heroes and have HttpClient.get() return an Observable.

5.1.2.1. Import Observabele (src/app/hero.service.ts (Observable imports))

import { Observable, of } from 'rxjs';

5.1.2.2. Replace the getHeroes() (src/app/hero.service.ts)
getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  return heroes;
}

of(HEROES) returns an Observable<Hero[]> that emits a single value, the array of mock heroes.

5.1.2.3. Subscribe in HeroesComponent (heroes.component.ts (Observable))

The new version waits for the Observable to emit the array of heroes, which could happen now or several minutes from now. The subscribe() method passes the emitted array to the callback, which sets the component's heroes property.
This asynchronous approach works when the HeroService requests heroes from the server.

getHeroes(): void {
  this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
}

6. Show message component

6.1. New Messages Component and Service

6.1.1. Create the MessagesComoponent

ng generate component messages

6.1.2. Edit AppComponet template to display the MessagesComponent.
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
6.2.1. Create the MessagesService

ng generate service message

6.2.2. Replace the MessagesServise content. (src/app/message.service.ts)
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}
6.3.1. Inject MessageService into the HeroService
6.3.1.1. Import the MessageService into the HeroService (src/app/hero.service.ts (import MessageService))

import { MessageService } from './message.service';

6.3.1.2. Add MessageService into the HeroService constructor (src/app/hero.service.ts)

constructor(private messageService: MessageService) { }

6.3.1.3. Send a message from HeroService (src/app/hero.service.ts)
getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  this.messageService.add('HeroService: fetched heroes');
  return heroes;
}
6.3.2. Display the message from HeroService
6.3.2.1. Import MessageService into MessagesComponent(src/app/messages/messages.component.ts (import MessageService))

import { MessageService } from '../message.service';

6.3.2.2. Add MessageService to MessagesComponent constructor.

constructor(public messageService: MessageService) {}
The messageService property must be public because you're going to bind to it in the template.

6.3.2.3. Bind to the MessageService(src/app/messages/messages.component.html)
<div *ngIf="messageService.messages.length">

  <h2>Messages</h2>
  <button type="button" class="clear"
          (click)="messageService.clear()">Clear messages</button>
  <div *ngFor='let message of messageService.messages'> {{message}} </div>

</div>
6.4.2. Add MessageService to HeroedComponent (src/app/heroes/heroes.component.ts)
import { Component, OnInit } from '@angular/core';

import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {

  selectedHero?: Hero;

  heroes: Hero[] = [];

  constructor(private heroService: HeroService, private messageService: MessageService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
    this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
  }

  getHeroes(): void {
    this.heroService.getHeroes()
        .subscribe(heroes => this.heroes = heroes);
  }
}

7. Add navigation with routing.

7.1. New router

In Agular, the best practice is to load and configure the router in a separate, top-level module. The router is dedicated to routing and imported by the root AppModule.
By convention, the module class name is AppRoutingModule and it belongs in the app-routing.module.ts in the src/app directory.

7.1.1. Create an application routing module

ng generate module app-routing --flat --module=app
--flat Puts the file in src/app instead of its own directory
--module=app Tells ng generate to register it in the imports array of the AppModule.

7.1.2. Replace with
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
7.1.2.1. Routes (src/app/app-routing.module.ts)
const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

path String that matches the URL in the browser address bar.
component Component that the router should create when navigating to this route.

7.1.2.2. RouterModule.forRoot() (src/app/app-routing.module.ts)

The @NgModule metadata initializes the router and starts it listening for browser location changes.
The following line adds the RouterModule to the AppRoutingModule imports array and configures it with the routes in one step by calling RouterModule.forRoot():
imports: [ RouterModule.forRoot(routes) ],

The method is called forRoot() because you configure the router at the application's root level. The forRoot() method supplies the service providers and directives needed for routing and performs the initial navigation based on the current browser URL.

7.1.2.3. Export RouterModule

AppRoutingModule exports RouterModule to be avilable throughout the application. exports: [ RouterModule ]

7.2.1. Add RouterOutlet (src/app/app.component.html (router-outlet))

Open the AppComponent template and replace the <app-heroes> element with a <router-outlet> element.

<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>

8. Dashboard view

8.1. Add a dashboard view

8.1.1. Generate DashboardComponent

ng generate component dashboard

8.1.2. Replace default content with (src/app/dashboard/dashboard.component.html)
<h2>Top Heroes</h2>
<div class="heroes-menu">
  <a *ngFor="let hero of heroes">
    {{hero.name}}
  </a>
</div>
8.1.3. Replace default content with (src/app/dashboard/dashboard.component.ts)
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}
8.1.4. Replace default content with (src/app/dashboard/dashboard.component.css)
/* DashboardComponent's private CSS styles */

h2 {
  text-align: center;
}

.heroes-menu {
  padding: 0;
  margin: auto;
  max-width: 1000px;

  /* flexbox */
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
  align-content: flex-start;
  align-items: flex-start;
}

a {
  background-color: #3f525c;
  border-radius: 2px;
  padding: 1rem;
  font-size: 1.2rem;
  text-decoration: none;
  display: inline-block;
  color: #fff;
  text-align: center;
  width: 100%;
  min-width: 70px;
  margin: .5rem auto;
  box-sizing: border-box;

  /* flexbox */
  order: 0;
  flex: 0 1 auto;
  align-self: auto;
}

@media (min-width: 600px) {
  a {
    width: 18%;
    box-sizing: content-box;
  }
}

a:hover {
  background-color: #000;
}
8.1.5. Add the dashboard route (src/app/app-routing.module.ts (import DashboardComponent))
import { DashboardComponent } from './dashboard/dashboard.component';
...
{ path: 'dashboard', component: DashboardComponent },
8.1.6. Add a default route (src/app/app-routing.module.ts)

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },

8.1.7. Add a dashboard link to the shell (src/app/app.component.html)
<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

9. Navigating to hero details

9.1. Add a hero detail route (src/app/app-routing.module.ts)

import { HeroDetailComponent } from './hero-detail/hero-detail.component';
...
{ path: 'detail/:id', component: HeroDetailComponent },

The colon : character in the path indicates that :id is a placeholder for a specific hero id.

9.2. Fix the dashboard hero links to navigate using the parameterized dashboard route. (src/app/dashboard/dashboard.component.html (hero links))

<a *ngFor="let hero of heroes"
  routerLink="/detail/{{hero.id}}">
  {{hero.name}}
</a>

There are Angular interpolation binding within the *ngFor repeater to insert the current iteration's hero.id into each routerLink.

9.3. HerosComponent hero links (src/app/heroes/heroes.component.html (list with onSelect))

Remove the inner HTML of

  • . Wrap the badge and name in an anchor element. Add a routerLink attribute to the anchor that's the same as in the dashboard template.
    <ul class="heroes">
      <li *ngFor="let hero of heroes">
        <a routerLink="/detail/{{hero.id}}">
          <span class="badge">{{hero.id}}</span> {{hero.name}}
        </a>
      </li>
    </ul>
    

    9.4. Remove dead code (src/app/heroes/heroes.component.ts (cleaned up))

    export class HeroesComponent implements OnInit {
      heroes: Hero[] = [];
    
      constructor(private heroService: HeroService) { }
    
      ngOnInit(): void {
        this.getHeroes();
      }
    
      getHeroes(): void {
        this.heroService.getHeroes()
        .subscribe(heroes => this.heroes = heroes);
      }
    }
    

    9.5. Routable HeroDetailComponent

    9.5.1. Add imports to file (src/app/hero-detail/hero-detail.component.ts)
    import { ActivatedRoute } from '@angular/router';
    import { Location } from '@angular/common';
    import { HeroService } from '../hero.service';
    
    9.5.2. Inject the ActivatedRoute, HeroService, and Location services into the constructor
    constructor(
      private route: ActivatedRoute,
      private heroService: HeroService,
      private location: Location
    ) {}
    

    The ActivatedRoute holds information about the route to this instance of the HeroDetailComponent. This component is interested in the route's parameters extracted from the URL. The "id" parameter is the id of the hero to display.
    The HeroService gets hero data from the remote server and this component uses it to get the hero-to-display.
    The location is an Angular service for interacting with the browser. This service lets you navigate back to the previous view.

    9.5.3. Extract the id route parameter (src/app/hero-detail/hero-detail.component.ts)
    ngOnInit(): void {
      this.getHero();
    }
    
    getHero(): void {
      const id = Number(this.route.snapshot.paramMap.get('id'));
      this.heroService.getHero(id)
        .subscribe(hero => this.hero = hero);
    }
    

    The route.snapshot is a static image of the route information shortly after the component was created.
    The paramMap is a dictionary of route parameter value extracted from the URL. The "id" key returns the id of the hero to fetch.
    Route parameters are always strings.The JavaScript Number function converts the string to a number, which is what a hero id should be.

    9.5.3. Add HeroService.getHero() (src/app/hero.service.ts (getHero))
    getHero(id: number): Observable<Hero> {
      // For now, assume that a hero with the specified `id` always exists.
      // Error handling will be added in the next step of the tutorial.
      const hero = HEROES.find(h => h.id === id)!;
      this.messageService.add(`HeroService: fetched hero id=${id}`);
      return of(hero);
    }
    
    9.5.3. Add "go back" button on the HeroDetatil view
    9.5.3.1. Add "go back" button to the component template (src/app/hero-detail/hero-detail.component.html (back button))
    <button type="button" (click)="goBack()">go back</button>
    
    9.5.3.2. Add goBack() method (src/app/hero-detail/hero-detail.component.ts (goBack))
    goBack(): void {
      this.location.back();
    }
    

    10. Get data from a server

    Angular's HttpClient features:

    • The HeroService gets hero data with HTTP requests
    • Add, Edit, and Delete heroes and save these changes over HTTP
    • Search for heroes by name

    10.1. Enable HTTP service (src/app/app.module.ts)

    HttpClient is Angular's mechanism for communicating with a remote server over HTTP. import { HttpClientModule } from '@angular/common/http';

    @NgModule({
      imports: [
    ...
        HttpClientModule,
      ],
    })
    

    10.2. Simulate a data server

    The sample mimics communication with a remote data server by using In-memory Web API module. After installing the module, the application makes requests to and receives responses from the HttpClient. The application doesn't know that the In-memory Web API is intercepting those requests, applying them to an in-memory data store, and returning simulated responses.

    10.2.1. Install the In-memory Web API package from npm

    npm install angular-in-memory-web-api --save

    10.2.2. HttpClientInMemoryWebApiModule and the InMemoryDataService (src/app/app.module.ts)

    Add classes imports

    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
    import { InMemoryDataService } from './in-memory-data.service';
    
    10.2.3. After the HttpClientModule, add the HttpClientInMemoryWebApiModule to the AppModule imports array and configure it with the InMemoryDataService. (src/app/app.module.ts)
    HttpClientModule,
    
    // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
    // and returns simulated server responses.
    // Remove it when a real server is ready to receive requests.
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, { dataEncapsulation: false }
    )
    
    10.2.4. Generate service InMemoryData

    ng generate service InMemoryData

    10.2.5. Replace default content
    import { Injectable } from '@angular/core';
    import { InMemoryDbService } from 'angular-in-memory-web-api';
    import { Hero } from './hero';
    
    @Injectable({
      providedIn: 'root',
    })
    export class InMemoryDataService implements InMemoryDbService {
      createDb() {
        const heroes = [
          { id: 12, name: 'Dr. Nice' },
          { id: 13, name: 'Bombasto' },
          { id: 14, name: 'Celeritas' },
          { id: 15, name: 'Magneta' },
          { id: 16, name: 'RubberMan' },
          { id: 17, name: 'Dynama' },
          { id: 18, name: 'Dr. IQ' },
          { id: 19, name: 'Magma' },
          { id: 20, name: 'Tornado' }
        ];
        return {heroes};
      }
    
      // Overrides the genId method to ensure that a hero always has an id.
      // If the heroes array is empty,
      // the method below returns the initial number (11).
      // if the heroes array is not empty, the method below returns the highest
      // hero id + 1.
      genId(heroes: Hero[]): number {
        return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
      }
    }
    

    10.3. Heroes and HTTP

    10.3.1. Import HttpClient and HttpHeader (src/app/hero.service.ts)

    import { HttpClient, HttpHeaders } from '@angular/common/http';

    constructor(
      private http: HttpClient,
      private messageService: MessageService) { }
    
    10.3.2. Add private log() (src/app/hero.service.ts)
    /** Log a HeroService message with the MessageService */
    private log(message: string) {
      this.messageService.add(`HeroService: ${message}`);
    }
    
    10.3.3. Define theheroUrl (src/app/hero.service.ts)

    private heroesUrl = 'api/heroes'; // URL to web api

    10.3.4. Convert that method to use HttpClient as follows (src/app/hero.service.ts)
    /** GET heroes from the server */
    getHeroes(): Observable<Hero[]> {
    ...
      return this.http.get<Hero[]>(this.heroesUrl)
    ...
    }
    
    10.3.4.1. HttpClient methods return one value

    All HttpClient methods return an RxJS Observable of something.

    10.3.4.2. HttpClient.get() returns response data.

    HttpClient.get() returns the body of the response as an untyped JSON object by default. Applying the optional type specifier, <Hero[]> , adds TypeScript capabilities, which reduce errors during compile time.

    10.4. Error handling

    10.4.1. Import the catchError symbol from rxjs/operators (src/app/hero.service.ts)

    The catchError() operator intercepts an Observable that failed. The operator then passes the error to the error handling function.

    import { catchError, map, tap } from 'rxjs/operators';
    ...
    getHeroes(): Observable<Hero[]> {
      return this.http.get<Hero[]>(this.heroesUrl)
        .pipe(
          catchError(this.handleError<Hero[]>('getHeroes', []))
        );
    }
    ...
    
    10.4.2. handleError

    The following handleError() can be shared by many HeroService methods so it's generalized to meet their different needs.
    Instead of handling the error directly, it returns an error handler function to catchError. This function is configured with both the name of the operation that failed and a safe return value.

    /**
     * Handle Http operation that failed.
     * Let the app continue.
     *
     * @param operation - name of the operation that failed
     * @param result - optional value to return as the observable result
     */
    private handleError<T>(operation = 'operation', result?: T) {
      return (error: any): Observable<T> => {
    
        // TODO: send the error to remote logging infrastructure
        console.error(error); // log to console instead
    
        // TODO: better job of transforming error for user consumption
        this.log(`${operation} failed: ${error.message}`);
    
        // Let the app keep running by returning an empty result.
        return of(result as T);
      };
    }
    
    10.4.3. Tap into the Observable (src/app/hero.service.ts)

    The HeroService methods taps into the flow of observable values and send a message, using the log() method, to the message area at the bottom of the page.
    The RxJS tap() operator enables this ability by looking at the observable values, doing something with those values, and passing them along. The tap() call back doesn't access the values themselves.

    /** GET heroes from the server */
    getHeroes(): Observable<Hero[]> {
      return this.http.get<Hero[]>(this.heroesUrl)
        .pipe(
          tap(_ => this.log('fetched heroes')),
          catchError(this.handleError<Hero[]>('getHeroes', []))
        );
    }
    
    

    10.5. Get hero by id

    Most web APIs support a get by id request in the form :baseURL/:id.

    /** GET hero by id. Will 404 if id not found */
    getHero(id: number): Observable<Hero> {
      const url = `${this.heroesUrl}/${id}`;
      return this.http.get<Hero>(url).pipe(
        tap(_ => this.log(`fetched hero id=${id}`)),
        catchError(this.handleError<Hero>(`getHero id=${id}`))
      );
    }
    

    10.6. Update heroes

    10.6.1. Add button save (src/app/hero-detail/hero-detail.component.html (save))

    <button type="button" (click)="save()">save</button>

    10.6.2. Add save() method src/app/hero-detail/hero-detail.component.ts (save)
    save(): void {
      if (this.hero) {
        this.heroService.updateHero(this.hero)
          .subscribe(() => this.goBack());
      }
    }
    
    10.6.3. Add HeroService.updateHero() (src/app/hero.service.ts (update))
    /** PUT: update the hero on the server */
    updateHero(hero: Hero): Observable<any> {
      return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
        tap(_ => this.log(`updated hero id=${hero.id}`)),
        catchError(this.handleError<any>('updateHero'))
      );
    }
    
    10.6.4. httpOptions (src/app/hero.service.ts)

    The heroes web API expects a special header in HTTP save requests. That header is in the httpOptions constant defined in the HeroService. Add the following to the HeroService class.

    httpOptions = {
      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
    };
    

    10.7. Add a new hero

    10.7.1. Add an element (src/app/heroes/heroes.component.html)
    <div>
      <label for="new-hero">Hero name: </label>
      <input id="new-hero" #heroName />
    
      <!-- (click) passes input value to add() and then clears the input -->
      <button type="button" class="add-button" (click)="add(heroName.value); heroName.value=''">
        Add hero
      </button>
    </div>
    
    10.7.2. Add add() to component (src/app/heroes/heroes.component.ts (add))
    add(name: string): void {
      name = name.trim();
      if (!name) { return; }
      this.heroService.addHero({ name } as Hero)
        .subscribe(hero => {
          this.heroes.push(hero);
        });
    }
    
    10.7.3. Add the addHero() method to the HeroService (src/app/hero.service.ts (addHero))
    /** POST: add a new hero to the server */
    addHero(hero: Hero): Observable<Hero> {
      return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
        tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
        catchError(this.handleError<Hero>('addHero'))
      );
    }
    

    10.8. Delete a hero

    10.8.1. Add delete button (src/app/heroes/heroes.component.html)
    <button type="button" class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
    
    10.8.2. Add the delete() handler to the component class. (src/app/heroes/heroes.component.ts (delete))
    delete(hero: Hero): void {
      this.heroes = this.heroes.filter(h => h !== hero);
      this.heroService.deleteHero(hero.id).subscribe();
    }
    
    10.8.3. Add a deleteHero() method to HeroService (src/app/hero.service.ts (delete))
    /** DELETE: delete the hero from the server */
    deleteHero(id: number): Observable<Hero> {
      const url = `${this.heroesUrl}/${id}`;
    
      return this.http.delete<Hero>(url, this.httpOptions).pipe(
        tap(_ => this.log(`deleted hero id=${id}`)),
        catchError(this.handleError<Hero>('deleteHero'))
      );
    }
    

    10.9. Search by name

    10.9.1. Add a searchHeroes() method to the HeroService (src/app/hero.service.ts)
    /* GET heroes whose name contains search term */
    searchHeroes(term: string): Observable<Hero[]> {
      if (!term.trim()) {
        // if not search term, return empty hero array.
        return of([]);
      }
      return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
        tap(x => x.length ?
           this.log(`found heroes matching "${term}"`) :
           this.log(`no heroes matching "${term}"`)),
        catchError(this.handleError<Hero[]>('searchHeroes', []))
      );
    }
    
    10.9.2. Add a heroes search feature to the Dashboard (src/app/dashboard/dashboard.component.html)
    <h2>Top Heroes</h2>
    <div class="heroes-menu">
      <a *ngFor="let hero of heroes"
          routerLink="/detail/{{hero.id}}">
          {{hero.name}}
      </a>
    </div>
    
    <app-hero-search></app-hero-search>
    
    10.9.3. Create HeroSearchComponent

    ng generate component hero-search

    10.9.4. Replace the HeroSearchComponent template with an <input> (src/app/hero-search/hero-search.component.html)
    <div id="search-component">
      <label for="search-box">Hero Search</label>
      <input #searchBox id="search-box" (input)="search(searchBox.value)" />
    
      <ul class="search-result">
        <li *ngFor="let hero of heroes$ | async" >
          <a routerLink="/detail/{{hero.id}}">
            {{hero.name}}
          </a>
        </li>
      </ul>
    </div>
    
    10.9.5. Edit the HeroSearchComponent class
    import { Component, OnInit } from '@angular/core';
    
    import { Observable, Subject } from 'rxjs';
    
    import {
       debounceTime, distinctUntilChanged, switchMap
     } from 'rxjs/operators';
    
    import { Hero } from '../hero';
    import { HeroService } from '../hero.service';
    
    @Component({
      selector: 'app-hero-search',
      templateUrl: './hero-search.component.html',
      styleUrls: [ './hero-search.component.css' ]
    })
    export class HeroSearchComponent implements OnInit {
      heroes$!: Observable<Hero[]>;
      private searchTerms = new Subject<string>();
    
      constructor(private heroService: HeroService) {}
    
      // Push a search term into the observable stream.
      search(term: string): void {
        this.searchTerms.next(term);
      }
    
      ngOnInit(): void {
        this.heroes$ = this.searchTerms.pipe(
          // wait 300ms after each keystroke before considering the term
          debounceTime(300),
    
          // ignore new term if same as previous term
          distinctUntilChanged(),
    
          // switch to new search observable each time the term changes
          switchMap((term: string) => this.heroService.searchHeroes(term)),
        );
      }
    }
    

    11. Building a template-driven form.

    FormsModule:

    • NgModel Reconciles value changes in the attached form element with changes in the data model, allowing you to respond to user input with input validation and error handling
    • NgForm Creates a top-level FormGroup instance and binds it to a <form> element to track aggregated form value and validation status. As soon as you import FormsModule, this directive becomes active by default on all <form> tags. You don't need to add a special selector.
    • NgModelGroup Creates and binds a FormGroup instance to a DOM element.
  • ⚠️ **GitHub.com Fallback** ⚠️