© 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_9

9. Creating Data Offline

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

Earlier in the book, we started integrating IndexedDB in an Angular application. We also established a use case for creating data, namely, user comments, offline. Imagine that a user is on a game details page and attempts to add a comment. However, the user loses network access. The Web Arcade application is resilient because it saves the comments on the client device/browser temporarily. When the user is back online, the application synchronizes the comments online.

This chapter elaborates on how to create data offline. It begins with instructions to identify if the application is online or offline. You use the status to determine how to reach server-side services or use the local IndexedDB store. Next, the chapter details how to add a comment to IndexedDB when offline. It details how to provide feedback to the user that the application is offline but the data is temporarily saved.

Then, the chapter covers how to synchronize offline comments with the server-side services once the application is back online. Remember, the server-side database and the services are the source of truth for the data. IndexedDB is temporary and provides a seamless experience to the user.

Adding Comments Online and Offline

The previous chapter described how to add a comment. The submit action calls the server-side HTTP endpoint. If the device is offline and has lost network connectivity, a typical web application shows an error. Once online, users might have to retry the operation. As mentioned earlier, Web Arcade uses IndexedDB, persists data temporarily, and synchronizes when the remote service is available.

Identifying the Online/Offline Status with a Getter

To identify whether the device (and the browser) is online, use the JavaScript API on the navigator object. It is a read-only property on the window object. The field onLine returns the current status, which is true if online and false if offline.

The developer tools on Google Chrome provide an option to throttle network speeds. This helps applications evaluate their performance and user experience. See Figure 9-1. The tools print the onLine field value on the navigator object. Notice the browser window is throttled offline.
Figure 9-1

Google Chrome Developer Tools, printing the onLine status on the console

Note

You can run a similar command on a browser of your choice. Figure 9-1 shows Google Chrome, which was chosen arbitrarily.

Remember, we created a service called IdbStorageAccessService to encapsulate access to IndexedDB. The online/offline status determines the components that can access IndexedDB. Hence, you should include lines of code to determine the online/offline status in the service.

Inject the Window service into IdbStorageAccessService, as shown in Listing 9-1, line 3.
1: @Injectable()
2: export class IdbStorageAccessService {
3:   constructor(
          private windowObj: Window) {
4:     // this.create();
5:   }
6: }
Listing 9-1

Inject the Window Service

Ensure the Window service is provided. See Listing 9-2, lines 10 and 15, in AppModule for the Web Arcade application. You provide the Window service with a global variable, window, as shown in line 14. This provides access to useful properties such as document, navigator, etc.
01: @NgModule({
02:     declarations: [
03:       AppComponent,
04:       // ...
05:     ],
06:     imports: [
07:       BrowserModule,
08:       // ...
09:     ],
10:     providers: [
11:       IdbStorageAccessService,
12:       {
13:         provide: Window,
14:         useValue: window
15:       }
16:     ],
17:     bootstrap: [AppComponent]
18:   })
19:   export class AppModule { }
Listing 9-2

Provide the Window Service

Create a getter function called IsOnline in IdbStorageAccessService. A service instance may use the IsOnline field to get the status of the browser. The code is abstracted in the service. See Listing 9-3.
1: get IsOnline(){
2:     return this.windowObj.navigator.onLine;
3: }
Listing 9-3

IsOnline Getter as Part of IdbStorageAccessService

Adding Online/Offline Event Listeners

It is possible that you will encounter a scenario where an action needs to be performed when the application goes online or offline. The window object (and hence the window service) provides the events online and offline. Add these events to IdbStorageAccessService at the time of initialization. The event handler callback function is invoked any time the event occurs.

Listing 9-4 prints a message on the browser console including the event data. You can perform an action when an event is triggered. See specifically lines 8 to 11 and lines 13 to 16.
01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   constructor(private windowObj: Window) {
05:   }
06:
07:   init() {
08:     this.windowObj.addEventListener("online", (event) => {
09:       console.log("application is online", event);
10:       // Perform an action when online
11:     });
12:
13:     this.windowObj.addEventListener('offline', (event)=> {
14:         console.log("application is offline", event)
15:         // Perform an action when offline
16:     });
17:   }
18: }
Listing 9-4

Online and Offline Events

Figure 9-2 shows the results.
Figure 9-2

Online and offline events

Adding Comments to IndexedDB

