Skip to content

TWebIndexedDbClientDataSet

Description

The component TIndexedDbClientDataset makes it easy for a Delphi web application to create and use IndexedDB databases by a familiar syntax of using ClientDataSet. It allows a seamless integration of an IndexedDB database with data-aware components like TWebDBGrid. All the database operations, including the creation of fields can be done in the standard Delphi way through the TIndexedDbClientDataset component. All you need to do is specify the IndexedDB database properties and add the fielddefs by code in a standard Delphi syntax. Then connect a DataSource and Data components to it and it starts working. It even creates the database if it doesn’t exist. Below is a list of the most important properties and methods of TWebIndexedDbClientDataSet component.

Properties of TWebIndexedDbClientDataSet

Property Description
Active Set this to True to activate the ClientDataSet. The IndexedDB database specified by IDB* properties is automatically created if it doesn’t exist.
IDBActiveIndex Set the name of the index to be made active. Once this is done, on next Refresh or Active, the ClientDataSet loads the objects in the order of the active index.
IDBAutoIncrement Set a True or False value to indicate that the primary key field is auto incremented (default True)
IDBDatabaseName Set the name of the database
IDBIndexDescending Set a True value to indicate descending order of the active index
IDBKeyFieldName Set the name of the primary key field
IDBObjectStoreName Set the name of the ObjectStore in the database
OnIDBError This is an event property to get notified of any errors. The event can be set up at design time in Object Inspector by double-clicking on it.
OnIDBAfterUpdate This event is triggered after an asynchronous IndexedDB operation was successfully executed.

Methods of TWebIndexedDbClientDataSet

Only methods specific to IndexedDB are listed. Other methods from the base ClientDataSet class continue to work as before.

Refresh

procedure Refresh;

Refresh reloads all the objects from the IndexedDB database. If an IDBActiveIndex has been specified, the objects are loaded in the order of that index. In addition, the current record pointer is restored after the Reload.

AddIDBIndex

Use AddIDBIndex to add one or more permanent indexes to the IndexedDB database. Make these calls before you make the component active. The index will be added only if the database does not exist and the component creates it. The call is ignored for an existing database.

procedure AddIDBIndex(
 anIndexName: String;
 fields: String;
 isUnique: Boolean=False);
Where

  • anIndexName - the name to be given to the index
  • fields - the field name on which index is to be built. To specify more than one field names, separate the names with semicolons
  • isUnique - Specify whether the field value is unique in each object (row)

Notes: - The call is ignored for an existing database. - Use the properties IDBActiveIndex and IDBIndexDescending to activate an index for data loading.

TIndexedDb (Advanced Use)

A Delphi web application can use the class TIndexedDb to directly create and use IndexedDB databases. Using this class needs a knowledge of Pas2Js classes TJSArray, TJSObject and their basic syntax in Delphi Pascal. We will try to describe them briefly in “Understanding how the data is stored and retrieved.” But you may skip the rest of this document if you are not interested in a low-level direct access to IndexedDB API.

Description

The original IndexedDB API is low-level and asynchronous. The class TIndexedDb provdies a simpler interface with methods to perform IndexedDB operations where the results are communicated to the specified Delphi event procedures of the application. When several operations are issued simultaneously where the same event procedure of the application gets the response, the application may want to identify the exact operation that was completed. For this purpose, the library provides a way for the application to pass custom data that comes back with the response and helps the application associate a response with an operation. Below is a list of the most important properties methods and events of TIndexedDb class.

Properties

Property Description
ActiveIndex Set the name of the index to be made active. Once this is done,the method GetAllObjsByIndex returns all objects in the order of the active index.
AutoIncrement Set a True or False value to indicate that the primary key field is auto incremented (default True)
DatabaseName Read Only property, get the Database Name
IndexDescending Set a True value to indicate descending order of the active index
KeyFieldName Read Only property, get the Primary Key Field Name
ObjectStoreName Read Only property, get the Object Store Name

Methods and Events of TIndexedDb

