ToDo List Ethereum Dapp (Step9) | Toggle Task Done & Keep Frontend Updated

This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 9. Tutorials already published:

In the last tutorial (step8) of this series we added a feature to create new tasks from the frontend.

In this step we will:

  • add a feature to toggle a task as done
  • Reload the frontend after we toggle the task done status

Let’s go!

Add new field to Task struct for toggling done status

Open contracts/ToDo.sol. We are going to add a new field dateComplete to each task to store when the task has marked as done. Add this field to the Task struct:

struct Task {
    uint id;
    uint date;
    string content;
    string author;
    bool done;
    uint dateComplete; //new field
  }

Next, below the TaskCreated event let’s add another event TaskStatusToggled to signal when a done status is toggled:

event TaskStatusToggled(uint id, bool done, uint date);

Now, it’s time to create a function to toggle the done status. Below getTask(), create the toggleDone() function:

function toggleDone(uint id) taskExists(id) public {
  Task task = tasks[id];
  task.done = !task.done;
  task.dateComplete = task.done ? now : 0;
  TaskStatusToggled(id, task.done, task.dateComplete);
}

What’s going on here? First, this function accepts as a parameter the id of the task to toggle:

function toggleDone(uint id) 

Then, we apply the modifier taskExists to make sure we don’t continue the execution of the function. If you don’t know what is a modifier, go back to the step 2 of this series where I explain this.

Then, we create a pointer to the task to toggle:

Task task = tasks[id];

Then, we actually toggle the done status:

task.done = !task.done;

Then we update the dateComplete field:

 task.dateComplete = task.done ? now : 0;

Finally, we emit a TaskStatusToggled event:

emit TaskStatusToggled(id, task.done, task.dateComplete);

In the getTask function we also need to return the new dateComplete field. Go to this function definition, and add dateComplete to the list of returned parameters:

function getTask(uint id) taskExists(id) public constant
    returns(
      uint,
      uint,
      string,
      string,
      bool,
      uint //new field added here
    ) {
    return(
        id,
        tasks[id].date,
        tasks[id].content,
        tasks[id].author,
        tasks[id].done,
        tasks[id].dateComplete //new field added here
    );
}

Alright, our smart contract is ready!

Let’s now turn our attention to the frontend.

Add HTML Checkbox to toggle done status

To mark a task as done, we are going to use a checkbox. Ticking the checkbox marks the task as done, and unticking it marks as undone. Easy 🙂

Open app/js/lib/render.js, and in the function ‘renderTasks()’, replace the line where we simply render the done status:

<td>${task[4]}</td>

By this:

<td><input type="checkbox" id="checkbox-${task[0]}" ${task[4] ? 'checked' : ''}/></td>

If the task is done, we display the checkbox, otherwise we don’t. We also store the id of each task (task[0]) into the id attribute of the checkbox so that we can identify the task that was toggled.

Below, add another column to the row to show the dateComplete field:

<td>${task[5] ? formatDate(task[5]) : ''}</td>

In the end, the updated renderTasks() function should look like this:

const renderTasks = ($tasks, tasks = []) => {
  if(tasks.length == 0) {
    $tasks.html('<tr><td scope="row">No task created yet...</td></tr>');
    return;
  }

  const html = tasks.map((task) => {
    return(`<tr>
      <td>${task[0]}</td>
      <td>${formatDate(task[1])}</td>
      <td>${task[2]}</td>
      <td>${task[3]}</td>
      <td><input type="checkbox" id="checkbox-${task[0]}" ${task[4] ? 'checked' : ''}/></td>
      <td>${task[5] ? formatDate(task[5]) : ''}</td>
     </tr>`);
   });
  $tasks.html(html.join(''));
};

Last thing, add a header field to the table for the dateComplete field. In app/index.html:

<table class="table">
  <thead>
    <tr>
      <th>ID</th>
      <th>Date</th>
      <th>Content</th>
      <th>Author</th>
      <th>Done</th>
      <th>Date Complete</th> <!-- We added this -->
    </tr>
  </thead>
  ...

