JavaScript: Building a To-Do App (Part 3)
Miguel Manjarres
Posted on July 24, 2020
๐ 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 calledgetOpenCursor
that returns thecursor
from theobjectStore
(if you don't know what acursor
is, or need a little refresher, refer back to part one ๐)Complete the
showTasks
function on theindex.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:
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.
- First, let's create an
article
element with the class ofmessage
andis-primary
- Using the InnerHTML property, we are going to create two
divs
, one for the title and one for the description - Append the new task to the
taskContainer
div - 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:
There were at least one record saved and now the cursor has reached the end of the table
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:
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.
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:
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 functioninit()
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 ๐.
Posted on July 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.