JavaScript: Building a To-Do App (Part 1)

devtony101

Miguel Manjarres

Posted on July 16, 2020

JavaScript: Building a To-Do App (Part 1)

If you have already developed dynamic web applications, you are probably familiar with the concepts of window.localStorage and window.sessionStorage, they are great tools that let us save information directly into the browser, but there is a problem, you can only save data in the form of a string, sure, there are some workarounds to this, like using the JSON.stringify() method but, wouldn't it be nice if we could just save the data as an object and retrieve it the same way?

Introduction

This is part one of a four part series where we will build a (yet another) To-Do web application using the IndexedDB API. In this first part however we fill focus on the theory. Specifically we will:

  • Have a brief introduction about what the IndexedDB API is
  • See how can we get an instance of a newly created database
  • Learn about the most relevant objects and functions to perform the CRUD operations

What is the IndexedDB API?

IndexedDB is a low level API that let us save structured-like data, like files and binary-large-objects (blobs). It provides us with methods for both synchronous and asynchronous operations, the latter being the one that web browsers implement.

In order create a brand new database we need to use the open(name, version) method on the indexedDB property of the window object. The open() method receives two parameters:

  • name: The name of the database
  • version: The version to open the database with. It defaults to 1

This returns a IDBOpenDBRequest object on which we can supply a callback for when the request is successfully resolved, and if that's the case, we can store the reference to our database.

The whole process looks something like this:

let indexedDB, dbName, dbVersion;
const dbRequest = window.indexedDB.open(dbName, dbVersion);
dbRequest.onsuccess = () => {
  indexedDB = dbRequest.result;
  console.log("Database created successfully!");
}
// You can also supply a callback for when (and if) something goes wrong
dbRequest.onerror = () => console.error("Something went wrong...");

Great 👏! We now have access to a brand new database, but right now its empty with no model whatsoever, before we can attempt to save something we need to specify a schema and for that we need to create an IDBObjectStore.

Introduction to IDBOjectStore

According to the official documentation:

Is an interface of the IndexedDB API and represents an object store in a database

Think of it as the model in a Relational DataBase, with a major exception, there is no id field. Whenever we want to save a new record, a key must be provided, then the object store will use this key to access the object (like indexes in an array) but, if we truly want to mimic the behavior of a real RDB, we can tell the object store to automatically generate this value for every new object we save by passing an optional optionalParameters object when we first create the object store.

When the object store is successfully created, we can use the instance to create the fields of our model using the createIndex(name, keyPath, parameters) method, each parameter being:

  • name: The name of the field
  • keyPath: The keyPath (name of the key field)
  • parameters: An optional object where we can specify additional properties to our field

Beware: You can only perform changes on the schema in the context of a versionChange transaction. More on transactions later.

When we first open up a request to create a database, we assign a version to it, and because that database didn't exist before, it upgraded its version from 01 to whatever number we pass (1 being the default), a onupgradeneeded event is fired2 and most importantly, a versionChange transaction is created.

The code to create the object store, given an IDBOpenRequest object is as follows:

dbRequest.onupgradeneeded = event => {
  // We retrieve the instance of the database
  const db = event.target.result;
  const objectStore = db.createObjectStore(dbName, {
    keyPath: "key", // Assign a key field to every record
    autoIncrement: true // The key is given by a key generator in a ordered sequence
  }

  // We then create the fields
  objectStore.createIndex("name", "name");
  // ...
}

Wonderful 👏! We now have our database populated with fields (columns), but how do we save (or update, or delete) any record on it?

Introduction to IDBTransaction

According to the official documentation:

Is an interface of the IndexedDB API. It provides a static, asynchronous transaction on a database using event handler attributes. All reading and writing of data is done within transactions

I think no further explanation is needed. To start (and use) a transaction we can follow this five steps:

  1. Create a transaction through the transaction() method on our database
  2. Set the mode of the transaction to either readonly or readwrite
  3. Access the IDBObjectStore through the transaction and store it
  4. Use the IDBObjectStore to make an asynchronous request (to delete or create something, for example)
  5. Define a behaviour for when the request is fulfilled
  6. Define a behaviour for when the transaction is completed

In code, it would look something like this:

let mode = ""; // readonly or readwrite
// Step 1-2
const transaction = indexedDB.transaction([dbName], mode);
// Step 3
const objectStore = transaction.objectStore(dbName);
// Step 4
// We open up the request through the objectStore object, we will see more on this in the next part
let request;
// Step 5
request.onsuccess = () => console.log("Success!")
// Step 6
transaction.onsuccess = () => console.log("Operation was successful");

Excellent 👏! Up to this point, we can do pretty much anything we want with our data, but we have yet to see how can we actually retrieve the information and use it.

Introduction to IDBCursorWithValue

According to the official documentation:

Is an interface of the IndexedDB API. It represents a cursor for traversing or iterating over multiple records in a database

Think of it as a literal cursor that can go in any direction (up and down) across the records.

table_cursor

To get an instance of a cursor, we can follow this steps:

  1. Grab the objectStore instance from the database
  2. Use the openCursor() on the objectStore, it will perform a request and return a new IDBCursorWithValue object
  3. Define a behaviour for when the request is fulfilled successfully
  4. Get the cursor from the event passed to this callback, if it's undefined then there is no data to retrieve

In code, it would look like this:

// Steps 1-2
const objectStore = indexedDB.transaction(dbName).objectStore(dbName);
// Step 3
objectStore.openCursor().onsuccess = event => {
  // Step 4
  const cursor = event.target.result;
  if (cursor) {
    // There is at least one record
  else {
    // There is no data or is the end of the table
  }
}

The onsuccess callback will be fired up for every record on the table.

That's it! Now we have everything we need to start developing our application, we will begin right away in the next chapter.

Thank you so much for reading! If you have questions or suggestions please leave them down below. See you next time 👋.


1: This is not accurate, but rather an example to make it easier to understand why the onupgradeneeded event is fired up
2: The onupgradeneeded event is fired up whenever an attempt to open a database with a version higher than its current version is made and not only when the database it's first created

💖 💪 🙅 🚩
devtony101
Miguel Manjarres

Posted on July 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related