JavaScript: Building a To-Do App (Part 1)
Miguel Manjarres
Posted on July 16, 2020
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:
- Create a transaction through the
transaction()
method on our database - Set the mode of the transaction to either
readonly
orreadwrite
- Access the
IDBObjectStore
through the transaction and store it - Use the
IDBObjectStore
to make an asynchronous request (to delete or create something, for example) - Define a behaviour for when the request is fulfilled
- 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.
To get an instance of a cursor, we can follow this steps:
- Grab the
objectStore
instance from the database - Use the
openCursor()
on theobjectStore
, it will perform arequest
and return a newIDBCursorWithValue
object - Define a behaviour for when the request is fulfilled successfully
- 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
Posted on July 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.