The methods and events are listed in their logical order so that they are easier to understand. This is so because IndexedDB only supports asynchronous operation. For each call, there is an event in which the resposne comes. Hence, describing the events along with the methods makes more sense.

Create and Destroy

These are the standard Delphi methods.

  constructor Create(AOwner: TComponent);
  destructor Destroy;

Open or Create an IndexedDB database

Open an IndexedDB database. The database is created if it doesn’t exist.

procedure Open(
            aDbName: String;
            objectStoreNameToCreate: String;
            KeyFieldName: String = '';
            autoIncrement: Boolean = False;
            sequenceID: Integer = 0);

Where

  • aDbName - the database name
  • objectStoreName - the object store name
  • KeyFieldName - the primary key field name. If non-empty, it means an in-line key otherwise an out-of-line key. These terms are described in the section “Understanding how the data is stored and retrieved” below.
  • autoIncrement - Specify whether the primary key field is AutoIncrement. This works for a new database only. For an existing database, it is ignored and if different a warning is shown
  • sequenceID - This is an optional ID to be passed to identify the response of this open in case multiple opens are issued simultaneously.

OnResult: Response Event of all methods like Open that perform operations on IndexedDB

Before calling open, you need to setup the OnResult event property to point to an event procedure. The same event gets responses of other operations too.

Format of the event procedure for OnResult:

procedure DoOnResult
          (success: Boolean;
          opCode: TIndexedDbOpCode;
          data: JSValue;
          sequenceId: JSValue;
          errorName: String;
          errorMsg: String);

where

  • success - indicates a True or False
  • opCode - helps identify the operation. Can be one of the following opcodes: opOpen, opAdd, opPut, opDelete, opGet, opGetAllKeys, opGetAllObjs, opGetAllIndexKeys, opGetAllObjsByIndex.
  • data - The data is sent back in the event for all the operations except for Open, Put or Delete. The data type of the parameter is JSValue. Depending on the operation, you need to cast it to proper data that you are expecting. This is further explained in the description of each operations later.
  • sequenceId - Id from the original call in order to identify the response in case of multiple operations of the same type issued simultaneously. Note that this is also of the type JSValue. So it is upto the application to send any kind of information to identify an operation. For example, it can even send a JS Object in place of sequence id having more information than just an Id number.
  • errorName - If you see the documentation of any operation in the original IndexedDB docs, you will the errors mentioned under an error name. That name is passed here in case you want to code some logic based on a particular error.
  • errorMsg - error messa ge if success is False

After the success of an open, the application may decide to issue other operations like add, get, etc.

Understanding how the data is stored and retrieved

Before discussing the Add, Get and other procedures that add or get data, we need to understand how the Data is stored and retrieved in IndexedDB. IndexedDB is a NoSQL, object database. In a relational database, a table stores a collection of rows. In IndexedDB, an ObjectStore stores a collection of JavaScript objects.

A brief introduction to a JavaScript Object

A JavaScript or JS object is a collection of named values. We will call these “properties” in further discussion. If you know OOP terms, a JS object is equivalent to a dictionary or map. You can have any number of properties representing “fields” of data. Further, there can be nested objects. So the value of a property can be a JS object and so on to any level deep. In TMS Web Core, you can create a JS object with a syntax similar to the following:

var
  aDataObj: TJSObject;
begin
  aDataObj := TJSObject.New;
  aDataObj['name'] := 'John';
  aDataObj['age'] := 44;

The above code creates a JS object with 2 properties.

Objects are stored by unique Primary Key

In IndexedDB, a JS object is stored and accessed by a primary key that is unique. You need to specify a primary key at the time of creating an ObjectStore in IndexedDB. This was the parameter KeyFieldName described above for the Open procedure. There are 2 types of primary keys in IndexedDb.

In-line key specification

When you pass a non-empty KeyFieldName to Open, it is an in-line key specification. The term “in-line” means the JS Object itself contains the primary key property by that name. EXAMPLE: Suppose you pass KeyFieldName as ‘id’ to the Open procedure that creates the ObjectStore. Then it means that when calling an Add procedure, you should pass the data as a JS Object that contains a property by the name ‘id’ having a unique key value. You are responsible for putting that property value in the JS Object before passing it to the Add procedure.

