JavaScript: Building a To-Do App (Part 1)
Tags: javascript, tutorial, webdev, database
Published: 2020-07-16 20:29:49

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:
```javascript
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 0<sup>1</sup> to whatever number we pass (`1` being the default), a `onupgradeneeded` event is fired<sup>2</sup> and most importantly, a `versionChange` transaction is created.

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

```javascript
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:
```javascript
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](https://dev-to-uploads.s3.amazonaws.com/i/3qymnv2h5qso5c97wsj4.png)

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:

```javascript
// 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 👋.

<hr/>
<small>1: This is not accurate, but rather an example to make it easier to understand why the `onupgradeneeded` event is fired up</small>
<small>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</small>