© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
V. K. KotaruBuilding Offline Applications with Angularhttps://doi.org/10.1007/978-1-4842-7930-4_8

8. Creating the Entities Use Case

Venkata Keerti Kotaru1  
(1)
-, Hyderabad, Telangana, India
 

While working with data, you began with data retrieval. You used remote HTTP services to make GET calls and show the data on the screen in an Angular application. You created the ability to cache resources including data calls. As you progress, applications need to create, update, and delete data. In an application that supports offline functionality, the create, update, and delete actions are complex.

This chapter establishes a use case for handling such scenarios. It details how to build Angular components and services that perform the create action. The example can be easily upgraded to edit and delete actions. The previous chapter introduced IndexedDB for persisting data in the browser. It is typically used for managing cache. The use case described in this chapter helps take full advantage of the IndexedDB implementation. As you proceed further in the book, the next chapter details how to perform offline actions with IndexedDB, so you need to understand the use case we will build in this chapter.

In Web Arcade, the create, update, and delete actions are performed on the game details page. The page will call remote HTTP services to save data. The chapter begins by explaining the HTTP methods for the previously mentioned actions. Next, it details how to create the component. It introduces Angular routing for navigation between the list component, which shows a list of board games and the details page. Next, it details the features we build on the game details page while developing the use case. Finally, the chapter details how to build mock server-side services to support the Angular application.

Web Arcade: Game Details Page

The game details page shows the details of a selected game. We use this page to showcase an example of how to synchronize offline actions with a remote HTTP service.

While invoking a remote HTTP service, the HTTP methods define a desired action to be performed. Consider the following most used HTTP methods:
  • GET retrieves data. For example, it retrieves a list of board games.

  • POST submits or creates an entity. For example, it creates a service to create a board game or add user comments on a game that implements the POST method.

  • PUT replaces or fully updates an entity. For example, consider a board game entity with fields for a game ID, the game title, a game description, a web link with comprehensive details about the game, the origin, etc. For a given game ID, use a PUT method to replace all the fields. Even if a few fields do not change, you can provide the same values again while using the PUT method.

  • PATCH replaces or updates a few fields on an entity. In the previous example, consider updating just the origin field. Develop an HTTP service with PATCH to update the origin field on an entity.

  • DELETE removes or deletes an entity.

Note

In addition to the previous HTTP methods, there are other less used HTTP methods including HEAD, CONNECT, OPTIONS, and TRACE.

So far, offline access was provided to GET service calls. This chapter uses IndexedDB to cache and synchronize POST service calls. You may use a similar implementation on the remaining HTTP methods.

In an earlier chapter, you created a component to show a list of games. In this chapter, you will update the sample so that a click selects a game to navigate to the details page. See Figure 8-1.
Figure 8-1

Navigation to a game details page

The game details page has a description and additional details about a game. It lists comments from all the users. It provides a form at the bottom to submit a new comment. See Figure 8-2.
Figure 8-2

Fields on game details page

Offline Scenario

A user can submit comments. When online, the service calls an HTTP service to post new comments. However, if offline, use IndexedDB to save the comments temporarily. Once back online, post the comments to the remote service.

Creating a Component for Game Details

Run the following Angular CLI command to create a new component:
ng g c game-details
In the next few sections, you will update the component to show the game details. However, the game was selected in the earlier game list component. How does the game details component know about the selected game? Remember, you navigate to the game details as the user selects from a list of board games. The list component provides the selected game ID as a query param in the URL. Consider the following for a URL with the game ID parameter:
http://localhost:4200/details?gameId=1

Routing

Angular routing enables Angular applications to take advantage of the URL (see the address bar in a browser) and load content on the fly. You map a component to a path in the URL. The component loads as the user navigates to the respective path.

Remember, when you created a new application for Web Arcade with Angular CLI, routing was already set up. This includes an AppRoutingModule for configuring custom paths and URLs. Update the route configuration to show the game list component first and the game details component when navigating to the details page (as shown at the previously mentioned URL). Consider the route configuration in app-routing.module.ts, as shown in Listing 8-1.
06: const routes: Routes = [{
07:   path: "home",
08:   component: BoardGamesComponent
09: }, {
10:   path: "details",
11:   component: GameDetailsComponent
12: }, {
13:   path: "",
14:   redirectTo: "/home",
15:   pathMatch: "full"
16: }];
17:
18: @NgModule({
19:   imports: [RouterModule.forRoot(routes)],
20:   exports: [RouterModule]
21: })
22: export class AppRoutingModule { }
23:
Listing 8-1

