CRUD are the basic principles of persistent storage - Create, Read, Update, and Delete resources.
In this chapter, we will create a simulation of a data server. The data will be provided to the HeroService
via HTTP requests helped from Angular's HttpClient
. On the end of this tutorial, you will know how to add, edit and delete heroes.
HttpClient
The HttpClient
is an injectable class that is used to perform HTTP requests.
We will use HttpClient
to make a simulation of communication with the server over HTTP.
Open the AppModule
and import the HttpClientModule
from @angular/common/http
. Also, do not forget to put it into the @NgModule.imports
array.
Let install the "angular-in-memory-web-api" with stable version:
npm install angular-in-memory-web-api@0.5.4 --save
If we want to make a real HTTP request we should use HttpClient
. More details about the right approach to make a HTTP request in Angular you can find on the official site.
The next step is to import the HttpClientInMemoryWebApiModule
and the InMemoryDataService
class. We will define the second class in the next steps. Open the AppModule
and add these changes:
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
Into the @NgModule.imports
add this after the HttpClientModule
:
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
We should create this InMemoryDataService
class in src/app/in-memory-data.service.ts
file with the following content:
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ 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};
}
}
Our app should work normally now. We clear the mock-heroes.ts
. We follow the single responsibility principle and when the server is ready you can detach the In-memory Web API and attach to the real server.
HeroesComponent
We should import HttpClient
and HttpHeaders
from '@angular/common/http'
.
import { HttpClient, HttpHeaders } from '@angular/common/http';
Now create the private property named http
:
constructor(
private http: HttpClient,
private messageService: MessageService) { }
The Message notification will stay, wrapping it into a private log
method that shows messages. Add this method to the HeroService
:
private log(message: string) {
this.messageService.add('HeroService: ' + message);
}
We continue in the same file with adding property that defines the url - heroesUrl
that contains the address of the resources on the server - private heroesUrl = 'api/heroes';
.
We have used RxJS of()
so far, now let's transform the getHeroes()
method to use HttpClient
:
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
}
The application should work after these changes because we receive the Observable<Hero[]>
type of data that is the same as before. We are using HttpClient.get()
method to retrieve the data. More details of the available options and methods of the HttpClient
can be found in the official site.
The data is returned from the `get()` method as an untyped JSON object by default but we are applying the optional type and receive the result in that type.
One more common case is to retrieve the data in an object and dig that data out by processing the `Observable` result with the RxJS `map` operator.
Tap
methodWithout making any changes in the values of the array heroes
we "tap" this observable
values and send a message with HeroService.log()
method. We should import the following methods from 'rxjs/operators'
:
import { tap } from 'rxjs/operators';
Also, need to add changes to the getHeroes()
method:
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`))
);
}
Important! In the real situation an error can appear and for this case it should handled manually. This will not be part of this tutorial.
getHero
by idIt is a typical case to use get by id to receive a specific record. The request will be something like api/hero/:id
. Let's add the needed changes depending on the HttpClient
into the getHero()
method:
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`))
);
}
We will add a new jqxButton with content "save". We have already imported the jqxButtonComponent
(#messagetemplate "Import jqxButtonComponent") and it is not necessary to do it again. Open the HeroDetailComponent
(src/app/hero-detail/hero-detail.component.html
) and add this:
<jqxButton (click)="save()" [theme]="'material'">save</jqxButton>
Let's add the relevant save()
method to the src/app/hero-detail/hero-detail.component.ts
:
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
This source code update one particular Hero
with the updateHero()
method of the HeroService
.
The next thing is to add updateHero()
method into the HeroService
. It is almost the same method as getHeroes()
but it uses http.put()
to save the changes into the data-base.
What is included in the HttpClient.put()
method:
Here the URL is unchanged. We know which hero to update by its id
.
As a third option, the API expects a special header in HTTP save request. We will define it in additional constant httpOptions
in the HeroService
:
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
The updateHero()
method will look as this:
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`))
);
}
What to do if we want to add a new hero? The typical situation is to add the new record.
In our case, we should define the new hero name. This can done in one input
.
We will add <jqxInput>
in the heroes.component.html
template and also one button to confirm the changes:
<jqxInput #heroName></jqxInput>
<jqxButton style="margin: 20px;" (onClick)="add(heroName.val())">add</jqxButton>
The #heroName
attribute is a reference to this concrete jqxInput
. We will use it to get the name of the new hero that we want to add. We use the val()
method of the jqxInput
directly into the add
event and get the current value typed in it.
Also, we should implement the response of the onClick
event on jqxButton
:
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
// Clearing the jqxInput
this.heroNameInput.val(null);
});
}
This gets the typed name and the handler creates it as a Hero
object. After that passes it to the service's addHero()
.
We need to implement HeroService.addHero() method to the HeroService
:
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`))
);
}
The differences between the addHero()
and the updateHero()
are that addHero()
HttpClient.post()
instead of put()
id
when it adds the new hero
Next step is to add an option to delete a record which is a normal situation.
We will make a few changes to the HeroesComponent
template and also its class.
First open the src/app/heroes.component.html
and replace the (onRowclick)="rowdetails($event)"
with this (onCellclick)="cellclick($event)"
. With this event, we could determine on which cell the user have clicked. More about the events you can find in the API Documentation. Also, we should add one more column that we will use to set button with which we will delete specific hero
by its id
. The HeroesComponent
template should look like this:
<h2>My Heroes</h2>
Hero name:
<jqxInput #heroName [theme]="'material'" [height]="25"></jqxInput>
<jqxButton style="margin: 20px;" (onClick)="add(heroName.val())" [theme]="'material'" [height]="30">add</jqxButton>
<jqxGrid #grid
(onCellclick)="cellclick($event)"
[width]="360"
[showheader]="false"
[autoheight]="true"
[theme]="'material'">
<tr>
<th>Id</th>
<th>__Name</th>
<th>del</th>
</tr>
<tr *ngFor="let hero of heroes">
<td>
<a routerLink="/detail/{{hero.id}}">
{{hero.id}}
</a>
</td>
<td>
{{hero.name}}
</td>
<td>
x
</td>
</tr>
</jqxGrid>
We add one additional column with name del
to be easy to recognize it - as a new <th>del</th>
tag. About the value of this column will use one similar ('x') symbol.
Also, in the heroes.component.css
will add the following style settings:
jqxgrid [role="gridcell"]:nth-of-type(3) .jqx-grid-cell-left-align {
margin-top: 2px ;
padding: 5px 0 5px 5px;
border-radius: 4px;
width: 90%;
height: 55%;
color: lightgray;
background-color: #607D8B;
font-weight: 800;
text-align: center;
}
We will replace the rowdetails
with the new event named onCellclick
. The difference with the old one is that we will compare where exactly the user clicks on the jqxGrid
- event.args.datafield == "del"
. The del
is the name of the new column. If the user clicks on it will activate HeroService.deleteHero()
method which will add afterwards.
The next step is to implement this delete
functionality to the heroes.component.ts
:
cellclick(event: any): void {
let args = event.args;
let rowData = args.row.bounddata;
let heroID = rowData.Id;
if (args.datafield != "del") {
this.router.navigate(['./detail/' + heroID.toString()]);
} else {
let hero: Hero = null;
this.heroes = this.heroes.filter(h => {
if (h.id !== heroID) {
return h;
} else {
hero = h; // The hero that will be deleted
}
});
this.heroService.deleteHero(heroID).subscribe();
}
}
Into the deleteHero()
method of the HeroService
we delegate the hero
that coincides the id
of clicked id
with the hero from this.heroes
array. We filtered this array but we must subscribe with the Observable
although there is nothing for the component to do.
You should replace the "rowdetails" with the "cellclick" with the changes above.
It is time to add HeroService.deleteHero() method like this:
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`))
);
}
We use the HttpClient.delete
instead of before where we used put
and post
to send data to the server. The URL is composited by "heroes resource" URL and the id
of the hero.
After all these changes the application is done and it should work fine.
It is possible to implement new features depending on the case and make our application richer - this is a personal decision.
post()
, put()
and delete()
.This tutorial follows the official Angular "Tour of Heroes" tutorial. Its structure and techniques are used to recreate it with the implementation of the jQWidgets components.
Final Result
Dashboard:
Heroes List:
The final code you can find here.