Remember, when needed, we intend to cache comments in IndexedDB. Considering IdbStorageAccessService abstracts the task of accessing the database from the rest of the application, augment the service and add a function that caches comments in IndexedDB. But before we do that, Listing 9-5 shows a quick recap of the service so far.
01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:
06:   constructor(private windowObj: Window) {
07:   }
08:
09:   init() {
10:
11:     let request = this.idb.open('web-arcade', 1);
12:
13:     request.onsuccess = (evt:any) => {
14:       console.log("Open Success", evt);
15:
16:     };
17:
18:     request.onerror = (error: any) => {
19:       console.error("Error opening IndexedDB", error);
20:     }
21:
22:     request.onupgradeneeded = function(event: any){
23:       let dbRef = event.target.result;
24:       let objStore = dbRef
25:         .createObjectStore("gameComments", { autoIncrement: true })
26:
27:       let idxCommentId = objStore.createIndex('IdxCommentId', 'commentId', {unique: true})
28:     };
29:
30:     this.windowObj.addEventListener("online", (event) => {
31:       console.log("application is online", event);
32:       // Peform an action when online
33:     });
34:
35:     this.windowObj.addEventListener('offline', (event) => {
36:         console.log("application is offline", event)
37:         // Perform an action when offline
38:     });
39:
40:   }
41: }
Listing 9-5

IdbStorageAccessService

So far, the service creates a reference to IndexedDB, opens a new database, and creates an object store and an index. Consider the following detailed explanation for Listing 9-5:
  • In line 4, an IndexedDB reference is set on a class variable, namely, idb.

  • Next, in the init() function (which initializes the service and appears on line 11), run the open() function on the idb object. It returns an object of the IDBOpenDBRequest object.

  • If this is the first time a user has opened the application on a browser, it creates a new database.
    1.     a. 

      The first parameter is the name of the database, web-arcade.

       
    2.     b. 

      The second parameter (a value of 1) specifies a version for the database. As you can imagine, new updates to the application cause changes to the IndexedDB structure. The IndexedDB API enables you to upgrade the database as the version changes.

       
For a returning user, the database was already created and available on a browser. The open() function attempts to open the database.
  1. 1.
    The IndexedDB APIs are asynchronous. The open action does not complete in line 11. You provide a function callback for the success and failure scenarios. They are invoked as a result of the open action.
    1. a.

      Notice the onsuccess() function callback on lines 13 to 16, which is invoked if the open database action is successful.

       
    2. b.

      The onerror() function callback on lines 18 to 20 is invoked if the open database action fails.

       
    3. c.

      The open function call returns IDBOpenDBRequest. The previous callback functions onsuccess and onerror are provided to this returned object.

       
     
  2. 2.
    See the code on lines 22 to 28, where onupgradeneeded is triggered after creating or opening IndexedDB. You provide a callback function, which is invoked by the browser when this event occurs. What is the significance of the onupgradeneeded event?
    1. a.

      In the case of a new database, the callback function is a good place to create object stores. In the current use case, you create an object store to save game comments. You name it gameComments.

       
    2. b.

      For a pre-existing database, if an upgrade is required, you may perform design changes here.

       
     
  3. 3.

    Finally, on lines 30 to 38, see the function callback for online and offline events when the browser goes online/offline.

     
The Angular service IdbStorageAccessService needs a reference to the web-arcade database . You use it to create a transaction. With IndexedDB, you need a transaction to perform create, retrieve, update, and delete (CRUD) operations. The statement on line 11, the this.idb.open('web-arcade',1) function call, attempts to open a database, namely, web-arcade. If it’s successful, you can access the database reference as part of the onsuccess() function callback. Consider Listing 9-6.
01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:   indexedDb: IDBDatabase;
06:   init() {
07:
08:     let request = this.idb.open('web-arcade', 1);
09:
10:     request.onsuccess = (evt:any) => {
11:       console.log("Open Success", evt);
12:       this.indexedDb = evt?.target?.result;
13:     };
14:   }
15: }
Listing 9-6

Access the web-arcade Database Reference

Consider the following explanation:
  • See line 5. indexedDB is a class variable accessible across the service.

  • A value is assigned on the successful opening of the web-arcade database, as shown on line 12. The database instance is available in the result variable on the target property of the event object (event.target.result).