Exception: The only exception is if you also specified AutoIncrement primary key for the ObjectStore when creating it with Open. In that case, you need not add the key property. IndexedDB fills it after adding the object by using its internal key generator. Note that the TIndexedDbClientDataset component described earlier internally uses an in-line key.

Out-of-line key specification If you pass an empty string as KeyFieldName property to the Open procedure, it means the ObjectStore is created with an out-of-line key. This means that the JS Object does not have an implicit (in-line) key property. Rather, you are supposed to pass a unique primary key value as a separate parameter (out-of-line) to Add procedure when adding data.

What is the use of out-of-line keys? You can have the data as any JS data type. In in-line keys, the data must be a JS Object. In out-of-line keys, the data can be anything, for instance, an integer or a string. For example, a simple picture application might use an out-of-line primary key as the name of a picture file and the data as the “binary” stream of the picture.

Methods to Add Data

AddData method

procedure AddData(
  data: JSValue;
  sequenceID: JSValue = 0);
Data parameter

  • Database created with in-line key and AutoIncrement - Pass a JS Data Object. After the Add completes a key property will be created with the key value generated internally.
  • Database created with in-line key and non-AutoIncrement - Pass a JS Data Object that must contain the key as a property having a unique value.
  • Database created with out-of-line key and AutoIncrement - Pass any kind of data, even primitive data types are possible.
  • Database created with out-of-line key and non-AutoIncrement - You can not use this method in this case. Use PutData method described below.

sequenceID parameter - Optional. Pass any value or object that will help you identify a particular add operation out of many in the OnResult event. Or, you can also pass any data as sequenceID that will help in processing the outcome in OnResult event.

What comes back in OnResult data

The key value that was generated in case of AutoIncrement comes back as data parameter of the event procedure.

PutData method

procedure PutData(
  akey: JSValue;
  data: JSValue;
  sequenceID: JSValue = 0);

Use this method only if the database was created with out-of-line key specification. Parameters - aKey - For Add operation, you must pass a unique key value. For Modify, pass the value of an existing key.

  • data - Pass any kind of data, even primitive data types can be passed.

  • sequenceID - Optional. Pass any value or object that will help you identify a particular PutData operation out of many in the OnResult event. Or, you can also pass any data as sequenceID that will help in processing the outcome in OnResult event.

Methods to Modify or Update Data

PutData method

There are 2 variations of this method. One is already described above for out-of-line key specification. When you pass an existing key value to that PutData, it acts as a Modify operation.

The second variation is without a key.

procedure PutData(
  data: JSValue;
  sequenceID: JSValue = 0);
Use this method only if the database was created with in-line key specification. In this case, the data must be a JS Data Object and an existing key value must be passed as a key property to modify that object or record.

DeleteData method

procedure DeleteData(
  akey: JSValue;
  sequenceID: JSValue = 0);
Pass the key of the object to be deleted. The sequenceID has same meaning as in earlier methods.

Methods to Get Data

GetData method

procedure GetData(
  akey: JSValue;
  sequenceID: JSValue = 0);
Pass the key of the object to fetch. The object comes back as data in OnResult response event. The sequenceID has same meaning as in earlier methods.

GetKeys method

procedure GetKeys(
  sequenceID: JSValue = 0);
The data that comes back in the OnResult response event is a JS Array containing all the keys in natural order. The sequenceID has same meaning as in earlier methods.

GetAllObjs method

procedure GetAllObjs(
  sequenceID: JSValue = 0);
The data that comes back in the OnResult response event is a JS Array containing all the data objects or items in natural order. The sequenceID has same meaning as in earlier methods.

Methods to Get Data by an Index

How permanent indexes are created in IndexedDB is described in the next section. Here, you will find a description of the methods that get data in the order of a particular index.

GetIndexData method

procedure GetIndexData(
  indexPropertyName: String;
  akey: JSValue;
  sequenceID: JSValue = 0);

