6. Handle Tasks
In the sixth and final chapter of the tutorial on building an end-to-end dapp on Aptos, you will add functionality to the app so the user interface is able to handle cases where an account has created a list.
We have covered how to fetch data (an account’s todo list) from chain and how to submit a transaction (new todo list) to chain using Wallet.
Let’s finish building our app by implementing fetch tasks and adding a task function.
Fetch tasks
- Create a local state
tasks
that will hold our tasks. It will be a state of a Task type (that has the same properties we set on our smart contract):
type Task = {
address: string;
completed: boolean;
content: string;
task_id: string;
};
function App() {
const [tasks, setTasks] = useState<Task[]>([]);
...
}
- Update our
fetchList
function to fetch the tasks in the account’sTodoList
resource:
const fetchList = async () => {
if (!account) return [];
try {
const todoListResource = await aptos.getAccountResource(
{accountAddress:account?.address,
resourceType:`${moduleAddress}::todolist::TodoList`}
);
setAccountHasList(true);
// tasks table handle
const tableHandle = (todoListResource as any).tasks.handle;
// tasks table counter
const taskCounter = (todoListResource as any).task_counter;
let tasks = [];
let counter = 1;
while (counter <= taskCounter) {
const tableItem = {
key_type: "u64",
value_type: `${moduleAddress}::todolist::Task`,
key: `${counter}`,
};
const task = await aptos.getTableItem<Task>({handle:tableHandle, data:tableItem});
tasks.push(task);
counter++;
}
// set tasks in local state
setTasks(tasks);
} catch (e: any) {
setAccountHasList(false);
}
};
This part is a bit confusing, so stick with us!
Tasks are stored in a table (this is how we built our contract). To fetch a table item (i.e a task), we need that task’s table handle. We also need the task_counter
in that resource so we can loop over and fetch the task with the task_id
that matches the task_counter
.
const tableHandle = (TodoListResource as any).data.tasks.handle;
const taskCounter = (TodoListResource as any).data.task_counter;
Now that we have our tasks table handle and our task_counter
variable, lets loop over the taskCounter
. We define a counter
and set it to 1 as the task_counter / task_id is never less than 1.
We loop while the counter
is less then the taskCounter
and fetch the table item and push it to the tasks array:
let tasks = [];
let counter = 1;
while (counter <= taskCounter) {
const tableItem = {
key_type: "u64",
value_type: `${moduleAddress}::todolist::Task`,
key: `${counter}`,
};
const task = await provider.getTableItem(tableHandle, tableItem);
tasks.push(task);
counter++;
}
We build a tableItem
object to fetch. If we take a look at our table structure from the contract:
tasks: Table<u64, Task>,
We see that it has a key
type u64
and a value
of type Task
. And whenever we create a new task, we assign the key
to be the incremented task counter.
// adds the new task into the tasks table
table::upsert(&mut todo_list.tasks, counter, new_task);
So the object we built is:
{
key_type: "u64",
value_type:`${moduleAddress}::todolist::Task`,
key: `${taskCounter}`,
}
Where key_type
is the table key
type, key
is the key value we are looking for, and the value_type
is the table value
which is a Task
struct. The Task struct uses the same format from our previous resource query:
- The account address who holds that module = our profile account address
- The module name the resource lives in =
todolist
- The struct name =
Task
The last thing we want to do is display the tasks we just fetched.
- In our
App.tsx
file, update our UI with the following code:
{
!accountHasList ? (
<Row gutter={[0, 32]} style={{ marginTop: "2rem" }}>
<Col span={8} offset={8}>
<Button
disabled={!account}
block
onClick={addNewList}
type="primary"
style={{ height: "40px", backgroundColor: "#3f67ff" }}
>
Add new list
</Button>
</Col>
</Row>
) : (
<Row gutter={[0, 32]} style={{ marginTop: "2rem" }}>
<Col span={8} offset={8}>
{tasks && (
<List
size="small"
bordered
dataSource={tasks}
renderItem={(task: any) => (
<List.Item actions={[<Checkbox />]}>
<List.Item.Meta
filename="{task.content}
description={
<a
href={`https://explorer.aptoslabs.com/account/${task.address}/`}
target="_blank"
>{`${task.address.slice(0, 6)}...${task.address.slice(-5)}`}</a>
}
/>
</List.Item>
)}
/>
)}
</Col>
</Row>
);
}
That will display the Add new list button if account doesn’t have a list or instead the tasks if the account has a list.
Go ahead and refresh your browser - see the magic!
We haven’t added any tasks yet, so we simply see a box of empty data. Let’s add some tasks!
Add task
- Update our UI with an add task input:
Import Input
from antd
- import { Input } from "antd";
{!accountHasList ? (
...
) : (
<Row gutter={[0, 32]} style={{ marginTop: "2rem" }}>
// Add this!
<Col span={8} offset={8}>
<Input.Group compact>
<Input
style={{ width: "calc(100% - 60px)" }}
placeholder="Add a Task"
size="large"
/>
<Button
type="primary"
style={{ height: "40px", backgroundColor: "#3f67ff" }}
>
Add
</Button>
</Input.Group>
</Col>
...
</Row>
)}
We have added a text input to write the task and a button to add the task.
- Create a new local state that holds the task content:
function App() {
...
const [newTask, setNewTask] = useState<string>("");
...
}
- Add an
onWriteTask
function that will get called whenever a user types something in the input text:
function App() {
...
const [newTask, setNewTask] = useState<string>("");
const onWriteTask = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNewTask(value);
};
...
}
- Find our
<Input/>
component, add theonChange
event to it, pass it ouronWriteTask
function and set the input value to be thenewTask
local state:
<Input
onChange={(event) => onWriteTask(event)} // add this
style={{ width: "calc(100% - 60px)" }}
placeholder="Add a Task"
size="large"
value={newTask} // add this
/>
Cool! Now we have a working flow that when the user types something on the Input component, a function will get fired and set our local state with that content.
- Let’s also add a function that submits the typed task to chain! Find our Add
<Button />
component and update it with the following
<Button
onClick={onTaskAdded} // add this
type="primary"
style={{ height: "40px", backgroundColor: "#3f67ff" }}
>
Add
</Button>
That adds an onClickevent
that triggers an onTaskAdded
function.
When someones adds a new task we:
- want to verify they are connected with a wallet.
- build a transaction payload that would be submitted to chain.
- submit it to chain using our wallet.
- wait for the transaction.
- update our UI with that new task (without the need to refresh the page).
- Add an
onTaskAdded
function with:
const onTaskAdded = async () => {
// check for connected account
if (!account) return;
setTransactionInProgress(true);
const transaction:InputTransactionData = {
data:{
function:`${moduleAddress}::todolist::create_task`,
functionArguments:[newTask]
}
}
// hold the latest task.task_id from our local state
const latestId = tasks.length > 0 ? parseInt(tasks[tasks.length - 1].task_id) + 1 : 1;
// build a newTaskToPush object into our local state
const newTaskToPush = {
address: account.address,
completed: false,
content: newTask,
task_id: latestId + "",
};
try {
// sign and submit transaction to chain
const response = await signAndSubmitTransaction(transaction);
// wait for transaction
await aptos.waitForTransaction({transactionHash:response.hash});
// Create a new array based on current state:
let newTasks = [...tasks];
// Add item to the tasks array
newTasks.push(newTaskToPush);
// Set state
setTasks(newTasks);
// clear input text
setNewTask("");
} catch (error: any) {
console.log("error", error);
} finally {
setTransactionInProgress(false);
}
};
Let’s go over on what is happening.
First, note we use the account
property from our wallet provider to make sure there is an account connected to our app.
Then we build our transaction data to be submitted to chain:
const transaction:InputTransactionData = {
data:{
function:`${moduleAddress}::todolist::create_task`,
functionArguments:[newTask]
}
}
function
- is built from the module address, module name and the function name.functionArguments
- the arguments the function expects, in our case the task content.
Then, within our try/catch block, we use a wallet provider function to submit the transaction to chain and an SDK function to wait for that transaction. If all goes well, we want to find the current latest task ID so we can add it to our current tasks state array. We will also create a new task to push to the current tasks state array (so we can display the new task in our tasks list on the UI without the need to refresh the page).
TRY IT!
Type a new task in the text input, click Add, approve the transaction and see it being added to the tasks list.
Mark task as completed
Next, we can implement the complete_task
function. We have the checkbox in our UI so users can mark a task as completed.
- Update the
<Checkbox/>
component with anonCheck
property that would call anonCheckboxChange
function once it is checked:
Import List
from antd
- import { List } from "antd";
<List.Item actions={[
<Checkbox onChange={(event) => onCheckboxChange(event, task.task_id)}/>
]}>
- Create the
onCheckboxChange
function (make sure to importCheckboxChangeEvent
fromantd
-import { CheckboxChangeEvent } from "antd/es/checkbox";
):
const onCheckboxChange = async (
event: CheckboxChangeEvent,
taskId: string
) => {
if (!account) return;
if (!event.target.checked) return;
setTransactionInProgress(true);
const transaction:InputTransactionData = {
data:{
function:`${moduleAddress}::todolist::complete_task`,
functionArguments:[taskId]
}
}
try {
// sign and submit transaction to chain
const response = await signAndSubmitTransaction(transaction);
// wait for transaction
await aptos.waitForTransaction({transactionHash:response.hash});
setTasks((prevState) => {
const newState = prevState.map((obj) => {
// if task_id equals the checked taskId, update completed property
if (obj.task_id === taskId) {
return { ...obj, completed: true };
}
// otherwise return object as is
return obj;
});
return newState;
});
} catch (error: any) {
console.log("error", error);
} finally {
setTransactionInProgress(false);
}
};
Here we basically do the same thing we did when we created a new list or a new task.
We make sure there is an account connected, set the transaction in progress state, build the transaction payload, submit the transaction, wait for it and update the task on the UI as completed.
- Update the
Checkbox
component to be checked by default if a task has already marked as completed:
Import Checkbox
from antd
- import { Checkbox } from "antd";
...
<List.Item
actions={[
<div>
{task.completed ? (
<Checkbox defaultChecked={true} disabled />
) : (
<Checkbox
onChange={(event) =>
onCheckboxChange(event, task.task_id)
}
/>
)}
</div>,
]}
>
...
Try it! Check a task’s checkbox, approve the transaction and see the task marked as completed.
You have now learned how to build a dapp on Aptos from end to end. Congratulations! Tell your friends. :-)