Route Configuration

Notice that the board games component is configured to load with the path /home, and the game details component is configured to load with the path /details, for example, http://localhost:4200/home and http://localhost:4200/details.

The components load at router-outlet in the HTML template. Remember, AppComponent is the root component. Update the router outlet to load the previously mentioned components as and when the user navigates to the respective URLs (paths). Consider the following short snippet:
1: <div class="container align-center">
2:     <router-outlet></router-outlet>
3: </div>

Navigate to Game Details Page

Next, update the list component (BoardGamesComponent) to navigate to the details page. Edit the component’s HTML template (src/app/components/board-games/board-games.component.html). See Listing 8-2.
01: <mat-toolbar color="primary">
02:     <mat-toolbar-row>Game List</mat-toolbar-row>
03: </mat-toolbar>
04: <div>
05:     <ng-container *ngFor="let game of (games | async)?.boardGames">
06:         <a (click)="gameSelected(game)">
07:             <mat-card>
08:                 <mat-card-header>
09:                     <h1>
10:                         {{game.title}}
11:                     </h1>
12:                 </mat-card-header>
13:                 <mat-card-content>
14:                     <span>{{game.alternateNames}}</span>
15:                     <div>{{game.origin}}</div>
16:                     <div>{{game.description}}</div>
17:                 </mat-card-content>
18:             </mat-card>
19:         </a>
20:     </ng-container>
21: </div>
Listing 8-2

Board Games Component Template

Consider the following explanation:
  • See lines 6 and 19. Each game (card) is enclosed in a hyperlink element, <a></a>.

  • Notice that line 5 iterates through a list of games with the ngFor directive. The variable game represents a game in the iteration.

  • In line 6, the click event is handled by the gameSelected() function. Notice the game variable is passed in as a parameter. This is the variable with game data in the current iteration.

The gameSelected function (defined in the game details component’s TypeScript file) navigates to the game details page, as shown in Listing 8-3.
01:
02: export class BoardGamesComponent implements OnInit {
03:
04:   constructor(private router: Router) { }
05:
06:   gameSelected(game: BoardGamesEntity){
07:    this.router.navigate(['/details'], {queryParams: {gameId: game.gameId}})
08:   }
09:
10: }
Listing 8-3

Navigate to the Details Page

Consider the following explanation:
  • A router service instance is injected in line 4.

  • Line 7 uses a router instance to navigate to the details page.

  • Notice that a query param game ID is provided. The game object is passed in from the template. See the earlier Listing 8-2.

  • The game-id selected in the current list component (BoardGamesComponent) will be used in the game details component. It retrieves complete details of the selected game.

Next, retrieve the game ID from the URL in the game details component, as shown in Listing 8-4.
01: import { Component, OnInit } from '@angular/core';
02: import { ActivatedRoute } from '@angular/router';
03:
04: @Component({
05:   selector: 'wade-game-details',
06:   templateUrl: './game-details.component.html',
07:   styleUrls: ['./game-details.component.sass']
08: })
09: export class GameDetailsComponent implements OnInit {
10:   game: BoardGamesEntity;
      commentsObservable = new Observable<CommentsEntity[]>();
11:   constructor(private router: ActivatedRoute, private gamesSvc: GamesService) { }
12:
13:   ngOnInit(): void {
14:     this.router
15:       .queryParams
16:       .subscribe( r =>
17:         this.getGameById(r['gameId']));
18:   }
19:
20:   private getGameById(gameId: number){
21:     this.gamesSvc.getGameById(gameId).subscribe(
22:       (res: BoardGamesEntity) => {
23:         this.game = res;
24:         this.getComments(res?.gameId);
25:       });
26:   }
27: }
28:
29: private getComments(gameId: number){
30:     this.commentsObservable = this.gamesSvc.getComments(gameId);
31: }
32:
Listing 8-4

Retrieve Game ID from Query Params

