This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 8. 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 (This tutorial)
- Step 9: Toggle task done & keep frontend updated
In the last tutorial (step7) of this series we read tasks from the frontend using jQuery
, Web3
and TruffleContract
. We also refactored our code by creating an App
object and separating configuration and bootstrapping code. Now our code looks better!
However, we are still only reading dummy data from the smart contract, instead of real data.
How can we create tasks from the frontend and read them? Read this tutorial until the end and you will know how to do that! We will continue to use jQuery
, Web3
and TruffleContract
.
Setup project
(If you have been following this tutorial series for a while this section can get a bit repetitive… feel free to skip it if you remember how to get started for each step).
Copy and paste the folder of the last episode (step7) and rename it to step8
:
cp -r special-episode-1/step7 special-episode-1/step8
Step into step8
folder and install npm dependencies:
cd special-episode-1/step8
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.
Setup the HTML
Let’s create a form to create new tasks. Open app/index.html
inside <div class="container"></div>
. At the very top, add this HTML:
<div class="container">
<!-- Create Task -->
<div class="card">
<!-- Create Tasks button -->
<div class="row">
<div class="col-sm-12">
<h2 class="orange">Create Task</h2>
</div>
</div>
<div class="row">
<form id="new-task" class="col-sm-12">
<div class="form-group">
<label for="task-content">Content</label>
<input id="task-content" type="text" class="form-control"></input>
</div>
<div class="form-group">
<label for="task-author">Author</label>
<input id="task-author" type="text" class="form-control"></input>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div><!--./row-->
</div><!--./card-->
...
</div><!--./container-->
You should see this in your browser:
We can click on the Submit
button, but it doesn’t do much at the moment. How can we make the form actually create a task in our smart contract?
Attach an event handler to the Submit button with jQuery
The first step to making our Submit button operational is to be able to respond to user clicks and trigger a specific action. We will use jQuery
to do that.
Open app/js/lib/app.js
. In the setup()
method, add this just below this.$tasks = $('#tasks')
:
this.$newTask = $('#new-task');
this.$taskContent = $('#task-content');
this.$taskAuthor = $('#task-author');
We are grabbing the DOM nodes of the create task form with jQuery
. The first one is the submit button, and the 2 other are the form elements that contain the data of the new task.
At the top of the init()
method add this:
this.$newTask.on('submit', (event) => {
event.preventDefault();
console.log('create task!');
});
When the user click on the Submit
button, we detect it thanks to the submit
event and trigger a function (this kind of function is called an event handler). We temporarily only put a console.log()
statement in it to make sure it works properly.
A word of caution for frontend beginner: the fact that the text of the button is Submit
has nothing to do with the naming of the submit
event. We could put whatever text we want in the button and still be able to capture clicks with the submit
event.
Open your web browser at http://localhost:3000
and check that in your dev console you see the console.log()
statement when you click on the Submit
button:
We are making progress. Next, we still need to actually to trigger the creation of the task in the smart contract from our event handler.
Get the address (account) to send our transaction from
Note: Ethereum has the concept of accounts
, where each address has not only an associated balance but also some data. Since each account is referenced by its address, we often use address and account interchangably.
In previous episodes, we already called smart contract functions without spending any ether. It was because we did not modify data on the Blockchain, but just reading it.
This time it’s different. We will call a function of our smart contract that actually modifies data on the Blockchain. When we do this, we need to pay some ether. Don’t panic, we are just using a local ethereum Blockchain with fake ether, so you don’t need to spend any money to follow this tutorial 🙂
In Ethereum terms, we will send to the Blockchain a data structure called a “transaction”. The term transaction can be confusing here, especially since Ethereum is able to perform financial transactions. In this context, a transaction is a signed message that says:
- Call this function in this smart contract
- If the execution result in any date changes, persist them in the Blockchain
Because transaction change data and cost money to the sender, we need to make sure the address owner actually sent the transaction. That’s why transactions need to be signed.
But how do we sign a transaction?
With the private keys of your address. The local Blockchain we use, ganache-cli
, provides you with 10 pre-funded addresses and their private keys. To send our transaction, we will use one of this address. But first, we need to let the frontend know what is this address.
app.js
starts to be a bit beefy, so let’s create a new file to host the code that will get our address. We Will call this file app/js/lib/actions.js
. Put this inside:
const getAccount = (web3) => {
return new Promise((resolve, reject) => {
web3.eth.getAccounts((error, accounts) => {
if(typeof error === null) {
return reject(error);
}
resolve(accounts[0]);
});
});
};
export {
getAccount,
}
We use web3.eth.getAccounts()
to get a list of all the pre-funded accounts of ganache-cli
. We return the first one. Note that we also transform the (old school) callback-style to a (more modern) promise.
Let’s make use of our new function. In app/js/lib/app.js
lets call this in the setup()
function.
First add this import statement at the top of the file:
import { getAccount } from './actions';
Then, in the setup()
function replace this:
return new Promise((resolve, reject) => {
Todo.at(address)
.then((todo) => {
this.todo = todo;
resolve(todo);
})
.catch((error) => {
reject(error);
});
});
By this:
return new Promise((resolve, reject) => {
getAccount(this.web3)
.then((account) => {
this.account = account;
return Todo.at(address);
})
.then((todo) => {
this.todo = todo;
resolve(todo);
})
.catch((error) => {
reject(error);
});
});
Now that the frontend knows which address to send transaction from, We are finally ready to create a task in our smart contract!
Create a task in the smart contract from the event handler
We are going to call the createTask()
function of our smart contract from the event handler we just created.
In app/js/lib/app.js
, replace this:
this.$newTask.on('submit', (event) => {
event.preventDefault();
console.log('create task!');
});
By this:
this.$newTask.on('submit', (event) => {
event.preventDefault();
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}`);
});
});
Go to frontend of the Dapp in your browser, create a task, and check that you see this console.log
statement after you click on submit:
We are finally able to create some blockchain data from the frontend. That’s awesome! But we still can’t see if from the frontend. Remember, in the last episode we made a function that return some dummy data to the frontend, and that’s what we see currently in the list of tasks. We need to change that!
Reading the ACTUAL tasks of the smart contract
Approach 1
The most simple would be to have a function in our smart contract that returns an array of our tasks:
function getTasks() returns(Task[])
...
However, our tasks are represented as struct (Task
) and its currently not possible to return an array of structs in Solidity.
Approach 2
Another approach would be to return a tuple of arrays, each representing a field of our struct:
function getTasks() returns(
uint[] ids,
uint[] dates,
string[] content,
string[] author,
bool[] done
)
...
However it’s not possible to return an array of string (string[]
) in Solidity yet. We could just use a simple string and concatenate the content / author of all tasks into it, but its not trivial to do so.
Approach 3
The solution we will take involve doing multiple calls instead of just one:
- From the frontend, first we will get the list of all task ids by calling
getTaskIds()
on the smart contract - Then we will get each task one-by-one by calling
getTask()
on the smart contract for each id.
In app/js/lib/actions.js
, add this function:
const getTasks = (todo) => {
return new Promise((resolve, reject) => {
todo.getTaskIds()
.then((taskIds) => {
const promises = [];
taskIds.forEach((taskId) => {
promises.push(todo.getTask(taskId));
});
return Promise.all(promises);
})
.then((tasks) => {
resolve(tasks);
})
.catch((error) => {
reject(error);
});
});
};
Export the function:
export {
getAccount,
getTasks
};
Then at top of app/js/lib/app.js
, import getTasks()
:
import { getAccount, getTasks } from './actions';
And make use of it by replacing this in init()
:
return new Promise((resolve, reject) => {
this.todo.getTaskFixtures(0)
.then((tasks) => {
renderTasks(this.$tasks, [task]);
});
});
By this:
return new Promise((resolve, reject) => {
getTasks(this.todo)
.then((tasks) => {
renderTasks(this.$tasks, tasks);
});
});
Go to the frontend of the Dapp in your browser, create a few tasks, reload the page, and you should see the tasks you just created showing in the list of tasks:
Fantastic! You made it!
The end
Where to go from there? There are different features / improvements that can be made:
- We still cant toggle the done status of a task
- We have no feedback message after we create a task
- After we create a new task, we still need to refresh manually the page to see the new task appearing
- The last 2 points can get considerably complex when you consider that for production deployments (mainnet) the latency between the moment you send a transaction and the moment it gets mined is at least 15s. Worst, the transaction can get rejected. How to poll the network to see when the transaction gets mined? How to handle failure cases? What to show to the user?
You will find the answers to all these questions by subscribing to EatTheBlocks Pro. Every week I will release a professionally edited video that goes in the deep topic that will allow you to produce a production-ready Dapp.
DappjQuerySolidity
Leave a Reply