Creating and sending data to the IndexedDB database
It would make for a very poor experience when using the application if we could not save details for use the next time we came back to it. Fortunately, newer web browsers provide support for something called IndexedDB, which is a web browser-based database. Using this as our data store means that the details will be available when we reopen the page.
While we are working with the database, we have two distinct areas that we need to bear in mind. We require code to build the database table and we require code to save the records in a database. Before we start writing the database table, we are going to add the ability to describe what our database looks like, which will be used to build the database.
Next, we will create a fluent interface to add the information that ITable exposes:
export interface ITableBuilder {
WithDatabase(databaseName : string) : ITableBuilder;
WithVersion(version : number) : ITableBuilder;
WithTableName(tableName : string) : ITableBuilder;
WithPrimaryField(primaryField : string) : ITableBuilder;
WithIndexName(indexName : string) : ITableBuilder;
}
The idea behind fluent interfaces is that they allow us to chain methods together so that they can be read in an easier fashion. They encourage the idea of keeping method operations together, making it easier to read what is happening to an instance because the operations are all grouped together. This interface is fluent because the methods return ITableBuilder. The implementations of these methods use return this; to allow the chaining of operations together.
The other side of building the table is the ability to get the values from the builder. As we want to keep our fluent interface purely dealing with adding the details, we are going to write a separate interface to retrieve these values and build our IndexedDB database:
export interface ITable {
Database() : string;
Version() : number;
TableName() : string;
IndexName() : string;
Build(database : IDBDatabase) : void;
}
While both of these interfaces serve different purposes and will be used by classes in different ways, they both refer to the same underlying code. When we write the class that exposes these interfaces, we are going to implement both the interfaces in the same class. The reason for doing this is so that we can segregate how they behave depending on which interface our calling code sees. Our table building class definition looks as follows:
export class TableBuilder implements ITableBuilder, ITable {
}
Of course, if we tried to build this right now, it would fail because we haven't implemented either of our interfaces. The code for the ITableBuilder portion of this class looks like this:
private database : StringOrNull;
private tableName : StringOrNull;
private primaryField : StringOrNull;
private indexName : StringOrNull;
private version : number = 1;
public WithDatabase(databaseName : string) : ITableBuilder {
this.database = databaseName;
return this;
}
public WithVersion(versionNumber : number) : ITableBuilder {
this.version = versionNumber;
return this;
}
public WithTableName(tableName : string) : ITableBuilder {
this.tableName = tableName;
return this;
}
public WithPrimaryField(primaryField : string) : ITableBuild
this.primaryField = primaryField;
return this;
}
public WithIndexName(indexName : string) : ITableBuilder {
this.indexName = indexName;
return this;
}
For the most part, this is simple code. We have defined a number of member variables to hold the details, and each method is responsible for populating a single value. Where the code does get interesting is in the return statement. By returning this, we have the ability to chain each method together. Before we add our ITable support, let's explore how we use this fluent interface by creating a class to add the personal details table definition:
export class PersonalDetailsTableBuilder {
public Build() : TableBuilder {
const tableBuilder : TableBuilder = new TableBuilder();
tableBuilder
.WithDatabase("packt-advanced-typescript-ch3")
.WithTableName("People")
.WithPrimaryField("PersonId")
.WithIndexName("personId")
.WithVersion(1);
return tableBuilder;
}
}
What this code does is create a table builder that sets the database name to packt-advanced-typescript-ch3 and adds the People table to it, setting the primary field as PersonId and creating an index in this named personId.
Now that we have seen the fluent interface in action, we need to complete the TableBuilder class by adding the missing ITable methods:
public Database() : string {
return this.database;
}
public Version() : number {
return this.version;
}
public TableName() : string {
return this.tableName;
}
public IndexName() : string {
return this.indexName;
}
public Build(database : IDBDatabase) : void {
const parameters : IDBObjectStoreParameters = { keyPath : this.primaryField };
const objectStore = database.createObjectStore(this.tableName, parameters);
objectStore!.createIndex(this.indexName, this.primaryField);
}
The Build method is the most interesting one in this part of the code. This is where we physically create the table using the methods from the underlying IndexedDB database. IDBDatabase is the connection to the actual IndexedDB database, which we are going to retrieve when we start writing the core database functionality. We use this to create the object store that we will use to store our people records. Setting keyPath allows us to give the object store a field that we want to search in, so it will match the name of a field. When we add indexes, we can tell the object store what fields we want to be able to search in.