ToDo List Ethereum Dapp (Step7) | Read Contract data from Frontend

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

In the last tutorial (step6) of this series we improved a lot our workflow thanks to webpack and truffle-solidity-loader. Now, when you make a change to your smart contract, you just have to reload manually the frontend in your browser, but no need to handle any of the smart contract workflow stuffs (compilation, migration…). Beautiful!

But we still have some work to do!

In this tutorial we are going to read the tasks created in the smart contract, using Truffle-Contract and web3.

When you finish this tutorial you will know how to read the data of a smart contract to the frontend UI.

Setup project

Copy and paste the folder of the last episode (step6) and rename it to step7:

cp -r special-episode-1/step6 special-episode-1/step7

Step into step7 folder and if you are just picking up the tutorial series here you need to install npm dependencies:

cd special-episode-1/step7
npm install

In a new terminal, launch a local Ethereum blockchain:

ganache-cli

In a new terminal, start the nodejs server:

npm run dev-back

In a new terminal, start the webpack watcher for the frontend:

npm run dev-front

Just to recap, you should have created a total of 3 terminal windows:

  • Terminal 1: for ganache-cli, the local Ethereum Blockchain
  • Terminal 2: for npm run dev-back, the nodejs server
  • Terminal 3: for npm run dev-front, the webpack watcher

Awesome! You are ready to get started

Create the HTML

We are going to show a list of all tasks that are stored in the smart contract. For that, let’s have a look at the content of the Task struct inside the contracts/ToDo.sol smart contract:

 struct Task {
   uint id;
   uint date;
   string content;
   string author;
   bool done;
 }

We want to show all the fields of each task. Let’s create an html table with these 5 fields. Copy the following snippet and paste it inside <div class="container"></div> tag in app/index.html:

<!-- Tasks List -->
<div class="card">
  <div class="row">
    <div class="col-sm-12">
      <h2 class="orange">Tasks</h2>
    </div><!--./col-sm-12-->
  </div><!--./row-->
  <div class="row">
    <div class="col-sm-12">
      <table class="table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Date</th>
            <th>Content</th>
            <th>Author</th>
            <th>Done</th>
          </tr>
        </thead>
        <tbody id="tasks">
          <!-- tasks here -->
        </tbody>
      </table>
    </div><!--./col-sm-12-->
  </div><!--./row-->
</div><!--./card-->

Load the frontend in your browser at http://localhost:3000. You should see this:

It’s shaping up! But it’s still empty… Let’s learn how to fill this up!

Create dummy data in the smart contract

Before we even start to read smart contract data from the frontend we need this data to actually exist on the smart contract. We will learn how to create data in the smart contract from the frontend in the next tutorial of this series. But for now we will temporarily use a new function that will return some hardcoded data. Open contracts/ToDo.sol and create this function:

function getTaskFixtures(uint _id) public constant returns(
    uint,
    uint,
    string,
    string,
    bool
   ) {
  return (0, now, "Create more tutorials for ETB", "Julien", false); 
}

Since we are editing our smart contract, let’s also update the code to be up-to-date with the latest Solidity features.

Change the signature of the constructor from function ToDo() public to this: constructor() public.

We also need to update our events by adding field names. Let’s first change the definition of the TaskCreated event. From this:

event TaskCreated(uint, uint, string, string, bool);

To this:

event TaskCreated(
  uint lastTaskId, 
  uint date, 
  string content, 
  string author, 
  bool done
);

Finally add the emit keyword when emitting the TaskCreated event inside createTask() function:

emit TaskCreated(lastTaskId, now, _content, _author, false); 

Now we can just call getTaskFixtures() to get some dummy data.

But how can we do this from the frontend?

Read smart contract data from the frontend

Open app/js/index.js and replace this:

abstraction.at(address)
  .then((todo) => {
    todo.getTaskIds()
    .then((taskIds) => {
      console.log(taskIds);
    });
  });

By this:

abstraction.at(address)
  .then((todo) => {
    return todo.getTaskFixtures(0);
  })
  .then(console.log);

Let’s also do some cleanup and remove web3.eth.getAccounts(console.log) just below.

In this new code we call getTaskFixtures() to get the dummy data from the smart contract and we print it to the console. You should see this in your browser console:

(5) [H, H, "Create more tutorials for ETB", "Julien", false]

You might see an error in the browser console if you followed an old version of this tutorial. To fix it, in app/js/index.js, replace this:

...
const networks = Object.keys(artifact.networks)[0];
...

By this:

...
const networks = Object.keys(artifact.networks);
const network = networks[networks.length - 1];
...

Woohoo! We managed to read smart contract data from the frontend!

In the next tutorial we will replace getTaskFixtures() by getTask() to retrieve real data.

Ok, we have fetched the smart contract data on the frontend, but how do we display is in the UI?

Setup jQuery

We will use jQuery to display the data and manipulate the DOM.

We previously installed jQuery “the old way” by loading it in a <script> tag in app/index.html. But in the meantime we refactored our code with ES6 and webpack. So let’s take advantage of this and load jQuery with ES6 import statement.

Delete this <script> tag in app/index.html:

 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>

Install jQuery with npm:

npm install -D jquery

At the top of app/js/index.js, add:

import $ from 'jquery';

We need to make sure the DOM is ready when we start exiting our code so thatWrap your code below the import statements with this:

$(() => {
});