Consider the following explanation:
  • The sample uses the ActivatedRoute service to read query params in the URL.

  • Line 2 imports the ActivatedRoute service from Angular’s router module. Next, line 11 injects the service into the component. The service instance is named router.

  • See the ngOnInit() function between lines 13 and 18. This function is invoked after the constructor and during the component initialization.

  • See the code between lines 14 and 17. Notice that the code uses router.queryParams. The queryParams is an observable. Subscribe to it to access the query params.

  • The result of the queryParams subscription is named r. Access the game ID as a field on the result, r['gameId']. Now, you have access to the game ID provided by BoardGamesComponent.

  • Pass the game ID as a function parameter to a private function, getGameById(). This function is defined in lines 20 to 27.

  • The getGameById() function invokes another function with the same name getGameById() defined as part of GamesService. It returns an observable, and subscribing to it returns results from an HTTP service. The remote HTTP service provides game details by GameId.

  • In line 23, you set the results from the HTTP service onto a game object on the component, which is used in the HTML template.

  • The HTML template shows the game details to the user.

  • Next, call the private function getComments(), which retrieves comments on the given board game. See lines 29 to 31. It invokes the getComments() function on the GameService instance, which obtains data from a remote HTTP service.

  • Set results from the HTTP service onto a commentsObservable object on the component, which is used in the HTML template. The HTML template shows the comments.

In summary, Listing 8-4 retrieves the game details and comments and sets them on a class variable. In line 23, the class field game has selected the game title, description, etc. Next, on line 30, the class field commentsObservable has a list of comments. These are comments made by various users on the selected game. Next, see the HTML template code that renders the game details and comments. Consider Listing 8-5 .
01:
02: <!-- Toolbar to provide a title-->
03: <mat-toolbar [color]="toolbarColor">
04:     <h1>Game Details</h1>
05: </mat-toolbar>
06:
07:
08: <!-- This section shows game title and description-->
09: <div *ngIf="game">
10:     <h2>{{game.title}}</h2>
11:     <div>{{game.description}}</div>
12: </div>
13:
14:
15: <!-- Following section shows comments made by users -->
16: <div>
17:     <strong>
18:         Comments
19:     </strong>
20:     <hr />
21:     <mat-card *ngFor="let comment of commentsObservable | async">
22:         <mat-card-header>
23:             <strong>
24:                 {{comment.title}}
25:             </strong>
26:         </mat-card-header>
27:         <mat-card-content>
28:             <div>{{comment.comments}}</div>
29:             <div><span>{{comment.userName}}</span> <span class="date">{{comment.timeCommented | date}}</span></div>
30:         </mat-card-content>
31:     </mat-card>
32: </div>
Listing 8-5

Game Details Component HTML Template

Consider the following explanation:
  • Lines 10 and 11 show the game title and description. Line 9 checks if the game object is defined (with an ngIf directive). This is to avoid errors with the component before the game data is obtained from the service. As you can imagine, when the component first loads, the service call is still in progress. The game title, description, and other fields are not yet available. Once retrieved from the service, the ngIf condition turns true, and the data is displayed.

  • See line 21. It iterates through the comments. Line 24 shows the comment title. Lines 28 and 29 show the comment description, username, and comment timestamp.

  • See Listing 8-4. commentsObservable is of type Observable. Hence, line 27 in Listing 8-5 uses | async.

  • Notice the following HTML styling decisions:
    • The listing uses Angular Material’s Toolbar component (mat-toolbar) to show the title. See lines 3 to 5.

    • Each comment is shown on an Angular Material card. See the components mat-card, mat-card-header, and mat-card-content on lines 21 to 31.

Listing 8-4 uses two functions from a service: getGameById() and getComments(). As you can imagine, the Angular service function invokes a remote HTTP service to get the data.

Remember, we developed mock services to demonstrate remote HTTP service functionality. You returned mock JSON for board games. For the previous two functions, getGameById() and getComments(), you will extend the Node.js Express service. It is covered later in the chapter, in the section “Updates to Mock HTTP Services.”

Note

A real-world service integrates with and creates, retrieves, and updates data in mainstream databases such as Oracle, Microsoft SQL Server, or MongoDB. It is out of scope for this book. To ensure the code samples are functional, we created mock services.