This is similar to the GetData method described earlier except that you also pass the name of an index to use and pass the key as the field value used in that index to fetch the data.

GetIndexKeys method

procedure GetIndexKeys(
  indexPropertyName: String;
  sequenceID: JSValue = 0);
This is similar to the GetIndexKeys method described earlier except that you also pass the name of an index for which you want to get a list of keys.

GetAllObjsByIndex method

procedure GetAllObjsByIndex(
  sequenceID: JSValue = 0); 

This method is the most useful that is used by ClientDataSet to load the data. It is similar to GetAllObjs described earlier. But it uses the value of 2 properties of the class to determine the index and the order of the objects returned:

  • ActiveIndex - Set this to the index name to use for the order of objects. If this is set to empty string (default), the objects are returned in the order of the primary key index.
  • IndexDescending - Set this to False (default) to get the objects in ascending order. Set to True to get them in Descending order.

Setting up the Indexes

AddIndex

Use AddIndex to add one or more permanent indexes to the IndexedDB database. Make these calls before you open the database. The index will be added only if the database does not exist and hence is created on open. The AddIndex call is ignored for an existing database.

procedure AddIndex(
            anIndexName: String;
            fields: String;
            isUnique: Boolean=False);

Where

  • anIndexName - the name to be given to the index
  • fields - the field name on which index is to be built. To specify more than one field names, separate the names with semicolons
  • isUnique - Specify whether the field value is unique in each object (row)

Notes: The call is ignored for an existing database.

Using indexes

This is described earlier under “Methods to Get Data by an Index”. There are two types of methods.

  • Methods that require Index Name as a parameter. These are GetIndexData and GetIndexKeys.
  • Methods that use the properties ActiveIndex and IndexDescending to work accodring to an index. Currently there is only one such method, GetAllObjsByIndex. However, these properties are also mapped to the TIndexedDbClientDataset component for easy use of indexes when loading data internally by using GetAllObjsByIndex.

Handling Asynchronous behavior of IndexedDB

If you do a Modify, Delete or Insert, there is a new event that if assigned gets a notification after it completes.

It is ONIDBAfterUpdate and the signature is as follows.

TKeyId = record
  value: JSValue;
end;

 TIDBAfterUpdateEvent = procedure(success: Boolean; opCode:
TIndexedDbOpCode; keyId: TKeyId; errorName, errorMsg: String) of
object;
Opcode specifies a modify, insert or delete enum. KeyId is the key of the new or modified record in case you want to use it.

Here is a sample code that adds 5 records on a button press.

var
  addNum: Integer = 1;
  countAdded: Integer = 0;

procedure TForm1.btAddMultipleRecordsClick(Sender: TObject);
begin
  countAdded := 0;
  IndexedDBClientDataSet.ONIDBAfterUpdate := DoAfterInsert;
  IndexedDBClientDataSet.Insert;
  IndexedDBClientDataSet.FieldByName('descr').AsString := Format('Task %d',[addNum]);
  IndexedDBClientDataSet.FieldByName('status').AsString := cbTaskStatus.Text;
  IndexedDBClientDataSet.FieldByName('due_date').AsDateTime := pickTaskDate.Date;
  IndexedDBClientDataSet.Post;
end;

procedure TForm1.DoAfterInsert(success: Boolean;
  opCode: TIndexedDbOpCode; keyId: TKeyId; errorName, errorMsg: string);
begin
  if not Success then
  begin
    ShowMessage('Error: '+ errorMsg);
    IndexedDBClientDataSet.ONIDBAfterUpdate := nil;
    Exit;
  end;
  // Do something with ID added if needed
  Console.log(Format('Id of the new record: %d',
[integer(keyId.value)]));
  Inc(countAdded);
  // Add next record
  if countAdded = 5 then
  begin
    ShowMessage('5 records added successfully.');
    IndexedDBClientDataSet.ONIDBAfterUpdate := nil;
    Exit;
  end;
  Inc(addNum);
  IndexedDBClientDataSet.Insert;
  IndexedDBClientDataSet.FieldByName('descr').AsString := Format('Task %d',[addNum]);
  IndexedDBClientDataSet.FieldByName('status').AsString := cbTaskStatus.Text;
  IndexedDBClientDataSet.FieldByName('due_date').AsDateTime := pickTaskDate.Date;
  IndexedDBClientDataSet.Post;