Next, add a function to create comments in IndexedDB. This creates an IndexedDB transaction, accesses the object store, and adds a new record. Consider Listing 9-7.
01: addComment(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
02:     let transaction = this.indexedDb
03:       .transaction("gameComments", "readwrite");
04:
05:       transaction.objectStore("gameComments")
06:         .add(
07:           {
08:             title,
09:             userName,
10:             timeCommented,
11:             comments,
12:             gameId,
13:             commentId: new Date().getTime()
14:           }
15:         )
16:
17:
18:       transaction.oncomplete = (evt) => console.log("add comment transaction complete", evt);
19:       transaction.onerror = (err) => console.log("add comment transaction errored out", err);
20:
21:   }
Listing 9-7

Add a New Record in IndexedDB

Consider the following explanation:

  • First, create a new transaction with the class variable indexedDB (created in Listing 9-6). See the transaction function on line 3. It takes two parameters:
    1. a.

       One or more object stores in which a transaction needs to be created. In this case, you create a transaction on an object store gameComments.

       
    2. b.

       Specify the transaction mode, readwrite. IndexedDB supports three modes of transactions, namely, readonly, readwrite, and versionchange. As you can imagine, readonly helps with retrieve operations, and readwrite helps with create/update/delete operations. However, versionchange mode helps create and delete object stores on an IndexedDB.

       
  • Next, perform the add record action on IndexedDB. Use the transaction object to access the object store on which the add action needs to be performed. See line 5, which uses the objectStore function to access the object store().

  • See lines 6 and 15. You store a JavaScript object including the fields for the comment title, username, time commented, comment description, game ID on which the comment was added, and a unique comment ID. To ensure uniqueness, you use a time value. You may use any unique value.

  • As you have seen with IndexedDB, the database actions are asynchronous. The add() function does not immediately add a record. It eventually invokes a success or error callback function. A transaction has the following callback functions:
    1. a.

      oncomplete: This is invoked on success. See line 18. It prints the status on the console.

       
    2. b.

      onerror: This is invoked on error. See line 19.

       
Figure 9-3 shows a record in IndexedDB.
Figure 9-3

A new record in IndexedDB

The User Experience of Adding Comments

Remember from the previous chapter that UserDetailsComponent adds a comment by calling the GameService function named addComments. This invokes the server-side POST call to add a comment. If the application is offline, it will error out. You show an error feedback to the user and request the user to retry.

In this chapter, you have done the background work to cache comments in IndexedDB, if the browser is offline. Next, update the component to check if the application is online or offline and invoke the respective service function. Consider the code snippet in Listing 9-8, which comes from GameDetailsComponent (app/components/game-details/game-details.component.ts).
01: @Component({ /* ... */ })
02: export class GameDetailsComponent implements OnInit {
03:
04:     constructor(private idbSvc: IdbStorageAccessService,
05:         private gamesSvc: GamesService,
07:         private snackbar: MatSnackBar,
08:         private router: ActivatedRoute) { }
09:
10:     submitComment() {
11:         if (this.idbSvc.IsOnline) {
12:             this
13:                 .gamesSvc
14:                 .addComments(/* provide comment fields */)
15:                 .subscribe((res) => {
16:
17:                     this.snackbar.open('Add comment successful', 'Close');
18:                 });
19:         } else {
20:             this.idbSvc.addComment(this.title, this.name, this.comments, this.game.gameId);
21:             this.snackbar.open('Application is offline. We saved it temporarily', 'Close');
22:         }
23:     }
24: }
Listing 9-8

Add a Comment in the Game Details Component

Consider the following explanation:

  • At the beginning, inject IdbStorageAccessService. See line 4. The service instance is named idbSvc.

  • Line 11 checks if the application is online. Notice that you use the IsOnline getter created in Listing 9-3.
    1. a.

       If true, continue to call the game service function, addComments(). It invokes the server-side service.

       
    2. b.

       If offline, use the IdbStorageAccessService function addComment(), which adds the comment to IndexedDB. See the implementation in Listing 9-7.

       
  • Notice on line 21 that you show a Snackbar component message that the application is offline. Figure 9-4 shows the result.

Figure 9-4

Snackbar component alert indicating application is offline

Synchronizing Offline Comments with the Servers

When the application is offline, you cache the comments within the browser in persistent storage using IndexedDB. Eventually, once the application is back online, when the user launches the application again, the comments need to be synchronized with the server side. This section details the implementation to identify that the application is online and synchronize the comment records.

