The Practical Client

Storing Lots of Data on the Client in JavaScript and TypeScript

IndexedDB allows you to store data on the client to let the user work offline and to reduce demands on the server. Here's enough code to both get you started and to show you the difference between doing it in JavaScript and doing it in TypeScript.

In a recent post I discussed accessing Local Storage from JavaScript. One of the readers for that column suggested that I also look at IndexedDB, another storage option included in the HTML5 specification (and supported in all the recent versions of the major browsers). So, in part, that's what this column is about.

But this is also a column for the "TypeScript-curious." If you're wondering what you would do differently if you switched from JavaScript to TypeScript, this column will show you (and, quite frankly, there's not much that you have to do differently). In this column, I'll give you the code you need in both languages to check for IndexedDB support and set up a table to store data in.

I'm using Visual Studio 2015 Community Edition for this column. This means that, after setting up my project, I just had to use NuGet to add the DefinitelyTyped definition file for jQuery so that my TypeScript code would know how to deal with jQuery. If you're using an earlier version of Visual Studio, you may need to install TypeScript from Visual Studio's Tools | Extensions and Updates menu. I didn't have to add a definition file to support IndexedDB because Visual Studio 2015 picks that up through the lib.d.ts file that's included with Visual Studio's installation (you don't even have to add the lib.d.ts file to your project).

One last caveat before we get started: I only tested this code in Chrome and Microsoft Edge so some tweaks to this code may be required to work in other browsers.

Setting Up to Use IndexedDB
If you're going to store a large amount of data on the user's computer then the right client-side storage tool is the IndexedDB. Not only does IndexedDB not have the size limitations of I discussed in my Local Storage article, IndexedDB allows you to create indexes that will speed searches through big tables.

IndexedDB also provides more direct support for complex record types than Local Storage (Local Storage is, fundamentally, just about storing key/value pairs). The case study that I'll use for this column is an HTML page that allows the user to enter a customer's Id, which I'll use to retrieve Customer information from IndexedDB. When I retrieve that customer information, the form will display the customer's first name and city, which the user can then update and save back to the database. In this column, all I'll show is how to get the database configured when the user visits the page for the first time.

The first step in working with IndexedDB is to check to see if the browser supports it. If the browser does, I store a reference to IndexedDB in a variable to save some typing later on (I've called the variable dbs because, to me, IndexedDB functions like a database server). Here's what the JavaScript to do that looks like, inside the jQuery ready method to ensure the code doesn't run until the page is fully displayed:

var dbs;
$(function () {
    if ("indexedDB" in window && window.indexedDB != undefined) {
        dbs = window.indexedDB; 
   }
    else {
        alert("No support for indexedDB")
    }
});

If you want to check how your page behaves without indexedDB support, try opening your page in your Micorsoft Edge's 'private' mode.

To replace this code with TypeScript code you first have to put your code in a TypeScript file (you don't have to put your JavaScript code in a separate file, though that's now considered a best practice). To add that file to your project all you need to do is right-click on your project's Script's folder in Solution Explorer and select Add | TypeScript file.

To have that file included in your page, just click on the TypeScript file in Solution Explorer and drag it into your HTML page. You should end up with a script tag that points to the JavaScript file that will be generated from your TypeScript code. My tag looked something like this (I keep the script files for my application in a subfolder called App under my project's Scripts folder):

<script src="Scripts/App/IndexedDBSampleTS.js"></script>

Converting to TypeScript: Why You Care
Because TypeScript is a superset of JavaScript, the JavaScript code that I showed earlier is valid TypeScript code. However, if you're going to switch to TypeScript, you should take advantage of TypeScript's datatyping support. To do that, I add a datatype to the dbs variable that holds my reference to IndexedDB. The only change to my previous code looks like this:

var dbs: IDBFactory;
$(function () {
    if ("indexedDB" in window && window.indexedDB != undefined) {
...

If I'm in doubt about datatype to assign to my dbs variable, I can just hover my mouse over window.indexedDB in my code and Visual Studio will provide a tooltip giving me the right datatype.

So, what difference does this make to my code? To begin with, the next developer who uses my code will have some clue about what that the dbs variable will be used for as soon as they see it (rather than hunting for the statement that sets the variable to window.IndexedDB). In addition, if I inadvertently try to set dbs to anything other than window.IndexedDB, my TypeScript code won't compile and I'll get a helpful error message (something like "Type 'string' is not assignable to type 'IDBFactory'"). That beats running my code, waiting for something odd to happen, and then figuring out what the problem is.

But the real benefit is in the IntelliSense support I get. With the TypeScript version of the code, the IntelliSense list for the dbs variable has three items: The three methods I can call from an IDBFactory. With the JavaScript code, the dbs variable's IntelliSense list has over four dozen items, virtually all of them irrelevant to using IndexedDB.

Let me be clear: I can program without IntelliSense and compile-time code checks. I just don't want to.

Setting up the Database
Like a database server, IndexedDB allows you to have multiple databases. My first step, therefore, is to set up the database I want to store my data in and add tables to it.

To set up a database, I try to open the database by calling the IDBFactory's open method, passing the name of my database (you can also pass a version number to support incremental changes to the database). This actually creates and returns an asynchronous request object. The request itself is queued up by the IndexedDB to be performed, eventually and asynchronously. To have code executed, you attach functions to the request object.

If the database that you asked for doesn't yet exist on the user's computer then the IndexedDB will run whatever function you store in the request object's onupgradeneeded property. Your onupgradeneeded function will be passed an event object which your function should accept.

This means that the start of my JavaScript code to initialize a database called CustomerOrder looks like this (because I use the database name in multiple places in my code, I've stored it a constant):

const dbCustOrdName = "CustomerOrder";
function InitializeDB() {
    var req = dbs.open(dbCustOrdName);
    req.onupgradeneeded = function (e) {

Again, the TypeScript code doesn't look much different except that I provide a datatype for my constant and for my request object (there's no good datatype for the event parameter, so I just use the default type of any):

const dbCustOrderName: string = "CustomerOrder";
function ResetDB() {
    dbs.deleteDatabase(dbCustOrderName);
    var req: IDBOpenDBRequest;
    req = dbs.open(dbCustOrderName);
    req.onupgradeneeded = function (e: any) {

By the time your onupgradeneeded function is executed, IndexedDB will have created your database which you can access through the request object's result property. The request object itself is in the target property of the event object passed to your onupgradeneeded function. Since, in SQL-Server-speak a database within a server is called a catalog, I've given this variable the name cat. Once I have the database, I pass it to a method that will add a table to it.

That JavaScript code looks like this:

req.onupgradeneeded = function (e) {
  var cat;
  cat = e.target.result;
  AddCustomersTable(cat);

As you probably expect, the TypeScript code looks the same but the cat variable now has a datatype:

req.onupgradeneeded = function (e: any) {
var cat: IDBDatabase;
cat = e.target.result;      
AddCustomersTable(cat);

One word of warning: Because this is asynchronous code, the values you store in variables in the onupgradeneeded function aren't going to be available to the rest of your code. Don't, for example, try to use this cat variable outside of the onupgradeneeded function (or some function called from your onupgradeneeded function).

Adding a Table/ObjectStore
In IndexedDB-speak, a database contains ObjectStore...but ObjectStores sure look like tables to me so I'm going to prefix any variable that points to an ObjectStore with tbl. To create an ObjectStore/table you call the database's createObjectStore function, passing the name of your table and any options you want to apply. In the following code the only option I've used is keyPath which specifies the attribute/property/column (pick your favorite term) that uniquely identifies each item in the ObjectStore. In my case, for my customers table, that attribute/property/column is called CustId.

The code to add my customers table and define my "primary key" looks like this in JavaScript:

const tblCustName = "customers";
const fldCustId = "CustId";

function AddCustomersTable(cat) {
  var tblLocal;
  var rowDef; 

  rowDef = { keyPath: fldCustId };
  tblLocal = cat.createObjectStore(tblCustName, rowDef);
}

And, as you can see, it doesn't look much different in TypeScript:

const tblCustName: string = "customers";
const fldCustId: string = "CustId";

function AddCustomersTable(cat: IDBDatabase) {
  var tblLocal: IDBObjectStore;
  var rowDef: IDBObjectStoreParameters;

  rowDef = { keyPath: fldCustId };
  tblLocal = cat.createObjectStore(tblCustName, rowDef);
}

Regardless of which language you chose, you now have a client-side database ready for you to add data to (and I'll look at how to do that in my next column). I imagine that experienced JavaScript developers regard the datatyping as extra work; server-side developers may regard the datatyping as a fundamental right. Certainly, if you do write your code in TypeScript, you'll have better IntelliSense support, better compile-time support, and fewer opportunities to make a mistake.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube