8. HTTP | CRUD

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.

What is a 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.

Changes in the 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.

Using Tap method

Without 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.

Transform getHero by id

It 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}`))
    );
}

Update heroes

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}`))
  );
}

Add a new hero

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()

Delete a 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 !important;
        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.

In conclusion

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:

RESULT

Heroes List:

RESULT

The final code you can find here.