The two events online and offline on window objects are triggered when the browser gains or loses connectivity. The IdbStorageAccessService service includes event handlers for the online and offline events. See Listing 9-4.

Next, update the online event handler. Consider the following steps to synchronize the data with the server-side databases. When the application is back online, you do the following:
  1. 1.

    Retrieve all the cached comments from IndexedDB.

     
  2. 2.

    Invoke a server-side HTTP service, which updates the primary database for the user comments.

     
  3. 3.

    Finally, clear the cache. Delete comments synchronized with the remote service.

     

Let’s begin with the first step in the previous list, retrieving all the cached comments from IndexedDB. The following section details various options and the available API to retrieve data from IndexedDB.

Retrieving Data from IndexedDB

IndexedDB provides the following API for retrieving data:
  • getAll(): Retrieves all records in the object store

As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-only transaction (considering it is a data retrieval operation) on the object store. Call the getAll() API, which returns IDBRequest, as shown in Listing 9-9.

On the IDBRequest object, provide the onsuccess and onerror callback function definitions. As you know, almost all IndexedDB operations are asynchronous. The data retrieval with getAll() does not happen immediately. It calls back the provided callback function.
1: let request = this.indexedDb
2: .transaction("gameComments", "readonly")
3: .objectStore("gameComments")
4: .getAll();
5:
6: request.onsuccess = resObject => console.log('getAll results', resObject);
7: request.onerror = err => console.error('Error reading data', err);
Listing 9-9

Using getAll()

See Figure 9-5 for the result. Notice the success handler on line 6 in Listing 9-9. The result variable is named resObject. Result records are available on a result object on the target property on the resObject (resObject.target.result).
Figure 9-5

The getAll() result

  • get(key): Retrieves a record by key. The get() function is run on an object store.

Similar to getAll(), create a read-only transaction for get() on the object store. The get() API returns IDBRequest, as shown in Listing 9-10.

Rest of the code handling the result or an error is the same. On the IDBRequest object, provide the onsuccess and onerror callback function definitions. As you know, almost all IndexedDB operations are asynchronous. The data retrieval with get() does not happen immediately. It calls back the provided callback function.
1: let request = this.indexedDb
2:  .transaction("gameComments", "readonly")
3:  .objectStore("gameComments")
4:  .get(30);
5:
6: request.onsuccess = resultObject => console.log('get() results', resultObject);
7: request.onerror = err => console.error('Error reading data', err);
Listing 9-10

Using get()

Notice the success handler on line 6 in Listing 9-10. The result variable is named resultObject. Result records are available on a result object on the target property on the resultObject (resultObject.target.result).
  • openCursor(): A cursor allows you to iterate through the results. It lets you act on one record at a time. We chose this option for the comments use case. It provides flexibility to transform the data format as and when you read from IndexedDB. The other two APIs, getAll() and get(), require an additional code loop to transform the data.

As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-only transaction (considering it is a data retrieval operation) on the object store. Call the openCursor() API, which returns IDBRequest.

Again, code handling the result or an error remains the same. On the IDBRequest object, provide the onsuccess and onerror callback function definitions. The data retrieval with openCursor() is asynchronous, which invokes above mentioned onsuccess or onerror callback functions.

Create a new private function to retrieve the cached comment records. Provide an arbitrary name of getAllCachedComments(). Add the private function shown in Listing 9-11 in IdbStorageAccessService.
01: private getAllCachedComments() {
02:     return new Promise(
03:       (resolve, reject) => {
04:         let results: Array<{
05:           key: number,
06:           value: any
07:         }> = [];
08:
09:         let query = this.indexedDb
10:           .transaction("gameComments", "readonly")
11:           .objectStore("gameComments")
12:           .openCursor();
13:
14:           query.onsuccess = function (evt: any) {
15:
16:             let gameCommentsCursor = evt?.target?.result;
17:             if(gameCommentsCursor){
18:               results.push({
19:                 key: gameCommentsCursor.primaryKey,
20:                 value: gameCommentsCursor.value
21:               });
22:               gameCommentsCursor.continue();
23:             } else {
24:               resolve(results);
25:             }
26:           };
27:
28:           query.onerror = function (error: any){
29:             reject(error);
30:           };
31:
32:       });
33:   }
Listing 9-11

Retrieve Cached Comments from IndexedDB

Consider the following explanation:
  • The function creates and returns a promise. See line 2. Considering the data retrieval is asynchronous, you cannot instantly return comment records from the getAllCachedComments() function . The promise is resolved once the cursor finishes retrieving data from IndexedDB.

  • Lines 9 and 12 create a read-only transaction, access object store gameComments, and open a cursor. This statement returns an IDBRequest object, which is assigned to a local variable query.

  • Remember, the onsuccess callback is invoked if the cursor is able to retrieve data from the object store. Otherwise, the onerror callback is invoked (lines 28 and 30).

  • See onsuccesscallback() defined in lines 14 to 26.

  • Access the results at event.target.result. See line 16.

Note

The ?. syntax in evt?.target?.result performs a null check. If a property is undefined, it will return null, instead of throwing an error and crashing the entire function workflow. The previous statement may return the results or null.

  • If the results are defined, transform the data to key-value pair format. Add the object to a local variable called result.

  • Remember, the cursor works on a single comment record at a time (unlike get() and getAll()). To move the cursor to the next record, call the continue function on the query object. Remember, the query object is an IDBRequest object returned by openCursor().

  • The if condition on line 17 results in a true value until all the records in the cursor are exhausted.

  • When false, as the entire dataset (comment records) is retrieved and added to the local variable result, resolve the promise. The calling function uses the results successfully resolved from getAllCachedComments() .

This completes the first step among the three described earlier, listed here:
  1. 1.

    Retrieve all the cached comments from the IndexedDB.

    Next, let’s proceed to the other two steps:

     
  2. 2.

    Invoke a server-side HTTP service, which updates the primary database for the user comments.

     
  3. 3.

    Finally, clear the cache. Delete comments synchronized with the remote service.

     

Bulking Updating Comments on the Server Side

A user might have added multiple comments while the application was offline. It is advisable to upload all the comments in a single call. The server-side HTTP POST endpoint /comments accepts an array of comments.

Remember, the Angular service GameService (src/app/common/game.service.ts) encapsulates all the game-related service calls. Add a new function, which accepts an array of comments and makes an HTTP POST call. Similar to earlier service calls, the new function uses an HttpClient object to make a post call. See Listing 9-12 for the new function addBulkComments (the function name is arbitrary). See lines 9 and 18.
02: @Injectable({
03:   providedIn: 'root'
04: })
05: export class GamesService {
06:
07:   constructor(private httpClient: HttpClient) { }
08:
09:   addBulkComments(comments: Array<{title: string,
10:     userName: string,
11:     comments: string,
12:     gameId: number,
13:     timeCommented: Date}>){
14:     return this
15:       .httpClient
16:       .post(environment.commentsServiceUrl, comments);
17:
18:   }
19: }
Listing 9-12

Add Bulk Comments

Note

The function addBulkComments() uses anonymous data type as a parameter. The comments variable is of type Array<{title: string, userName: string, comments: string, gameId: number, timeCommented: Date}>. The highlighted type is without a name. You may use this technique for one-off data types.

You may choose to create a new entity and use it.

The service function is now available, but it has not been called yet. However, you have the service function available to bulk update the cached comments. Before we start using this function, consider adding a function to delete.

This completes the second step as well. Now you have the code to retrieve cached comments from IndexedDB and call a server-side service for synchronizing the offline comments.
  1. 1.

    Retrieve all the cached comments from the IndexedDB.

     
  2. 2.

    Invoke a server-side HTTP service, which updates the primary database for the user comments.

     
  3. 3.

    Finally, clear the cache. Delete comments synchronized with the remote service.

     

Next, add code to clean up IndexedDB.

Deleting Data from IndexedDB

IndexedDB provides the following API for deleting data from IndexedDB:
  • delete(): Removes records in the object store. This selects the record to be deleted by its record ID.

As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-write transaction on the object store. Call the getAll() API, which returns IDBRequest.