However, as you have seen in the previous code samples, the components do not integrate directly with remote HTTP services. You use an Angular service, which uses other services to abstract this functionality from the components. The components purely focus on presentation logic for the application.

Remember, you created a service called GamesService for encapsulating the code that retrieves games data. Next, update the service to include the previous two functions getGamesById() and getComments(), as shown in Listing 8-6.
01: @Injectable({
02:     providedIn: 'root'
03:   })
04:   export class GamesService {
05:
06:     constructor(private httpClient: HttpClient) { }
07:
08:     getGameById(gameId: number): Observable<BoardGamesEntity>{
09:       return this
10:         .httpClient
11:         .get<BoardGamesEntity>(environment.boardGamesByIdServiceUrl,{
12:           params: {gameId}
13:         });
14:     }
15:
16:     getComments(gameId: number): Observable<CommentsEntity[]>{
17:         return this
18:           .httpClient
19:           .get<CommentsEntity[]>(environment.commentsServiceUrl,{
20:             params: {gameId}
21:           });
22:       }
23:
24: }
Listing 8-6

Game Service Invoking Remote HTTP Services

Consider the following explanation:
  • Line 6 injects the HttpClient service. It is an out-of-the-box service provided by Angular to make HTTP calls.

  • See lines 10 and 18. The functions use the HttpClient instance httpClient. It invokes the remote HTTP service.

  • Both the functions use the GET HTTP method. The first parameter is the endpoint URL.

  • It is advisable to configure URLs (instead of hard-coding them in the application). Hence, the URLs are updated in an environment file. See Listing 8-7 for the environment file.

  • Notice that both the functions require gameId as a parameter. See lines 8 and 16.

  • The game is passed as a query param to the remote HTTP service. See lines 12 and 20.

  • Notice that getGameById() returns an observable of type BoardGamesEntity (Observable<BoardGamesEntity>). The remote service is expected to return a JSON response adhering to the interface contract specified in BoardGamesEntity. See Listing 8-8(a) for the interface definition.

  • getComments() returns an observable of type CommentsEntity (Observable<CommentsEntity>). As multiple comments are retrieved from the service, it is an array. The remote service is expected to return a JSON response adhering to the interface contract specified in CommentsEntity. See Listing 8-8(b) for the interface definition.

  • The remote service calls return an observable because they are asynchronous. The service does not return the data as soon as the browser invokes it. The code does not wait until the result is returned. Hence, a subscriber callback function is invoked once the data is available from the remote service.

08: export const environment = {
09:   boardGameServiceUrl: `/api/board-games`,
10:   commentsServiceUrl: '/api/board-games/comments',
11:   boardGamesByIdServiceUrl: '/api/board-games/gameById',
12:   production: false,
13: };
14:
Listing 8-7

Environment File with Additional Endpoints

01: export interface BoardGamesEntity {
02:     gameId: number;
03:     age: string;
04:     link: string;
05:     title: string;
06:     origin: string;
07:     players: string;
08:     description: string;
09:     alternateNames: string;
10: }
Listing 8-8(a)

TypeScript Interface BoardGamesEntity

1: export interface CommentsEntity {
2:     title: string;
3:     comments: string;
4:     timeCommented: string;
5:     gameId: number;
6:     userName:string;
7: }
Listing 8-8(b)

TypeScript Interface CommentsEntity

Note

The URLs are required in two files: src/environments/environment.ts and src/environments/environment.prod.ts. The environment.ts file is used for development builds (for example, yarn start). The environment.prod.ts file is used for production builds (for example, yarn build or ng build).

Adding Comments

See Figure 8-2. Notice the last section with a data form to add a comment. It enables users to add comments about the board game. So far you have largely worked with data retrieval. This is an example of creating an entity, namely, a comments entity. As mentioned earlier, you use the HTTP POST method for creating an entity in the back-end system.

