This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 7. 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 (This tutorial)
- Step 8: Create smart contract data from frontend
- Step 9: Toggle task done & keep frontend updated
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
, thewebpack
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 ofWeb3
and ofTruffleContract
(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.
DappjQueryTruffle
Leave a Reply