On the IDBRequest object, provide the onsuccess and onerror callback function definitions. As mentioned earlier, almost all IndexedDB operations are asynchronous. The delete operation does not happen immediately. It calls back the provided callback function, as shown in Listing 9-13. Notice that it returns a promise. The promise is resolved if the delete action is successful. See line 10. If it fails, the promise is rejected. See line 14.
01: deleteComment(recordId: number){
02:     return new Promise( (resolve, reject) => {
03:       let deleteQuery = this.indexedDb
04:             .transaction("gameComments", "readwrite")
05:             .objectStore("gameComments")
06:             .delete(recordId);
07:
08:       deleteQuery.onsuccess = (evt) => {
09:         console.log("delete successful", evt);
10:         resolve(true);
11:       }
12:       deleteQuery.onerror = (error) => {
13:         console.log("delete successful", error);
14:         reject(error);
15:       }
16:     });
17:   }
Listing 9-13

Using delete()

Include the previous function in IdbStorageAccessService. Remember, this service encapsulates all actions related to IndexedDB. Now, you have the code for all three steps described for synchronizing offline comments.
  1. 1.

    Retrieve all the cached comments from the IndexedDB.

     
  2. 2.

    Invoke a server-side HTTP service, which updates the primary database for the user comments.

     
  3. 3.

    Finally, clear the cache. Delete comments synchronized with the remote service.

     
Notice that these service functions are available, but they are not yet triggered when the application comes back online. Earlier in the chapter, the service IdbStorageAccessService includes an event handler for the online event . It is called when the application comes back online. Update this event handler to synchronize offline comments. Consider Listing 9-14 to be updated in IdbStorageAccessService.
01: this.windowObj.addEventListener("online", (event) => {
02:     this.getAllCachedComments()
03:     .then((result: any) => {
04:       if (Array.isArray(result)) {
05:         let r = this.transformCommentDataStructure(result);
06:         this
07:           .gameSvc
08:           .addBulkComments(r)
09:           .subscribe(
10:             () => {
11:               this.deleteSynchronizedComments(result);
12:             },
13:             () => ({/* error handler  */})
14:           );
15:       }
16:     });
17:   });
Listing 9-14

The Online Event Handler

Consider the following explanation:

  • First, you retrieve all cached comments. See line 2, which calls the getAllCachedComments() service function. See Listing 9-11 to review retrieving cached comments from IndexedDB.

  • The function returns a promise. When the promise is resolved, you have access to the comment records from IndexedDB. You use this data to add comments in the back end, synchronizing server-side services and databases.

  • Before you call the server-side service, transform the comment record to the request object structure. You loop through all the comments and change the field names as required by the server-side service.
    1. a.

       Listing 9-15 defines a private function called transformCommentDataStructure() . Notice the forEach() on the array of comments obtained from the IndexedDB object store. The comments are transformed and added to a local variable, comments. This is returned at the end of the function.

       
  • Next call the GameService function addBulkComments() , which in turn calls the server-side service. To review the addBulkComments() function, see Listing 9-12.

  • Remember, the function addBulkComments() returns an observable. You subscribe to the observable, which has handlers for success and failure. The success handler indicates the comments are added/synchronized with the server side. Hence, you can now delete cached comments in IndexedDB.

  • Invoke a private function deleteSynchronizedComments() defined as part of the service IdbStorageAccessService. It loops through each comment record and deletes the comments from the local database. See Listing 9-16 for the deleteSynchronizedComments() function definition.
    1. a.

       Notice that the forEach loop uses an anonymous type with a key-value pair. See line 3 (r: {key: number; value: any}). It defines the expected structure for the comments data.

       
    2. b.

      deleteComment() deletes each record by its ID. To review the function again, see Listing 9-13.

       
01: private transformCommentDataStructure(result: Array<any>){
02:     let comments: any[] = [];
03:     result?.forEach( (r: {key: number; value: any}) => {
04:         comments.push({
05:           title: r.value.title,
06:           userName: r.value.userName,
07:           comments: r.value.comments,
08:           gameId: r.value.gameId,
09:           timeCommented: new Date()
10:         });
11:     });
12:     return comments ;
13: }
Listing 9-15

Transform Comments Data

1: private deleteSynchronizedComments(result: Array<any>){
2:     result
3:       ?.forEach( (r: {key: number; value: any}) => this.deleteComment(r.key));
4:   }
Listing 9-16

Delete Synchronized Comments