end;

IndexedDB

IndexedDB is a NoSQL database that allows a web application to store anything persistently in the user’s browser. Significant amount of structured data can be stored on the client-side including files and blobs. In addition, it provides indexes for fast searching of this data. Each IndexedDB database is unique to a domain or subdomain. It can not be accessed by any other domain.IndexedDB is available in the latest releases of all browsers supported by TMS WEB Core.

TMS WEB Core IndexedDB Library

TMS WEB Core IndexedDB Library provides two ways to create and use IndexedDB databases.

TIndexedDbClientDataset Component

The component TIndexedDbClientDataset makes it easy for a Delphi web application to create and use IndexedDB databases by a familiar syntax of using ClientDataSet. It also allows a seamless integration of an IndexedDB database with data-aware components like TWebDBGrid. All the database operations, including the creation of fields can be done in the standard Delphi way through the TIndexedDbClientDataset component. Internally, the TIndexedDbClientDataset component uses TIndexedDb class described below to provide this seamless integration thus hiding all the complexity of dealing with asynchronous IndexedDB operations and their responses.

TIndexedDb class

A Delphi web application can also use the class TIndexedDb to directly create and use IndexedDB databases. The original IndexedDB API is low-level and asynchronous. The class TIndexedDb provdies easier methods to perform IndexedDB operations where the results are communicated to the specified Delphi event procedures of the application. However, the use of this class needs a basic knowledge of using JavaScript objects, arrays (Pas2Js syntax) and coding Delphi events without using design time aids.

Your first IndexedDB application

Create a TMS web application

Create a standard TMS Web Application in the Delphi IDE by choosing File, New, Other, TMS Web Application. A new web form is created.

Set up the IndexedDB Client Data Set component

Go to the Tool Palette and select the TWebIndexedDbClientDataset component from the “TMS Web Data Access” section and drop it on the web form.

Specify the IndexedDB Database Properties

Set up the properties of the IndexedDB database in the Object Insector as given below: 1. IDBDatabaseName: NotesDB 2. IDBObjectStoreName: “Notes” 3. IDBKeyFieldName: “id” 4. IDBAutoIncrement: true (default) This tells the component to use the object store “Notes” in the database “NotesDB.” The primary key field for the object store is specified as “id” which is set up as an auto increment key. The component is smart enough to create the database if it doesn’t exist.

Create the Fields or Properties of each object in the Object Store

The fields of the object store need to be set up in the WebFormCreate event code. In the Object Inspector, double-click on OnCreate event of the Web Form. This creates an event handler procedure WebFormCreate. Type the following code in it that sets up the fields and then makes

the DataSet active.

  WebIndexedDbClientDataset1.FieldDefs.Clear;
  WebIndexedDbClientDataset1.FieldDefs.Add('id',ftInteger);
  WebIndexedDbClientDataset1.FieldDefs.Add('note',ftString);
  WebIndexedDbClientDataset1.FieldDefs.Add('date',ftDate);
  WebIndexedDbClientDataset1.Active := True;

Note that special attention is required when using multiple tables in the same IndexedDB database. Due to the asynchronous nature, create a new table and activating it is not happening synchronously. This implies that when creating and activating multiple (new) tables, this needs to be done after each other. For this reason, the IndexedDBClientDataSet.Init method with anonymous method parameter or OnInitSuccess event is provided. Here the dataset can be easily asynchronously activated after initialization and the initialization of multiple tables can be done after each other.

Here is example code initializing a single IndexedDB database with two different tables used by two different datasets:

