Angular - ILLYAKO/mywiki GitHub Wiki
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
- Node.js
- npm package manager
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
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
cd angular-tour-of-heroes
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 tohttp://localhost:4200/
.
You should see the Welcome Angular page.
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.
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.
Change title = 'Tour of Heroes'
in app.component.ts
(class title property)
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.
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;
}
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 theHeroesComponent
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.
hero = 'Windstorm';
<h2>{{hero}}</h2>
To display the new component, it must be added to the template of the shell AppComponent
<app-heroes></app-heroes>
Create a Hero
interface in its own file in the src/app
directory.
export interface Hero {
id: number;
name: string;
}
Import the Hero
interface to HerroesComponent
class.
Refactor the component's hero
property to be of type Hero
. Initialize it with an ``idof
1` 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'
};
}
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>
https://angular.io/guide/pipes
<h2>{{hero.name | uppercase}} Details</h2>
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.
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>
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 ],
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' }
];
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.
<li *ngFor="let hero of heroes">
<button type="button" (click)="onSelect(hero)">
<!-- ... -->
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;
}
<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 theselectedHero
is defined after it has been selected by clicking on a hero.
Angular's class binding can add and remove a CSS class conditionally.
[class.selected]="hero === selectedHero"
Use this
ng generate
command to create a new component namedhero-detail
.ng generate component hero-detail
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)
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;
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>
Services are a great way to share information among classes that don't know each other.
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.
The HeroService could get hero data from anywhere such as a web service, local storage, or a mock data source.
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
getHeroes(): Hero[] {
return HEROES;
}
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',
})
import { HeroService } from '../hero.service';
heroes: Hero[] = [];
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();
}
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();
}
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.
import { Observable, of } from 'rxjs';
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.
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);
}
ng generate component messages
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
ng generate service message
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.1. Import the MessageService
into the HeroService
(src/app/hero.service.ts (import MessageService))
import { MessageService } from './message.service';
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
this.messageService.add('HeroService: fetched heroes');
return heroes;
}
6.3.2.1. Import MessageService
into MessagesComponent
(src/app/messages/messages.component.ts (import MessageService))
import { MessageService } from '../message.service';
constructor(public messageService: MessageService) {}
The messageService
property must be public because you're going to bind to it in the template.
<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>
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);
}
}
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.
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
.
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 { }
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.
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. TheforRoot()
method supplies the service providers and directives needed for routing and performs the initial navigation based on the current browser URL.
AppRoutingModule
exports RouterModule to be avilable throughout the application.
exports: [ RouterModule ]
7.2. RouterOutlet
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>
ng generate component dashboard
<h2>Top Heroes</h2>
<div class="heroes-menu">
<a *ngFor="let hero of heroes">
{{hero.name}}
</a>
</div>
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));
}
}
/* 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;
}
import { DashboardComponent } from './dashboard/dashboard.component';
...
{ path: 'dashboard', component: DashboardComponent },
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
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.
Remove the inner HTML of
<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>
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);
}
}
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.
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.
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.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>
goBack(): void {
this.location.back();
}
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
HttpClient
is Angular's mechanism for communicating with a remote server over HTTP.import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
...
HttpClientModule,
],
})
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.
npm install angular-in-memory-web-api --save
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 }
)
ng generate service InMemoryData
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;
}
}
import { HttpClient, HttpHeaders } from '@angular/common/http';
constructor(
private http: HttpClient,
private messageService: MessageService) { }
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
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.
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', []))
);
}
...
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);
};
}
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', []))
);
}
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}`))
);
}
<button type="button" (click)="save()">save</button>
save(): void {
if (this.hero) {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}
/** 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'))
);
}
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' })
};
<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>
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
/** 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'))
);
}
<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();
}
/** 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'))
);
}
/* 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', []))
);
}
<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>
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>
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)),
);
}
}
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 importFormsModule
, 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.