Open In App

Build a To-do List Web Application Powered by Blockchain

Improve
Improve
Like Article
Like
Save
Share
Report

Here, we are going to build a to-do list application that will save the data in the blockchain. The blockchain part of this application can also be understood as a database. First, we’ll create a smart contract and subsequently the web application itself. We’ll use Bloc as the application name but first, let’s look at the components.

Components in Bloc Application

  • Ganache- A local Ethereum blockchain.
  • Web3 JS- For the application to be able to communicate to the blockchain.
  • Bootstrap- For the application’s front end.
  • Solidity- For compilation smart contract.
  • JQuery- For DOM manipulation.

What is a Smart Contract?

To be able to communicate with blockchain we’ll need to write a smart contract. A smart contract can also use understood as a backend script that contacts the blockchain. The smart contract will allow us to store our to-do list tasks in the blockchain.

To write and develop the smart contract we will use the REMIX IDE

Note: Make sure you are using the Http site instead of HTTPS. The HTTP site will allow us to deploy our smart contract in our local blockchain.

Click on the plus icon to create a bloc.sol file.

Create .sol file

Create bloc.sol file

The smart contract first line must declare the SPDX-License-Identifier and solidity version to compile our smart contract to do that we’ll write the following:

// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0;  

To tell the compiler about our smart contract we’ll define a contract block.  A contract like a class in OOPs which contains all the fields and methods:

// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0; 
contract Bloc{
}  

To store the task we need to do the following:

1. Create a struct for your task: Struct allows for creating a user-defined datatype. It will have a string as a task and a boolean to tell whether the task is completed or not.

// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0; 
contract Bloc{
 struct Task{
    string task;
       bool isDone;
   } 
}  

2. Create a mapping to store our task array with an associated user address: Mapping is like a Hash Table here we are creating an address as a key and the value will be an array of task struct. Set this mapping as private for the access modifier. The address is a data type in solidity which stress the account address.

// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0; 
 contract Bloc{ 
   struct Task{
    string task;
       bool isDone;
   }
   mapping (address => Task[]) private Users;
}

Methods to manipulate our task in the contract:

1. Create Task: This method will create a task.

  • The addTask method takes in a string as an argument.
  • calldata set the data location for the string argument.
  • external makes the method available when called through web3js.
  • msg.sender gives us the address of the user calling the method.
  • The push method to add the task to the mapping.
// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0;
/// @title A contract for demonstrate how to build a to-do list application
/// @notice For now, this contract just show how to add the task 
contract Bloc{ 
   struct Task{
    string task;
       bool isDone;
   }
   
   mapping (address => Task[]) private Users;
 
  function addTask(string calldata _task) external{
      Users[msg.sender].push(Task({
          task:_task,
          isDone:false
      }));
   } 
}

2. Read Task: This method helps to read the value in the task.

  • To return Task struct from getTask method we need to add line 2.
  • The getTask method takes in the task index and gives the task.
  • memory is the data location for Task to be returned.
  • view tells that the  function doesn’t modifier state of the blockchain.
// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0;
/// @title A contract for demonstrate how to build a to-do list application
/// @notice For now, this contract just show how to get the task
contract Bloc{ 
   struct Task{
    string task;
       bool isDone;
   }
   
   mapping (address => Task[]) private Users;
 
  function addTask(string calldata _task) external{
      Users[msg.sender].push(Task({
          task:_task,
          isDone:false
      }));
   }
   
   function getTask(uint _taskIndex) external view returns (Task memory){
       Task storage task = Users[msg.sender][_taskIndex];
       return task;
   } 
}

3. Update Task: This method will update the values in the Task.

  • This method sets the task to be checked or unchecked.
  • The updateStatus method will take task index and the status to update.
  • By the taskIndex we will be able to access the task struct so we’ll set the isDone to the status passed.
// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0; 
/// @title A contract for demonstrate how to build a to-do list application
/// @notice For now, this contract just show how to update the task 
contract Bloc{ 
   struct Task{
    string task;
       bool isDone;
   }
   
   mapping (address => Task[]) private Users;
 
  function addTask(string calldata _task) external{
      Users[msg.sender].push(Task({
          task:_task,
          isDone:false
      }));
   }
   
   function getTask(uint _taskIndex) external view returns (Task memory){
       Task storage task = Users[msg.sender][_taskIndex];
       return task;
   }
 
  function updateStatus(uint256 _taskIndex,bool _status) external{
        Users[msg.sender][_taskIndex].isDone = _status;
   } 
}

4. Delete Task: deleteTask method will take the task index and then delete the element from the array just like in C.

// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.8.0 <0.9.0; 
/// @title A contract for demonstrate how to build a to-do list application
/// @notice For now, this contract just show how to delete the task 
contract Bloc{ 
   struct Task{
    string task;
       bool isDone;
   }
   
   mapping (address => Task[]) private Users;
 
  function addTask(string calldata _task) external{
      Users[msg.sender].push(Task({
          task:_task,
          isDone:false
      }));
   }
   
   function getTask(uint _taskIndex) external view returns (Task memory){
       Task storage task = Users[msg.sender][_taskIndex];
       return task;
   }
 
  function updateStatus(uint256 _taskIndex,bool _status) external{
        Users[msg.sender][_taskIndex].isDone = _status;
   }
 
    function deleteTask(uint256 _taskIndex) external{
       delete Users[msg.sender][_taskIndex];
   } 
}

5. Get Task Count: The count of tasks can be retrieved as the task array length.

And the complete solidity program after adding all the above methods to deal with a task looks like this:

Solidity




// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0; 
/// @title A contract for demonstrate how to build a to-do list application
/// @notice For now, this contract just show how to add/delete/get/update/count the task
contract Bloc{
    // Defining a structure to
    // store a task
    struct Task
    {
        string task;
        bool isDone;
    }
 
    mapping (address => Task[]) private Users;
         
    // Defining function to add a task
    function addTask(string calldata _task) external
    {
        Users[msg.sender].push(Task({
            task:_task,
            isDone:false
        }));
    }
 
    // Defining a function to get details of a task
    function getTask(uint _taskIndex) external view returns (Task memory)
    {
        Task storage task = Users[msg.sender][_taskIndex];
        return task;
    }
 
    // Defining a function to update status of a task
    function updateStatus(uint256 _taskIndex,bool _status) external
    {
        Users[msg.sender][_taskIndex].isDone = _status;
    }
 
    // Defining a function to delete a task
    function deleteTask(uint256 _taskIndex) external
    {
        delete Users[msg.sender][_taskIndex];
    }
 
    // Defining a function to get task count.
    function getTaskCount() external view returns (uint256)
    {
        return Users[msg.sender].length;
    }
}


 
 
Click on the Compile button under the Solidity left navigation bar option after that Click on Deploy and Run Transaction in their deploy and inside the deployed contract you can find all the methods.