procedure TMyForm.WebFormCreate(Sender: TObject);
begin
  userds.FieldDefs.Clear;

  userds.FieldDefs.Add('id',ftInteger, 0, true, 3);
  userds.FieldDefs.Add('username',ftString);
  userds.FieldDefs.Add('city',ftString);
  userds.FieldDefs.Add('country',ftString);

  orderds.FieldDefs.Clear;
  orderds.FieldDefs.Add('id',ftInteger, 0, true, 3);
  orderds.FieldDefs.Add('product',ftString);
  orderds.FieldDefs.Add('quantity',ftInteger);
 orderds.FieldDefs.Add('price',ftFloat);
end;

procedure TMyForm.WebFormShow(Sender: TObject);
begin
  userds.Init(
    procedure
    begin
      orderds.Init(
        procedure
        begin
          Userds.Active := true;
          Orderds.Active := true;
        end
       )
     end
);
end;

Add DB-aware components that connect to the DataSet

Now select and drop a TWebDataSource, TWebDBGrid and TWebDBNavigator components on the Web Form.

Set up the DataSource and Data components

Set the DataSource’s DataSet property to WebIndexedDbClientDataset1. Then set the DataSource property of the grid and navigator to point to TWebDataSource1.

Set up the Columns of the DBGrid

Do that by clicking on the Columns properties of the DBGrid as shown in the picture.

Set up a New Record event

There is one last thing to do. Since we will be adding New Records or Objects with the DB Navigator, we need to set up the default values of the record. For this, we set up an OnNewRecord event procedure for the IndexedDB Client Data Set in the Object Inspector and type the following code in it.

procedure TForm1.NewRecord(DataSet: TDataSet);
begin
  DataSet.FieldByName('note').AsString := 'New Note';
  DataSet.FieldByName('date').AsDateTime := Today;
end;

Run the Web Application

Now you can build and run the application. First time, the IndexedDB Client DataSet component will automatically create the database as it doesn’t exist. The DB Grid will appear empty as there are no records. Try adding new records with the Navigator and see how it goes.

Managing the IndexedDB Database

In Chrome, start Developer Tools and then select Application. Then see IndexedDB under Storage section. You will see the NotesDB database in it. Here, you can browse through the records and do other operations. For instance, if you change the fields of the database, you can delete the database itself so that it is recreated with the new fields when you run the application again.

Todo List Demo

Please find this demo in the folder Demos. It shows more advanced usage of using the IndexedDB through the IndexedDbClientDataSet.

Additional features in this Demo

Creating a Permanent Index on a Field

We want to be able to sort on any column of the DB Grid by clicking on the header of the column. So we need to be able to read all the records in the order of that field. For this, we need to create permanent indexes on those fields in IndexedDB. The following code in WebFormCreate event takes care of it.

  IndexedDBClientDataSet.AddIDBIndex('ByDate', 'due_date');
  IndexedDBClientDataSet.AddIDBIndex('ByStatus', 'status');
  IndexedDBClientDataSet.AddIDBIndex('ByDescr', 'descr');
  IndexedDBClientDataSet.IDBActiveIndex := 'ByDate';

The first parameter to AddIDBIndex call is the name that we want to give to an index. The second parameter is the field name. Third parameter is a Boolean specifying isUnique which is OFF by default. Since the fields can contain repeated values, we leave isUnique at default. In order to read the objects in the order of an index, we need to use a code like this:

  IndexedDBClientDataSet.IDBActiveIndex := 'ByDate';
  IndexedDBClientDataSet.IDBIndexDescending := False;
  ....
  IndexedDBClientDataSet.Refresh;

The property IDBActiveIndex specifies the objects to be read in the order of ‘ByDate’ index. Further, the IDBIndexDescending specifies whether the order is Descending or not. The Demo uses this kind of code on the Column Click event of the DB Grid to rekiad it in the desired order. The actual reload is done by the Refresh call. This Demo also shows an example of connecting Data components like CheckBox or a Memo to the database so that those fields can be edited in the current record. After editing, a call to Update from the update button takes care of committing the changes to the IndexedDB. Similarly the Demo has examples of Inserting a new record and Deleting the current record by respective calls.