This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 9. Tutorials already published:
- Intro
- Step 1: Writing The ToDo Smart contracts- beginning
- Step 2: Writing The ToDo Smart contracts – end
- Step 3: Writing The Nodejs Backend
- Step 4: Setup the frontend and read account data
- Step 5: Build a smart contract client with Truffle Contract
- Step 6: Refactor With Webpack, ES6 and truffle-solidity-loader
- Step 7: Read smart contract data from frontend
- Step 8: Create smart contract data from frontend
- Step 9: Toggle task done status (This tutorial)
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:
- Send a
POST
request to API - Read returned data from API, including updated fields of modified object
- 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:
- We can’t return data coming from a function
return
statement to an external caller, as weird as it seems. - 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!
DappSolidity
First of all, I would like to thank you for this great tutorial. And as a way to express my thanks, I have purchased the ebook. Second I have two questions:
1- How we can enable the Dapp to interact with MetaMask addon so that the user can select the account where he wants to send a transaction to the smart contract?
2- I tried to use Ganache GUI instead of Ganache cli but I got this error
Uncaught (in promise) Error: Cannot create instance of ToDo; no code at address 0x010f8E6685c5e2F11834fF4a89C3a30022606d16
at Object.callback (bundle.js:63)
at bundle.js:8
at bundle.js:86
at XMLHttpRequest.r.onreadystatechange (bundle.js:63)
how we can resolve this error