We have our HTML setup. Very good!

But the HTML does not respond to anything yet. We need to add some Javascript!

Add the event listener (Javascript)

We will now add the Javascript code that will:

  • listen to click events on the done checkbox
  • trigger the smart contract toggleDone() function

Open app/js/lib/app.js, in init(), before return new Promise((resolve, reject) => {, add this:

this.$tasks.on('click', (event) => {
  if($(event.target).is('input')) {
    const [,id] = event.target.id.split('-');
    this.todo.toggleDone(id, { from: this.account, gas: 1000000 })
    .then(() => {
     //Refresh tasks
    });
   }
});

Let’s see what happen in this snippet.

First, we attach an event listener for clicks to the root of the tasks table. Thanks to event bubbling, we only need a single event listener for all the checkboxes. That’s much cleaner and simple that having a separate event listener for EACH checkbox:

this.$tasks.on('click', (event) => {
  if($(event.target).is('input')) {
  }
});

Then, we grab the id attribute of the checkbox HTML element that was clicked, so that we can get the id of the task that was toggled.

const [,id] = event.target.id.split('-');

Finally we actually trigger the smart contract toggledDone() function, passing it the task id we just got before;

this.todo.toggleDone(id, { from: this.account, gas: 1000000 })
.then(() => {
  //Refresh tasks
}

Go to your browser and try to toggle some tasks. It works!

However, there is still a small issue. After we toggle the done status, the UI does not update to reflect the new data of the task.

Reload frontend after toggling done status

Once we toggle the done status on the smart contract, we need to reload the data of the frontend.

In the event listener we created just before, we need to replace the //Refresh tasks comment by code that actually reload the frontend:

this.$tasks.on('click', (event) => {
    if($(event.target).is('input')) {
        const [,id] = event.target.id.split('-');
        this.todo.toggleDone(id, { from: this.account, gas: 1000000 })
        .then(() => {
            //Refresh tasks
        }
    }
});

We will use the same code as we already have in the promise returned by init():

 getTasks(this.todo)
 .then((tasks) => { 
   renderTasks(this.$tasks, tasks);  
 });

We could just copy paste it, but that would not be very DRY (Don’t Repeat Yourself).

Instead, let’s create a function to re-use this code. Above init(), create a new function :

getAndRenderTasks() {
  getTasks(this.todo)
  .then((tasks) => renderTasks(this.$tasks, tasks));
}

Let’s make use of this new function. Replace:

return new Promise((resolve, reject) => { 
  getTasks(this.todo)
  .then((tasks) => { 
    renderTasks(this.$tasks, tasks);  
   }); 
}); 

By :

this.getAndRenderTasks();

Finally, just above when we execute the toggleDone() function, replace the //Refresh tasks comment by a call to getAndRenderTasks():

this.$tasks.on('click', (event) => {
  if($(event.target).is('input')) {
    const [,id] = event.target.id.split('-');
    this.todo.toggleDone(id, { from: this.account, gas: 1000000 })
    .then(() => this.getAndRenderTasks());
  }
});

Now, go to your browser, and try again to toggle the done status of a task. You should see the UI reloading with the latest data of the smart contract (i.e dateComplete is up-to-date now). Fantastic!

We have one last improvement to make. After we create a new task, the UI is not refreshed, and that’s not good!

Reload frontend after creating a task

Like we reload the frontend after we toggle the done status, we also have to reload the frontend after we create a new task.

In the init() function of app.js, replace:

this.todo.createTask(
  this.$taskContent.val(), 
  this.$taskAuthor.val(), 
  {from: this.account, gas: 1000000}
).then(() => {
  console.log('Task created!');
})
.catch((error) => {
  console.log(`Oops... There was an error: ${error}`);
});

By:

this.todo.createTask(
  this.$taskContent.val(),
  this.$taskAuthor.val(),
  { from: this.account, gas: 1000000 }
).then(() => {
  this.$taskContent.val('');
  this.$taskAuthor.val('');
  this.getAndRenderTasks();
});

Once the task is created on the smart contract, we clear the fields of the new task form, and trigger getAndRenderTasks().

Now, test that it works by creating a new task in your browser. You should see the new task appearing automatically in the list of tasks after you create it. Sweet!

Let’s get a high-level overview of the pattern we have been following.

The TRR pattern (Transact-Read-Render)

We can generalize what we have done for new tasks and done status to a pattern called the TRR (Transact-Read-Render):

1. Transact: Sending a transaction
2. Read: Re-reading smart contract data
3. Render: Reloading the UI with fresh data

You need to use the pattern whenever your send a transaction (i.e modify data) to your smart contract, and want to keep the UI up-do-date.

When we use traditional web APIs, something similar seems to happen:

  1. Send a POST request to API
  2. Read returned data from API, including updated fields of modified object
  3. Render the frontend with new data

Is it the same as TRR? No, because 1, 2 and 3 require just one HTTP call in this case. A traditional database modifies data right away, and the API can directly returned the updated data in the same HTTP request.

In the context of Ethereum transactions and Solidity smart contracts, we have 2 problems that prevent us from doing the same thing:

  1. We can’t return data coming from a function return statement to an external caller, as weird as it seems.
  2. Data is not modified right away in the Blockchain. Instead, a miner needs to pickup the transaction, include it a block and mine the block. This usually takes 15s (i.e the block time on Ethereum).

For 1. , it forces us to do a second HTTP request to read the updated data. For 2. , it forces us to wait a bit before reading the updated data of the smart contract, and possibly try to read the updated data several times, until the transaction is mined. For high-value transactions where several confirmations are required, the logic can even be more complex.

In our case, we use ganache-cli as our Ethereum Blockchain, where we have instant mining, so we didn’t need to worry about 2. But for a deployment to mainnet where we don’t have instant mining, we might have to take this into consideration depending on the value of the transaction.

The end

In this tutorial, we added the following features:

  • We can toggle the done status of a task
  • We can see a dateComplete field in each task, showing when the task was marked as done
  • The UI is refreshed after we toggle the done status of a task

To add these features, we had to:

  • update the toggleDone() function of the smart contract
  • add a checkbox on the frontend to toggle the done status
  • add an event listener to respond to checkbox changes
  • Re-fetch data from smart contract and re-render it after a transaction is sent to the smart contract

If you had just one thing to remember from this tutorial it should be how to keep the UI updated after you modify some data in a smart contract.

Congratulations for following this series! You have demonstrated that you are persistent and motivated, these qualities will help you a lot in your journey to become a Blockchain developer.

Next steps

Where to go from here? In this tutorial we have been using jQuery. jQuery is enough for very simple projects, but as soon as we start to have more complex interactions on the frontend it can becomes messy. That’s when you need to use React.

The ebook of the series (Learn Ethereum Dapps) contains all of the free steps of this series plus 2 exclusive bonus steps:

  • Refactor our Dapp with React
  • Refactor our Dapp with Drizzle. Drizzle is a cutting-edge Javascript frontend framework for smart contracts, created by the good folks of Truffle.

The series on the ToDo List Dapp will not stop here. I will also publish 2 other bonus steps on EatTheBlocks Pro:

  • Test our smart contract with Truffle
  • Connect to Metamask, the in-browser Ethereum Wallet

EatTheBlocks Pro is the upcoming paid screencast of EatTheBlocks. It will be a weekly screencast for Ethereum Dapps developers, available with a starting price of 10 USD a month. Every week I will publish 10-20 mins high-quality videos exclusively available to members of EatTheBlocks Pro. It will cover intermediate to advanced topics on Ethereum Dapps and smart contracts. At the time of writing this article I haven’t made an official announcement yet, so if you read this you are one of the first one to learn about this! I will release EatTheBlocks Pro in a couple of weeks, so stay tuned!

Leave a Reply

Your email address will not be published. Required fields are marked *