This a continuation of our Todo web app.
So far we have been adding just one task at a time to our to-do app. However, we want to give the user more power and the flexibility to add more than one task at once.
In our app, we are going to use a comma ,
as a delimiter(separator) for more than one task.
In App.jsx
, go to the function where we were handling form submission. Here π
const handleSubmit = (e) => {
e.preventDefault()
// console.log(e.target.taskInput)
const inputValue = e.target.taskInput.value.trim()
if (inputValue) {
const taskObject = {
id: uniqueId(),
title: inputValue,
}
setTasks([...tasks, taskObject])
e.target.taskInput.value = ''
} else {
alert('Can not add an empty task')
}
}
As you can see in the code above, we are just getting the input value and using it to create an object that we then use to update our state with.
What if we wanted to add more than one task and use a separator to extract each task from the whole input then use each task extracted to update the state?
To do that we need to refactor our code a little bit.
First, delete some of the code in the function and remain with π
const handleSubmit = (e) => {
e.preventDefault()
}
Now let us rethink how our app should work.
A user enters many tasks and separates each task with a comma then she submits the tasks.
We get the whole string and now we need to use some logic to separate each task where there is a comma use each task to update our state.
Let us take it step-by-step.
Step1. We want to get the input element.
In part-1 of this article, we learned how to use e.target
and e.currentTarget
to get the input element.
It won't a bad idea to explore another way(s) to get the input element.
We will use JavaScript built-in method to access an HTML element. Let's use getElementById
.
In our code, we set the id
of the input element in the form to taskInput
.
We will use the id to the input element and assign it to a variable I called it taskInputElement
. Then let's log the element to the browser console.
Your code should look like this.
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
console.log(taskInputElement)
}
Note that we could also useπ (among other methods).
document.querySelector('#taskInput')
If you click on Add Task
button, you should see the input element logged to the consoleπ
Now that we have the input element, we can access its value and assign it to a variable value
and log the value to the console. Let's also make sure we trim
whitespaces as I explained in part-1 of the article.
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
const value = taskInputElement.value.trim()
console.log(value)
}
If you type something and click on Add Task button you should see the value logged to the console.
Let's add more tasks separated by a comma and click on the Add Task
button again.
We get our long input value logged to the console.
Now we want to separate our tasks using the comma.
We will use the JavaScript string method split
to separate our input value. Then log the result to the browser console.
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
const value = taskInputElement.value.trim()
const splitTasks = value.split(',')
console.log(splitTasks)
}
You should see a new array containing substrings of the input value logged to the console.
side noteπβ
The split
method returns a new array containing substrings. If you don't provide the delimiter and just do split('')
the string will be split at every character boundary (at each character).
For example,
const task = 'Go to the gym';
console.log(task.split(''));
//output
['G', 'o', ' ', 't', 'o', ' ', 't', 'h', 'e', ' ', 'g', 'y', 'm']
// if you use a space as a delimiter (note a space in split);
console.log(task.split(' '));
//output
['Go', 'to', 'the', 'gym']
In the same way, we can use a character to define where a string should be separated.
In our case, we used a comma.
...end of side noteπ
Now we want to loop over the array and for each task, we create an object with an id and title.
We have many options, we can use loops: while, for, do-while, for in, for of... the list goes on.
You can also use the array method map()
(though you'll have to do some extra work).
Let's use the a for loop.π
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
const value = taskInputElement.value.trim()
const splitTasks = value.split(',')
const tasksLength = splitTasks.length
if (value) {
for (let i = 0; i < tasksLength; i++) {
const newTask = {
id: uniqueId(),
title: splitTasks[i],
}
console.log(newTask)
}
}
}
First, we get the length of the array of strings with splitTasks.length
and assign the length to a variable. We will use the length to determine how many times an loop should run. I assigned the length to a variable for readability. The length of the splitTasks
vary depending on how many tasks you enter into the input.
We want to run a for loop only if we have a value in the input field. That's why we check if the value translates to true(if there is a value) using if(value)
.
Then we create a for loop and initialize the starting point using i = 0
and set the condition i < tasksLength
, which means the loop will execute as long as i
is less than tasksLength
with increments of 1.
On each iteration of the loop, the i
variable is incremented by 1 using the expression i++
( same as i + 1
)
The number of times the loop will run (where the loop will stop) depends on the tasksLength
.
For each iteration, we create an object with an id
(generated by uuid) and a title
property that is set to the current task at the i
index of the splitTasks
array.
side note πβ
For example, the first time the loop runs the title will be set this way title: splitTasks[0]
, after the first round, the i
will be incremented by 1. Our next title will be set title: splitTasks[1]
and so on... till we reach tasksLength
.
Remember arrays are 0 indexed, so there is no need to subtract one from the split tasks length since we are checking the condition using the less-than operator <
.
Let's say our tasks length is 5 (in that we have five items). If we run a loop that stops at i < 5
, in our last iteration i
will be 4 (we stop at i
is less than 5). Thus, the loop will iterate five times, with i
taking on the values of 0, 1, 2, 3, and 4.
...end of side noteπ
Back to our web app in the browser, if you put many tasks separated by a comma and click Add Task button, you should see objects created from the substrings logged to the console.π
Now we know that our for loop is working.
Instead of logging to the console, we want to use the objects to update the state of tasks
.
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
const value = taskInputElement.value.trim()
const splitTasks = value.split(',')
const tasksLength = splitTasks.length
if (value) {
for (let i = 0; i < tasksLength; i++) {
const newTask = {
id: uniqueId(),
title: splitTasks[i],
}
setTasks((prevTasks) => [...prevTasks, newTask])
}
}
}
In, setTasks((prevTasks) => [...prevTasks, newTask])
we use setTasks
function, which is the updater function for our state variable tasks
.
We want to update the state of our tasks, by adding a new task object to an existing array of tasks.
We use an arrow function to spread the previous state of tasks
(stored in prevTasks
) into a new array using the spread syntax ...prevTasks
. Then, we append the newTask
object to the end of this new array using the spread syntax [...prevTasks, newTask]
.
prevTasks
represent the previous state of tasks. The setTasks
function has access to the tasks
state since we set it as the updater function for the state of our tasks
when we first set the state const [tasks, setTasks] = useState([])
.
The reason why we store the previous state before updating the state is that the new state depends on the previous state.
Remember we are running a for loop, so after we add our first task object, we don't want to go back to the tasks
directly to get the state. We want once we add an object, the new state to be stored in a variable in our case prevTasks
so that in the next iteration the loop gets the updated version of tasks before appending another task. That way we can avoid potential race conditions that may occur when updating the state directly.
We want to append a new task object to the existing array of tasks, without mutating the original array. So we spread the previous state ( stored in prevTasks
) then we append each of our new task object newTask
to the previous tasks in a sequential manner.
So, our new state will contain the previous tasks plus the new task(s).
Let's also make sure we clear the input value after our tasks have been added and also alert the user to enter a task if he tries to add an empty task.
const handleSubmit = (e) => {
e.preventDefault()
const taskInputElement = document.getElementById('taskInput')
const value = taskInputElement.value.trim()
const splitTasks = value.split(',')
const tasksLength = splitTasks.length
if (value) {
for (let i = 0; i < tasksLength; i++) {
const newTask = {
id: uniqueId(),
title: splitTasks[i],
}
setTasks((prevTasks) => [...prevTasks, newTask])
taskInputElement.value = ''
}
} else {
alert('Can not add an empty task')
}
}
After adding the tasks we reset our input value using taskInputElement.value = ''
We checked if there is a value before we ran our for loop, what if there is no value? We should alert the user. That's why I added the else.
In our jsx let's make the user know that he can add more than one task at once.
Within the main
element but above the div let's add a h2
with some textπ
//some code above
<main className='parent-container'>
<h2 className='title'>Separate more than one task with a comma ,
</h2>
<div className='content'>
//some code below
Now in index.css let's use the utility class title
to style the h2.
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
/* colors */
.pri-color {
color: #ffffff;
}
.bg-pri {
background-color: rgb(12, 74, 110);
}
/* css for elements */
.parent-container {
@apply flex flex-col justify-center items-center min-h-screen;
}
/* our new style for title π add flex-col π */
.title {
@apply text-sky-900 text-xl py-2 my-2 font-medium;
}
.content {
@apply bg-black p-7 rounded-2xl flex justify-around items-center flex-col;
}
.input-label {
@apply text-2xl pri-color px-3 font-semibold;
}
.add-btn {
@apply text-lg bg-pri rounded-lg px-3 py-1 ml-3 pri-color font-medium;
}
.ul-container {
@apply py-5 list-none;
}
.task-container {
@apply flex justify-between pri-color bg-pri py-1 px-3 rounded-md text-lg my-2;
}
.empty-tasks {
@apply pri-color p-1 text-lg;
}
.trash-icon {
@apply w-5 h-5 hover:scale-125 transition;
}
.clear-btn {
@apply text-xl block w-full bg-red-900 hover:opacity-80 transition rounded-lg px-3 py-1 pri-color font-medium;
}
}
Add flex-col
to .parent-container
as I did π
The title should be displayed correctly.
Congratulations on making it this farππ
You are destined for greatness.
You can tell me any problem you encountered during this tutorial.
That's it, you can now add more than one task at once by separating the tasks with a comma.
In part-4 we will persist/store the tasks to the localStorage API.