JavaScript: Building a To-Do App (Part 3)
Tags: javascript, tutorial, webdev, database
Published: 2020-07-24 18:54:03

## 📖 Introduction
Welcome to part three of the "Introduction to the IndexedDB API" series. In the last post, we started the construction of our application by creating a `Database` class that contains the instance of the indexed database and we also managed to save some data by creating a `persist` method. In this part, we are going to focus on how to retrieve the data stored in the database.

### Goals 
- Create a method on the `Database` class called `getOpenCursor` that returns the `cursor` from the `objectStore` (if you don't know what a `cursor` is, or need a little refresher, refer back to part one 😉)

- Complete the `showTasks` function on the `index.js` file (present on the starting code) so that it renders out the tasks in the page

### Initial Setup 
If you want to code along (which is **_highly recommended_**), then go to the following GitHub repository:

{% github DevTony101/js-todo-app-indexed_db no-readme %}

Once there, go to the `README.md` file and search for the link labeled `Starting Code` for the second part. It will redirect you to a commit tagged as `starting-code-part-two` that contains all we have done so far plus the new `showTasks` function.

## Creating the `getOpenCursor` Function 🛠
Once we have downloaded the source code, let's go to the `Database` class and create a method called `getOpenCursor`, inside, similar to the `persist` function, we are going to get an instance of the object store and use the `openCursor()` method to send a _request_ for the cursor to open. The key difference here, in contrast to the `persist` function, is that we are going to return the request so it becomes easier to handle the `onsuccess` callback.

```javascript
export default class Database {
  constructor(name, version, fields) {
    // ...
  }

  persist(task, success) {
    // ...
  }
  
  getOpenCursor() {
    const transaction = this.indexedDB.transaction([this.name], "readonly");
    const objectStore = transaction.objectStore(this.name);
    return objectStore.openCursor();
  }
}
```

This `onsuccess` callback is special because it will be emitted for every<sup>1</sup> record on the table but only if we explicitly tell it to do so by calling the `continue()` method.
The resultant code in the `showTasks` function would look something like this:

```javascript
function showTasks() {
  // Leave the div empty
  while (tasksContainer.firstChild) tasksContainer.removeChild(tasksContainer.firstChild);
  const request = database.getOpenCursor();
  request.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor) {
      // Advance to the next record
      cursor.continue();
    } else {
      // There is no data or we have come to the end of the table
    }
  }
}
```

Remember, _if_ the cursor is not `undefined` then the data exist and is stored within the `value` property of the `cursor` object, that means we can recover the information as follows:

```javascript
function showTasks() {
  // ...
  request.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor) {
      const {title, description} = cursor.value;
      // Advance to the next record
      cursor.continue();
    } else {
      // There is no data or we have come to the end of the table
    }
  }
}
```
Great 👏! To display this information on the page, we'll be using **Bulma's** `message` component.

1. First, let's create an `article` element with the class of `message` and `is-primary`
2. Using the InnerHTML property, we are going to create two `divs`, one for the title and one for the description
3. Append the new task to the `taskContainer` div
4. Repeat

Feel free to visit **Bulma's** official documentation [here](https://bulma.io/documentation/components/message/) if you want to know a little more.

The resulting code would look something like this:

```javascript
function showTasks() {
  // ...
  const request = database.getOpenCursor();
  request.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor) {
      const {title, description} = cursor.value;
      // Step 1
      const message = document.createElement("article");
      message.classList.add("message", "is-primary");
      // Step 2
      message.innerHTML = `
        <div class="message-header">
          <p>${title}</p>
        </div>
        <div class="message-body">
          <p>${description}</p>
        </div>
      `;
       // Step 3
       tasksContainer.appendChild(message);
       // Step 4
       cursor.continue();
    } else {
      // There is no data or we have come to the end of the table
    }
  }
}
```

Good 👏! Now, what should happen if the cursor is `undefined`? We need to consider two edge cases:

1. There were at least one record saved and now the cursor has reached the end of the table

2. The table was empty

An easy way to know if the table is indeed empty is by checking if the `taskContainer` div is empty (that is, it has no children), in that case, we can simply create a `paragraph` element with the text "There are no tasks to be shown." to let the user know that there are no tasks created yet, like this:

```javascript
function showTasks() {
  // ...
  const request = database.getOpenCursor();
  request.onsuccess = event => {
    const cursor = event.target.result;
    if (cursor) {
      // ...
    } else {
      if (!tasksContainer.firstChild) {
        const text = document.createElement("p");
        text.textContent = "There are no tasks to be shown.";
        tasksContainer.appendChild(text);
      }
    }
  }
}
```

And that's it! Our `showTasks` function is complete. Now we have to figure out _where_ we should call it. 

## Using the `showTasks` Function 👨‍💻
Remember the `oncomplete` event of the `transaction` object in the `saveTasks` function? We said that if the event is emitted, we could assure the task was created, what better place to call our `showTasks` function than within this callback? That way we can update the list of created tasks on the page every time a new one is saved.

```javascript
function saveTask(event) {
  // ...
  const transaction = database.persist(task, () => form.reset());
  transaction.oncomplete = () => {
    console.log("Task added successfully!");
    showTasks();
  }
}
```

Now let's test it out! Start your local development server, go to the `index` page of the application, and create a new task:

![Index Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/anzda3coqc20mej47on8.png)

Immediately after you press on the `Create` button, you will see a new panel appears at the bottom, effectively replacing the "There are no tasks to be shown" message.

![Screenshot Panel](https://dev-to-uploads.s3.amazonaws.com/i/dpc9jpo8caa7oqv4sw4e.png)

Awesome 🎉! Everything works as expected! But... what's this? When you reload the page, the panel disappears and the text saying that there are no tasks returns once again but, we know this is not true, in fact, if we check the _Application_ tab in the **Chrome DevTools** we will see our task there:

![Application Tab Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/jn1wnw6ypkw66ll5c094.png)

So what's wrong? Well, _nothing_. The problem is that we are _only_ calling the `showTasks` function when we add a new task but we also have to call it when the page is loaded because we don't know if the user has already created some [tasks]. We _could_ just call the function inside the listener of the `DOMContentLoaded` event but is better to play it safe and call the function inside the `onsuccess` event emitted when the connection with the database is established.

We _could_ pass a callback function to the constructor but, is better if we do a little refactoring here because the constructor is not supposed to take care of that. Let's create a new function called `init()`, inside let's move out the code where we handle the `onsuccess` and the `onupgradeneeded` events. Of course, the function will receive two arguments, the fields of the table and the callback function.

```javascript
export default class Database {
  constructor(name, version) {
    this.name = name;
    this.version = version;
    this.indexedDB = {};
    this.database = window.indexedDB.open(name, version);
  }

  init(fields, successCallback) {
    this.database.onsuccess = () => {
      console.log(`Database ${this.name}: created successfully`);
      this.indexedDB = this.database.result;
      if (typeof successCallback === "function") successCallback();
    }

    this.database.onupgradeneeded = event => {
      const instance = event.target.result;
      const objectStore = instance.createObjectStore(this.name, {
        keyPath: "key",
        autoIncrement: true,
      });

      if (typeof fields === "string") fields = fields.split(",").map(s => s.trim());
      for (let field of fields) objectStore.createIndex(field, field);
    }
  }

  persist(task, success) {
    // ...
  }

  getOpenCursor() {
   // ...
  }
}
```

Now in the `index.js` file, we create the instance of the `Database` class and call the `init()` method right after, like this:

```javascript
document.addEventListener("DOMContentLoaded", () => {
  const database = new Database("DBTasks", 1);
  database.init("title, description", () => showTasks());
  // ...

  function saveTask(event) {
    // ...
  }

  function showTasks() {
    // ...
  }
});
```

And _voilá_! No matter how many times we refresh the page, if there any tasks saved in the database, the app will render them right away. 

## Let's Recap 🕵️‍♂️
In this third part, we:

- Learned how to use the `IDBCursorWithValue` interface
- Learned how to properly retrieve the information saved in the database through the `cursor` object
- Learned how to render the data on the page
- Organized the responsibilities in the `Database` class by creating a new function `init()`

Remember, the complete code for this section is available in the project's repository under the tag `finished-code-part-two`.

That's all 👏! In the next part, we will finish the application by adding the ability to effectively delete any given task from the database.

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