Your app/js/index.js` should look like this:

import $ from 'jquery';
import Web3 from 'web3';
import TruffleContract from 'truffle-contract';
import artifact from '../../contracts/ToDo.sol';
import config from './config';

$(() => {
  const web3 = new Web3(new Web3.providers.HttpProvider(config.ethereumUrl));

  const abstraction = new TruffleContract(artifact);
  abstraction.setProvider(web3.currentProvider);

  const networks = Object.keys(artifact.networks);
  const network = networks[networks.length - 1];
  const address = artifact.networks[network].address;

  abstraction.at(address)
    .then((todo) => {
      return todo.getTaskFixtures(0);
    })
    .then(console.log);
});

Render the smart contract data with jquery

We will create a renderTasks() function which will:

  • Create an html fragment containing the tasks returned from the smart contract
  • Use jQuery to insert this html fragment into the DOM

The app/index.js file starts to have too many different responsibilities. We will create a app/js/lib folder and put our new code inside:

mkdir app/lib
touch app/lib/render.js

Inside we will define a function to render the list of tasks:

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>${task[1]}</td>
      <td>${task[2]}</td>
      <td>${task[3]}</td>
      <td>${task[4]}</td>
     </tr>`);
   });
  $tasks.html(html.join(''));
};

export {
  renderTasks
}

This functions takes as arguments a jquery-wrapped DOM node ($tasks) where we will insert tasks, and a tasks array containing data returned from the smart contract. We then use ES6-style string templates to build each <tr> row. Note that we can’t reference field by names (id, date, content, etc…). Instead we need to know what is the index of each field in the returned data. Web3 and Solidity are unable to return a struct to the frontend at the moment. Some contributors are working on this issue and soon we will be able to use a struct and have better-looking code.

In app/js/index let’s use this:

...
import { renderTasks } from './lib/render.js';


abstraction.at(address)
  .then((todo) => {
     return todo.getTaskFixtures(0);
   })
   .then((task) => {
     renderTasks($('#tasks'), [task]);
   });

Now, reload the frontend. You should see this:

We are almost there. But the date field is not formatted properly. Web3 uses BigNumber instances to represent numbers. When a BigNumber is displayed, that’s not human-friendly. Let’s fix this. Create a file called app/js/lib/utils.js:

touch app/js/lib/utils.js

And add this formatting function inside:

const formatDate = (rawDate) => { 
  const _date = new Date(rawDate.toNumber()); 
  const date = `${_date.getDate() + 1}/${_date.getMonth() + 1}/${_date.getFullYear()}`; 
  const time = `${_date.getHours()}h ${_date.getMinutes()}m`; return `${date} - ${time}`; 
};

export { formatDate }

Let’s use this in app/js/render.js:

import { formatDate } from 'utils'; 
...
... 
<td>${formatDate(task[1])}</td>
...

Now the formatting of the date should be fixed:

Give yourself a good pat in the back. You made it!

However, the app/js/index.js file still has too many responsibilities and start to be too confusing. We need to refactor that.

Refactor with an App object

We are going to extract some of the code of app/js/index.js into a new file called app/js/lib/app.js. Start by creating this file:

touch app/js/lib/app.js

In our new file copy paste the below code:

import $ from 'jquery'; 
import Web3 from 'web3'; 
import TruffleContract from 'truffle-contract'; 
import artifact from '../../../contracts/ToDo.sol'; 
import { renderTasks } from './render';

class App { 
  constructor(config) { 
    this.config = config; 
  }

  setup() { 
    const { ethereumUrl } = this.config; 
    const web3 = new Web3(new Web3.providers.HttpProvider(ethereumUrl));

    const Todo = new TruffleContract(artifact);
    Todo.setProvider(web3.currentProvider);

    const networks = Object.keys(artifact.networks);
    const network = networks[networks.length - 1];
    const address = artifact.networks[network].address;

    this.web3 = web3;
    this.address = address;
    this.Todo = Todo;
    this.$tasks = $('#tasks');

    return new Promise((resolve, reject) => {
      Todo.at(address)
      .then((todo) => {
         this.todo = todo;
         resolve(todo);
      })
      .catch((error) => {
        reject(error);
      });
    });   
  }

  init() { 
    return new Promise((resolve, reject) => { 
      this.todo.getTaskFixtures(0)
      .then((task) => { 
        renderTasks(this.$tasks, [task]);  
      }); 
    }); 
  } 
}

export default App;

What is going on in this file?

  • First, The setup() function creates an instance of Web3 and of TruffleContract (todo) so that we can interact with our smart contract. It also creates a jQuery-wrapped DOM node ($tasks) for later use when inserting smart contract data into the UI.
  • Then, the init() function fetches a task from the smart contract and renders it in the html.

Now that we have this new App object, let’s make use of it in app/js/index.js. Update this file to this:

import $ from 'jquery'; 
import config from './config'; 
import App from './lib/app';

$(() => { 
  const app = new App(config); 
  app.setup()
  .then(() => { 
    return app.init(); 
  }) 
  .then(() => { 
    console.log('ETB ToDo List Dapp loaded!'); 
  }) 
  .catch((error) => {
    console.error(`Ooops... something went wrong: ${error}`);
  });
});

The end

Great, you can now read the smart contract!

However:

  • We want to read the normal data of the smart contract, not some fixtures we created just for development
  • We want to be able to create a new task from the frontend

The solution to these problems will be given in the next tutorial of this series! Stay tuned.

Leave a Reply

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