Consider Listing 8-9, which shows the Add Comment HTML template.
01: <div>
02:     <mat-form-field>
03:         <mat-label>Your name</mat-label>
04:         <input matInput type="text" placeholder="Please provide your name" (change)="updateName($event)">
05:     </mat-form-field>
06: </div>
07:
08: <div>
09:     <mat-form-field>
10:         <mat-label>Comment Title</mat-label>
11:         <input matInput type="text" placeholder="Please provide a title for the comment" (change)="updateTitle($event)">
12:     </mat-form-field>
13: </div>
14:
15: <div>
16:     <mat-form-field>
17:         <mat-label>Comment</mat-label>
18:         <textarea name="comment" id="comment" placeholder="Write your comment here" (change)="updateComments($event)" matInput cols="30" rows="10"></textarea>
19:     </mat-form-field>
20: </div>
21:
22: <button mat-raised-button color="primary" (click)="submitComment()">Submit</button>
Listing 8-9

Add Comment HTML Template

Consider the following explanation:
  • Notice lines 1 to 20. They create form fields for the username, title, and comment detail.

  • The listing uses Material Design components and directives. Lines 4, 11, and 18 use matInput with the elements input and text area, respectively. These Angular Material elements need the Material Design input module. See Listing 8-10, lines 1 and 8.

  • The mat-form-field component encapsulates the form field and the label. The component mat-label shows a label for the form field.

  • Lines 4, 11, and 18 use change event data binding with the functions updateName(), updateTitle(), and updateComments(). Listing 8-11 sets the value of a form field to a variable in the component. The change event occurs every time a change occurs (a user types in a value) in the form field.

  • In Listing 8-9, notice the click event data binding in the HTML template on line 22. The TypeScript function submitComments() is called as and when the user clicks the button.

01: import { MatInputModule } from '@angular/material/input';
02:
03: @NgModule({
04:   declarations: [
05:     AppComponent,
06:   ],
07:   imports: [
08:     MatInputModule,
09:     BrowserAnimationsModule
10:   ],
11:   bootstrap: [AppComponent]
12: })
13: export class AppModule { }
14:
Listing 8-10

Import Angular Material Input Module

Consider Listing 8-11 for the component’s TypeScript code. It includes change event handlers and click event handlers in the comments form.
01: import { Component, OnInit } from '@angular/core';
02: import { MatSnackBar } from '@angular/material/snack-bar';
03: import { GamesService } from 'src/app/common/games.service';
04:
05: @Component({
06:   selector: 'wade-game-details',
07:   templateUrl: './game-details.component.html',
08:   styleUrls: ['./game-details.component.sass']
09: })
10: export class GameDetailsComponent implements OnInit {
11:
12:   name: string = "";
13:   title: string = "";
14:   comments: string = "";
15:
16:   constructor( private gamesSvc: GamesService,
17:     private snackbar: MatSnackBar) { }
18:
19:   updateName(event: any){
20:     this.name = event.target.value;
21:   }
22:
23:   updateTitle(event: any){
24:     this.title = event.target.value;
25:   }
26:
27:   updateComments(event: any){
28:     this.comments = event.target.value;
29:   }
30:
31:   submitComment(){
32:       this
33:         .gamesSvc
34:         .addComments(this.title, this.name, this.comments, this.game.gameId)
35:         .subscribe( (res) => {
36:           this.snackbar.open('Add comment successful', 'Close');
37:         });
38:   }
39:
40: }
Listing 8-11

Comments Form Handlers

Consider the following explanation:
  • See lines 19 to 29 for the functions updateName(), updateTitle(), and updateComments(). Remember, they are invoked on change events in the form fields. Notice the function definition uses event.target.value. The event’s target refers to the form field (DOM element). The value returns the data typed in by the user.

  • The values are set to the class variables name (for user name), title (for comment title), and comments (for comment description).

  • The submit button’s click event is data bound to the submitComment() function. See lines 32 to 38. Notice that it invokes the addComments() function on the service GameService instance (gameSvc). On line 16, GameService is injected to be used in the component.

  • Notice the service function requires a list of parameters including the username, title, and description. The values captured earlier (with the change event handler) are passed into the service function.

  • addComments() invokes the server-side HTTP service. If the add comment action is successful, the success callback for the observable is invoked. It shows a success message, providing the feedback on the add comment action.

