DEV Community

Cover image for JavaScript: Building a To-Do App (Part 3)
Miguel Manjarres
Miguel Manjarres

Posted on • Updated on

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

πŸ“– 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 logo DevTony101 / js-todo-app-indexed_db

This is a to-do web application that uses the IndexedDB API.

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.

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 every1 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:

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:

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 if you want to know a little more.

The resulting code would look something like this:

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:

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.

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

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

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

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.

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:

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 πŸ‘‹.

Top comments (0)