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
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.
- 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.
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
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
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.
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
Pass the key of the object to be deleted. ThesequenceID
has same meaning as in earlier methods.
Methods to Get Data
GetData method
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
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
The data that comes back in the OnResult response event is a JS Array containing all the data objects or items in natural order. ThesequenceID
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
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
This is similar to theGetIndexKeys
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
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.
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
andGetIndexKeys
. - Methods that use the properties
ActiveIndex
andIndexDescending
to work accodring to an index. Currently there is only one such method,GetAllObjsByIndex
. However, these properties are also mapped to theTIndexedDbClientDataset
component for easy use of indexes when loading data internally by usingGetAllObjsByIndex
.
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;
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.