Listing 8-12 shows the GameService implementation . The listing focuses on the addComments() action.
01: import { Injectable } from '@angular/core';
02: import { HttpClient } from '@angular/common/http';
03: import { environment } from 'src/environments/environment';
04:
05:
06: @Injectable({
07:   providedIn: 'root'
08: })
09: export class GamesService {
10:
11:   constructor(private httpClient: HttpClient) { }
12:
13:   addComments(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
14:     return this
15:       .httpClient
16:       .post(environment.commentsServiceUrl, [{
17:         title,
18:         userName,
19:         timeCommented,
20:         comments,
21:         gameId
22:       }]);
23:   }
24: }
Listing 8-12

GameService Implementation

Consider the following explanation:
  • Line 11 injects the HttpClient service. This is an out-of-the-box service provided by Angular to make HTTP calls.

  • In lines 14 and 22, the functions use the HttpClient instance called httpClient. This invokes the remote HTTP service.

  • In line 16, notice you are making an HTTP POST call. The first parameter is the service URL. Considering that the URL is a configuration artifact, it is updated in the environment file.

  • The second parameter is the POST method’s request body. See Figure 8-5 to understand how the values translate to the request body on the network.

Note

One comments URL is used for two actions, retrieving and creating comments. A RESTful service uses the HTTP method GET for retrieval. For a create action, the same URL is used with the POST HTTP method.

Updates to Mock HTTP Services

The new components need additional data and features from the remote HTTP services. This section details changes to the mock services. In a real-world application, such services and features are developed by querying and updating the database. As it is out of scope for the book, we will develop mock services.

Filtering Game Details by ID

The game details component needs one game detail at a time. Remember, in an earlier chapter, you developed a service to return all the board games. This section details how to retrieve game data by ID.

Remember, we use mock-data/board-games.js for all the board games related endpoints. Add a new endpoint, which retrieves a game by ID. Name it /gameById, as shown in Listing 8-13.
1: var express = require('express');
2: var router = express.Router();
3: var dataset = require('../data/board-games.json');
4:
5: router.get('/gameById', function(req, res, next){
6:     res.setHeader('Content-Type', 'application/json');
7:     res.send(dataset
          .boardGames
          .find( i => +i.gameId === +req.query.gameId));
8: });
Listing 8-13

Filter Game by an ID

Consider the following explanation:
  • Line 7 filters board games by game ID. The statement dataset .boardGames.find( i => +i.gameId === +req.query.gameId) returns the game details with the given ID. Typically, we expect a single game with an ID. In a different scenario, if you anticipate more than one result, use the filter() function instead of find().

  • The results from the find() function are passed in as a parameter to the send() function on the response object (variable name res). This returns the results to the client (browser). See Figure 8-3.
    • See line 3. The mock service retrieves a list of board games from a mock JSON object in the data directory.

    • See line 5. The HTTP method for this filter endpoint is GET.

    • See line 6. The response content type is set to JSON, which is a ready-to-use format for the Angular services and components.

Figure 8-3

Filtering the game by an ID

Note

Notice the + symbol on line 7. This is a way in JavaScript to type case a string to a number.

Retrieving Comments

The game details page lists comments, as shown in Figure 8-2. Notice the list of comments below the game description. This section details how to create a mock server-side service to retrieve comments.

The service returns comments on a given game. It uses a query param for the game ID. Consider an example URL, http://localhost:3000/api/board-games/comments?gameId=1. See Figure 8-4.
Figure 8-4

The comments endpoint

A real-world service integrates with a database to efficiently store and query data. As mentioned earlier, it is a mock service. Hence, it reads comments from a file. Consider Listing 8-14 in mock_sevices/routes/board-games.js. The board-games.js file is appropriate as it includes all the endpoints related to board games. The comments are on a board game.
01: var fs = require('fs');
02: var express = require('express');
03: var router = express.Router();
04:
05: router.get('/comments', function(req, res){
06:     fs.readFile("data/comments.json", {encoding: 'utf-8'},  function(err, data){
07:         let comments = [];
08:         if(err){
09:             return console.log("error reading from the file", err);
10:         }
11:         res.setHeader('Content-Type', 'application/json');
12:         comments = JSON.parse(data);
13:         comments = Object.values(comments).filter( i => {
14:
15:             return +i.gameId === +req.query.gameId
16:         });
17:         res.send(comments);
18:     });
19: });
Listing 8-14

An Endpoint to Retrieve Comments

Consider the following explanation:
  • Line 5 creates an endpoint, which responds to an HTTP method, GET. The get() function on the express router instance enables you to create the endpoint.

  • Line 6 retrieves a current list of comments from a file on disk, data/comments.json.

  • The readFile() function on the fs module (for “file system”) is asynchronous. You provide a callback function, which is invoked as the API successfully reads a file or errors out. In Listing 8-14, notice the callback function between lines 6 and 18.

  • While the first parameter, err, represents an error, the second parameter, data, contains the contents of the file. See lines 8 to 10. If an error is returned, it is logged and returns the control out of the function.

  • Assuming there are no errors reading the file, the file contents include an entire list of comments belonging to all the games in the system. The service is expected to return a comment only for the given game. Hence, you filter the comments by game ID. See the code on lines 13 to 16, which creates a filter. The filter() function is defined on JavaScript array objects. The predicate on line 15 tests each item in the array. The comments with the given game ID are returned.

  • See line 17, which responds with the filtered comments to the client (e.g., browser).

Adding Comments

The game details page allows users to comment, as shown in Figure 8-2. The form has fields for a username, a title, and a detailed comment. This section details how to create a mock server-side service to save comments.

Adding a comment is done through a create action. You are creating a new comment entity. Remember, the POST method is appropriate to create entities. A POST method has a request body, which includes a list of comments created by the Angular application (typed in by an user). See Figure 8-5.
Figure 8-5

The create comments endpoint

Consider Listing 8-15 in mock_sevices/routes/board-games.js. The board-games.js file is appropriate as it includes all the endpoints related to board games. Users are commenting on board games.
01: var fs = require('fs');
02: var express = require('express');
03: var router = express.Router();
04:
05: router.post('/comments', function(req, res){
06:     let commentsData = [];
07:     try{
08:         fs.readFile("data/comments.json", {encoding: 'utf-8'},  function(err, data){
09:             if(err){
10:                 return console.log("error reading from the file", err);
11:             }
12:             commentsData = commentsData.concat(JSON.parse(data));
13:             commentsData = commentsData.concat(req.body);
14:
15:             fs.writeFile("data/comments.json", JSON.stringify(commentsData), function(err){
16:                 if(err){
17:                     return console.log("error writing to file", err);
18:                 }
19:                 console.log("file saved");
20:             });
21:         });
22:         res.send({
23:             status: 'success'
24:         });
25:     }catch(err){
26:         console.log('err2', err);
27:         res.sendStatus(200);
28:     }
29: });
Listing 8-15

A POST Endpoint to Create Comments

Consider the following explanation:
  • Line 5 creates an endpoint, which responds to the HTTP POST method. The post() function on the express router instance enables you to create the endpoint.

  • The endpoint needs to append the new comments to the current list of comments. The file data/comments.json has an array of the current list of comments.

  • The readFile() function on the fs module is asynchronous. You provide a callback function, which is invoked as the API successfully reads a file or errors out. In Listing 8-15, notice the callback function on lines 8 to 21.

  • While the first parameter, err, represents an error, the second parameter, data, has contents of the file. See lines 9 to 11. If an error is returned, it is logged and returns the control out of the function.

  • Assuming there is no error reading the file, line 12 adds comments in the file to a local variable called commentsData.

  • Next, line 13 concatenates a new list of comments on the request object to the commentsData variable. As mentioned earlier, the POST method has a request body. It includes a list of comments provided by the Angular application.

  • The consolidated list of comments are written back to the file. See line 15, which writes the entire list of comments to the file.

Summary

This chapter builds on the Web Arcade use case. It is crucial for understanding the offline function we will build in the next chapter. So far, while working with data, you performed data retrieval and caching. This chapter established a use case for the create, update, and delete scenarios.

Exercise
  • The game details page just shows a title and a description for a board game. However, the mock service and the TypeScript interface include many additional fields including origin, alternate names, recommended number of players, etc. Include the additional fields on the game details page.

  • The add comment functionality shows a Snackbar component message if the action is successful (see Listing 8-11, line 36). The sample does not show a message for an error. Update the code sample to show a Snackbar component alert for errors.

  • Implement a back button on the game details page to navigate back to the list screen.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.195.91