Now, you have synchronized offline comments with the server side. See Listing 9-17, which includes the event handler for handling the online event and private functions that orchestrate the synchronization steps.
01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:   indexedDb: IDBDatabase;
06:
07:   constructor(private gameSvc: GamesService, private windowObj: Window) {
08:   }
09:
10:   init() {
11:     let request = this.idb
12:       .open('web-arcade', 1);
13:
14:     request.onsuccess = (evt:any) => {
15:       this.indexedDb = evt?.target?.result;
16:     };
17:
18:     request.onupgradeneeded = function(event: any){
19:         // Create object store for game comments
20:     };
21:
22:     this.windowObj.addEventListener("online", (event) => {
23:       console.log("application is online", event);
24:       this.getAllCachedComments()
25:       .then((result: any) => {
26:         if (Array.isArray(result)) {
27:           let r = this.transformCommentDataStructure(result);
28:           this
29:             .gameSvc
30:             .addBulkComments(r)
31:             .subscribe(
32:               () => {
33:                 this.deleteSynchronizedComments(result);
34:               },
35:               () => ({/* error handler  */})
36:             );
37:         }
38:       });
39:     });
40:
41:     this.windowObj.addEventListener('offline', (event) => console.log("application is offline", event));
42:
43:   }
44:
45:   private deleteSynchronizedComments(result: Array<any>){
46:     result?.forEach( (r: {key: number; value: any}) => {
47:       this.deleteComment(r.key);
48:     });
49:   }
50:
51:   private transformCommentDataStructure(result: Array<any>){
52:       let comments: any[] = [];
53:       result?.forEach( (r: {key: number; value: any}) => {
54:           comments.push({
55:             title: r.value.title,
56:             userName: r.value.userName,
57:             comments: r.value.comments,
58:             gameId: r.value.gameId,
59:             timeCommented: new Date()
60:           });
61:       });
62:       return comments ;
63:   }
64:
65:   deleteComment(recordId: number){
66:     // Code in the listing 9-13
67:   }
68:
69:   private getAllCachedComments() {
70:     // Code in the listing 9-11
71:   }
72:
73: }
Listing 9-17

Synchronized Comments with Online Event Handler

Updating Data in IndexedDB

IndexedDB provides the following API for updating data in IndexedDB:
  • put(): Updates records in the object store. This selects the record to be updated by its record ID.

As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-write transaction on the object store. Call the put() API, which returns IDBRequest.

On the IDBRequest object, provide the onsuccess and onerror callback function definitions. As mentioned, almost all IndexedDB operations are asynchronous. The data retrieval with put() does not happen immediately. It calls back the provided callback function, as shown in Listing 9-18.
01: updateComment(recordId: number, updatedRecord: CommentEntity){
02:     /* let updatedRecord = {
03:         commentId: 1633432589457,
04:         comments: "New comment data",
05:         gameId: 1,
06:         timeCommented: 'Tue Oct 05 2021 16:46:29 GMT+0530 (India Standard Time)',
07:         title: "New Title",
08:         userName: "kotaru"
09:     } */
10:
11:     let update = this.indexedDb
12:         .transaction("gameComments", "readwrite")
13:         .objectStore("gameComments")
14:         .put(updatedRecord, recordId);
15:
16:     update.onsuccess = (evt) => {
17:         console.log("Update successful", evt);
18:     }
19:     update.onerror = (error) => {
20:         console.log("Update failed", error);
21:     }
22: }
Listing 9-18

Update Records in IndexedDB

Consider the following explanation:
  • You create a new function to update comments. Imagine a form that allows users to edit a comment. The previous function can perform this action.

    Note The current use case does not include an edit comment use case. The previous function is for demonstrating the put() API on IndexedDB.

  • Notice the commented lines of code between lines 2 and 9. This provides an arbitrary structure for updated comment data. However, the calling function provides the updated comment in an updatedRecord variable.

  • See line 14. The put function takes two parameters.
    1. a.

      updatedRecord: This is the new object to replace the current one.

       
    2. b.

      recordId: This identifies the record to be updated by the second parameter, recordId.

       

Summary

This chapter provided an elaborate explanation for adding records to IndexedDB. In the Web Arcade use case with the game details page, the application allows users to add comments offline. The data is temporarily cached in IndexedDB, which is eventually synchronized with server-side services.

Exercise
  • You have seen how to use the put() API to update a record in IndexedDB. Add the ability to edit comments. If the application is offline, provide the ability to temporarily save edits in IndexedDB.

  • Notice that the deleteComment() function deletes records one at a time. Provide error handling to identify and correct failures.

  • Provide a visual indicator when the application is offline. You may choose to change the color of the toolbar and the title.

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

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