Deploy contract

  • With this, we are done creating a basic smart contract. This smart contract we will be using in the next article to communicate with a web application. Now, we’ll create a web application that can communicate with the smart contract that we have created above.
  • Before we start building the web application we’ll need the Ethereum blockchain which will have the smart contract and the data of our application. We are going to use Ganache to go ahead download and install it.
  • After installation, open Ganache and click on quick start Ethereum after that note the address labeled under RPC Server (should be something like this http://127.0.0.1:7545).

Now building the web application

HTML




<!doctype html>
<html lang="en">
 
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
        integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <!-- App CSS-->
    <style>
        .card-container {
            width: 300px;
            height: 500px;
            background: #2699FB;
            border: 1px solid #2699FB;
            border-radius: 10px;
            opacity: 1;
            margin-right: auto;
            margin-left: auto;
            border-left: 0;
        }
 
        .icon-circle {
            background: white;
            border-radius: 200px;
            height: 70px;
            font-weight: bold;
            width: 70px;
            display: table;
            margin-top: 12px;
            margin-left: 31px;
        }
 
        .bloc {
            margin-top: 12px;
            margin-left: 28px;
        }
 
        .task-count {
            margin-top: 7px;
            margin-left: 31px;
        }
 
        .task-card {
            width: 300px;
            height: 325px;
            border: 1px solid #FFFFFF;
            background: #FFFFFF;
            border-radius: 25px 25px 10px 10px;
            opacity: 1;
            position: relative;
        }
 
        .fab {
            background: #2699FB;
            border-radius: 200px;
            height: 40px;
            width: 40px;
            font-weight: bold;
            border: 0;
            position: absolute;
            right: 0;
            bottom: 0;
            margin-right: 27px;
            margin-bottom: 31px;
        }
 
        .add-task-container {
            top: 150px;
            width: 300px;
            height: 187px;
            background: #FFFFFF;
            border-radius: 25px 25px 0px 0px;
            opacity: 1;
            position: absolute;
        }
 
        .task-done {
            color: gray;
            text-decoration: line-through;
        }
    </style>
    <title>Bloc</title>
</head>
 
<body style="background: #BCE0FD">
    <div class="card-container my-5 border-left-0">
        <div class="icon-circle">
            <img class="px-2 py-2" src="https://github.com/gupta-shrinath/Bloc/raw/gupta-shrinath/images/add-list.png"
                alt="icon">
        </div>
        <h2 class="bloc text-white"><strong>BLOC</strong></h1>
            <p id="taskCount" class="task-count text-white">0 Task</p>
 
 
 
 
            <div class="task-card">
                <!-- Floating Action Button -->
                <button id="fab" class="fab float-right" data-toggle="modal" data-target="#add-task-container">
                    <img class="m-auto" src="https://github.com/gupta-shrinath/Bloc/raw/gupta-shrinath/images/plus.png"
                        alt="" height="16" width="16">
                </button>
                <!-- #Floating Action Button -->
 
                <!-- Task List-->
                <ul id="list" class="list-group mt-3">
                </ul>
                <!-- #Task List-->
 
                <!-- Add Task Modal -->
                <div id="add-task-container" class="modal add-task-container" data-backdrop="static">
                    <div class="container">
                        <div class="col mx-2">
                            <h5 class="text-primary text-center mt-4">New Task</h5>
                            <input id="new-task" class="mt-3" type="text">
                            <button type="button" data-dismiss="modal" class="btn btn-primary btn-block mt-3"
                                onclick="addTask(document.getElementById('new-task').value);">Add
                                Task</button>
                        </div>
                    </div>
                </div>
                <!-- #Add Task Modal -->
 
            </div>
    </div>
 
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
        crossorigin="anonymous"></script>
        integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
        crossorigin="anonymous"></script>
        integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV"
        crossorigin="anonymous"></script>
    <!-- App and related files JS-->
    <script src="js/config.js"></script>
    <script src="js/getAccount.js"></script>
    <script src="js/app.js"></script>
</body>
 
</html>


 
 The webpage will look like this-

 

bloc

getAccount.js

Javascript




// Connect to Ganache Make sure you enter the address you noted earlier here //
web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
// getAccount() will get the first account from  ganache and will set it as defaultAccount for our contract operations ////
async function getAccount() {
    let accounts = await web3.eth.getAccounts();
    web3.eth.defaultAccount = accounts[0];
    console.log(web3.eth.defaultAccount + ' account detected');
    return web3.eth.defaultAccount;
}


config.js

  • Go to Remix IDE and make sure you have the bloc.sol from the previous tutorial (Make sure to sure HTTP site, not https).
  • Go to the solidity compiler located in the left panel and click on compile bloc.sol. Down you’ll find a button with copy icon and text as ABI click on it. Paste it in js/config.js line 1.
let contractABI = COPIED TEXT;
The copied text will be enclosed in [].
  • Go to deploy and run transactions under environment select Web3 Provider.
  • Enter the address you copied from Ganache and paste it click on OK.
  • Now a deploy button will be visible click on it.
  • Down you’ll find Deployed Contracts label now there will a copy icon button click on it.
  • And paste in js/config.js line 2.
let contractAddress = 'COPIED text';
The copied text might look like this 0xF3017acEDd45526aC6153FBBCfcA8096173D245a.
The contractABI helps the web3js with our smart contract
The contractAddress tells the web3js about where on blockchain is our smart contract

app.js

Javascript




$(document).ready(createTaskList());
 
// Auto focus on input of add task modal //
$('#add-task-container').on('shown.bs.modal', function () {
    $('#new-task').trigger('focus');
});
 
/**
 * createTaskList() set the contract object and gets the number
 * of tasks of the user and then calls addTaskToList() to add
 * them to HTML one after the other after all task are added to
 * HTML then calls updateTaskCount()
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
*/
async function createTaskList() {
    // Get account from the Ganache EVM //
    try {
        await getAccount();
        // Set contract and set gas //
        contract = new web3.eth.Contract(contractABI, contractAddress);
        try {
            numberOfTask = await contract.methods.getTaskCount().call({ from: web3.eth.defaultAccount });
            /*  The actual number of task may differ because
                when an task is removed the task element is
                removed and the index value now has nothing.
            */
            console.log('Number of Tasks are ' + numberOfTask);
            // If there are task present //
            if (numberOfTask != 0) {
                // Fetch one task after the other until no task remain //
                console.log('Start fetching task ...');
                let taskIterator = 0;
                while (taskIterator < numberOfTask) {
                    try {
                        let task = await contract.methods.getTask(taskIterator).call({ from: web3.eth.defaultAccount });
                        if (task[0] != '') {
                            // addTaskToList add this task as children to the ul tag //
                            addTaskToList(taskIterator, task[0], task[1]);
                        }
                        else {
                            console.log('The index ' + taskIterator + ' is empty');
                        }
                    } catch {
                        console.log('Failed to get Task ' + taskIterator);
                    }
                    taskIterator++;
                }
                // Update the task count in HTML //
                updateTasksCount();
            }
        } catch {
            console.log('Failed to get task count from blockchain');
        }
    } catch {
        console.log('Failed to get the account');
    }
 
}
 
/**
 * addTaskToList() takes the task attributes and adds them to
 * the HTML
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
 * @param {number} id
 * @param {string} name
 * @param {boolean} status
 */
function addTaskToList(id, name, status) {
    console.log('addTaskToList(): Add Task ' + (id) + ' ' + [name, status]);
    /*  Get the id of ul element so to be able to
        add children to it
    */
    let list = document.getElementById('list');
    /*  Create a li element and add the class
        required to make look good  and
        set the id of it
    */
    let item = document.createElement('li');
    item.classList.add('list-group-item', 'border-0', 'd-flex', 'justify-content-between', 'align-items-center');
    item.id = 'item-' + id;
    // Create a text to add it to the li element//
    let task = document.createTextNode(name);
    /*  Create a checkbox and set its id and checked
        value to add it to the li element
    */
    var checkbox = document.createElement("INPUT");
    checkbox.setAttribute("type", "checkbox");
    checkbox.setAttribute("id", "item-" + id + "-checkbox");
    checkbox.checked = status;
    /*  if status is true then add task-done class to li
        element so that the text gets a linethrough
    */
    if (status) {
        item.classList.add("task-done");
    }
    // Add the li element to ul element //
    list.appendChild(item);
    /* Set a ondblclick event to able to remove the
       item when double clicked on it */
    item.ondblclick = function () {
        removeTask(item.id);
    }
    // Append the text of task //
    item.appendChild(task);
    // Append the checkbox for task //
    item.appendChild(checkbox);
    // Add onclick to the checkbox //
    checkbox.onclick = function () { changeTaskStatus(checkbox.id, id); };
}
 
/**
 * removeTask() remove the task from blockchain and then from
 * the HTML using JQuery
 * Note: The taskIndex is the li element id {item-taskIndex}
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
 * @param {string} taskIndex
 */
async function removeTask(taskIndex) {
    console.log("removeTask(): Remove Task " + taskIndex);
    // Create the selector for the Task //
    let taskSelector = '#' + taskIndex;
    // Make taskIndex to have only task index number
    taskIndex = taskIndex.replace('item-', '');
    try {
        await contract.methods.deleteTask(taskIndex).send({ from: web3.eth.defaultAccount });
        console.log('Remove Task ' + taskIndex + ' from the blockchain');
        // Remove the task from the HTML //
        $(taskSelector).remove();
        // Update the task count in HTML//
        updateTasksCount();
    } catch {
        console.log('Issue occurred while removing task item-' + taskIndex);
    }
}
 
/**
 * changeTaskStatus() change the status of task in blockchain and
 * then in the HTML
 * Note: The id is the checkbox id {item-taskIndex-checkbox}
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
 * @param {string} id
 * @param {number} taskIndex
 */
async function changeTaskStatus(id, taskIndex) {
    // Get checkbox element //
    let checkbox = document.getElementById(id);
    // Get the id of the li element //
    let textId = id.replace('-checkbox', '');
    // Get the li element //
    let text = document.getElementById(textId);
    try {
        await contract.methods.updateStatus(taskIndex, checkbox.checked).send({ from: web3.eth.defaultAccount });
        console.log('changeTaskStatus(): Change status of task ' + textId + ' to ' + checkbox.checked);
        if (checkbox.checked) {
            text.classList.add("task-done");
        } else {
            text.classList.remove("task-done");
        }
    } catch (error) {
        console.log('Failed to change Status of task ' + textId + ' in blockchain');
    }
}
 
/**
 * updateTaskCount() update the number of task in HTML by counting
 * the number of item in the ul element
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
 */
function updateTasksCount() {
    // Get the element of ul tag //
    let list = document.getElementById('list');
    // Get the count of the ul element //
    let taskCount = list.childElementCount;
    console.log('updateTaskCount(): The number of task are ' + taskCount);
    // Set the count to the taskCount id element //
    let count = document.getElementById('taskCount');
    count.innerText = taskCount + " Task";
}
 
/**
 * addTask() add the task to the HTML via adddTasktoList() and then
 * add it to blockchain and update the count via updateTaskCount()
 * @author Gupta Shrinath <https://github.com/gupta-shrinath>
 * @param {string} name
 */
async function addTask(name) {
    // Get the form element containing the new task //
    let form = document.getElementById('add-task-form');
    // Check if the input is valid and then add it//
    if (form.checkValidity()) {
        console.log('Get the number of task from blockchain');
        // Set blank value for text in the addtask modal //
        document.getElementById('new-task').value = '';
        // Remove the mentioned class because it might be
        // present if a task was added before
        form.classList.remove('was-validated');
        // Get the number of task from blockchain //
        contract.methods.getTaskCount().call({ from: web3.eth.defaultAccount }).then(numberOfTask => {
            // Add the task to the HTML //
            addTaskToList(numberOfTask, name, false);
            // Update the task count in HTML//
            updateTasksCount();
        }, err => {
            console.log('Failed to get the number of task in blockchain ' + err);
        });
        try {
            await contract.methods.addTask(name).send({ from: web3.eth.defaultAccount });
            console.log('Add task ' + name + ' to blockchain');
        } catch {
            console.log('Failed to add task to EVM');
        }
 
    } else {
        form.addEventListener('submit', function (event) {
            // Stop all events //
            event.preventDefault();
            event.stopPropagation();
            // Add the mentioned class to able to display
            // error to user
            form.classList.add('was-validated');
            // Set blank value for text in the addtask modal //
            document.getElementById('new-task').value = '';
        }, false);
 
    }
 
}


Now the application is fully working.



Last Updated : 24 Apr, 2023
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads