I'm trying to make a simple todo in react. I want to be able to click in the button next to the todo text and mark it as complete, with a line passing through it, so I guess the point of the button would be to toggle between the two stylings. But I don't know how to apply the styling to that specific todo. Here's my code so far:
import React, { useState } from 'react';
function App() {
const [todos, setTodos] = useState([])
const toggleComplete = (i) => {
setTodos(todos.map((todo, k) => k === i ? {
...todo, complete: !todo.complete
} : todo))
}
const handleSubmit = (event) => {
event.preventDefault()
const todo = event.target[0].value
setTodos((prevTodos) => {
return [...prevTodos, {
userTodo: todo, completed: false, id: Math.random().toString()
}]
})
}
return (
<div>
<form onSubmit={handleSubmit}>
<input placeholder='name'></input>
<button type='submit'>submit</button>
</form>
<ul>
{todos.map((todos) => <li key={todos.id}>
<h4>{
todos.completed ? <s><h4>{todos.userTodo}</h4></s> : <h4>{todos.userTodo}</h4>}
</h4>
<button onClick={toggleComplete}>Mark as complete</button>
</li>)}
</ul>
</div>
);
}
export default App;
You can see that the toggleComplete function takes a parameter i which is the id of the todo, so you should call it like onClick={() => toggleComplete(todos.id)}.
However this still didn't work since you are assigning random numbers as strings as id to the todos then iterating over the array.
As Alex pointed out, there's a bug in your code regarding the completed toggle, so I fixed it and here's a working version of the code you can take a look at and improve:
import React, { useState } from "react";
export default function App() {
const [todos, setTodos] = useState([]);
const toggleComplete = (i) => {
setTodos(
todos.map((todo, k) => {
return k === i
? {
...todo,
completed: !todo.completed
}
: todo;
})
);
};
const handleSubmit = (event) => {
event.preventDefault();
const todo = event.target[0].value;
setTodos((prevTodos) => {
return [
...prevTodos,
{
userTodo: todo,
completed: false,
id: prevTodos.length
}
];
});
};
return (
<div>
<form onSubmit={handleSubmit}>
<input placeholder="name"></input>
<button type="submit">submit</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.completed ? (
<s>
<p>{todo.userTodo}</p>
</s>
) : (
<p>{todo.userTodo}</p>
)}
<button onClick={() => toggleComplete(todo.id)}>
Mark as complete
</button>
</li>
))}
</ul>
</div>
);
}
There are 2 problems in your code as i see:
typo in the toggleComplete function
Fix: the following code complete: !todo.complete shopuld be completed: !todo.completed as this is the name of the key that you're setting below on handleSubmit.
the toggleComplete function receives as an argument the javascript event object and you are comparing it with the key here:
(todo, k) => k === i
(see more here:
https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event)
Fix: You can modify the lines of code for the todo render as follows:
{todos.map((todo, index) => <li key={todo.id}>
<React.Fragment>{
todo.completed ? <del><h4>{todo.userTodo}</h4></del> : <h4>{todo.userTodo}</h4>}
</React.Fragment>
<button onClick={() => {toggleComplete(index)}}>Mark as complete</button>
</li>)}
Related
I'm trying to delete ToDo list by event delegation so I placed delete onClick handler on the ul element. Question I have is why toDo != e.target.parentNode.children[0].innerText never equal to true even when I place same value for both elements. So after I add "study" and "eat" for toDo list then click on delete button next to "study", e.target.parentNode.children[0].innerText shows "study" and toDo shows "study" as well, but it doesn't get filtered out. They both have string type so I'm confused. So why doesn't this work?
import './styles.css';
import React, {useState} from 'react';
export default function App() {
const [toDoList, setToDoList] = useState([]);
const [value, setValue] = useState([]);
const addToDo = () => {
let copyToDoList = [...toDoList];
setToDoList([...copyToDoList, value]);
setValue("");
}
const deleteToDo = (e) => {
let copyToDoList = [...toDoList];
let filteredList = copyToDoList.filter((toDo) => {
return toDo != e.target.parentNode.children[0].innerText})
console.log(filteredList)
setToDoList(filteredList);
}
const handleChange = (e) => {
setValue(e.target.value);
}
return (
<div>
<h1>Todo List</h1>
<div>
<input value={value} onChange={handleChange} type="text" placeholder="Add your task" />
<div>
<button onClick={addToDo}>Submit</button>
</div>
</div>
<ul onClick={deleteToDo}>
{toDoList.map((toDo, idx) => {
return(
<li>
<span>{toDo} </span>
<button>Delete</button>
</li>
)
})}
</ul>
</div>
);
}
Try the following command. Then you would see that toDo and e.target.parentNode.children[0].innerText have different lengths.
e.target.parentNode.children[0].innerText has one more lengths than toDo
console.log(toDo.length, e.target.parentNode.children[0].innerText.length);
And the solution is below
change this line {toDo} to {toDo}
Remove space.
This happens because you left a space character " " inside the span. This means "aaa" !=="aaa ".
To avoid this error you can add the trim like I did. (but just removing the space is enough...)
const deleteToDo = e => {
const copyToDoList = [...toDoList]
const filteredList = copyToDoList.filter( toDo => {
return toDo !== e.target.parentNode.children[0].innerText.trim()
})
setToDoList(filteredList)
}
<ul onClick={deleteToDo} > {
toDoList.map((toDo, idx) => {
return (
<li key={idx}>
<span>{toDo}</span>
<button>Delete</button>
</li>
)
})
}</ul>
I am working through a tutorial for a course I'm taking. The lab I'm working on walks through creating a to-do app. I'm on step 3, which asks us to create a button that deletes a task. I feel ridiculous, because I know I can figure it out but...well, I haven't yet! I will post the code to see if there are any initial issues, and then update with the methods I've already tried. Any help is greatly appreciated. Thank you!
import React, { useState } from "react";
import "./App.css";
const App = () => {
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = {
id: new Date().getTime(),
text: todo.trim(),
completed: false,
};
if (newTodo.text.length > 0) {
setTodos([...todos].concat(newTodo));
setTodo("");
} else {
alert("Enter Valid Task");
setTodo("");
}
}
const deleteTodo = (id) => {
let updatedTodos = [...todos].filter((todo) => todo.id !== id);
setTodos(updatedTodos);
}
const button = <button onClick={() => deleteTodo(todo.id)}>Delete</button>
return (
<div>
<h1>To-do List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setTodo(e.target.value)}
placeholder="Add a new task"
value={todo}
/>
<button type="submit">Add Todo</button>
</form>
{todos.map((todo) => <div>ID: {todo.id} Task: {todo.text} {button}</div>)}
</div>
);
};
export default App;
I didn't just copy and paste, so it's possible that I messed something up while typing. I'm expecting the deleteTodo() function to accept a todo.id and filter the list of todos, excluding the one I want to delete. I'm thinking that the issue may be cause by the way I've created the button? Again, I'm not sure why I can't figure it out. TIA.
EDIT: Okay, it works now! Thank you all so much for explaining this. For anyone else that comes across this problem, here's where I mis-stepped:
const button = <button onClick={() => deleteTodo(todo.id)}Delete<button>
#Nicholas Tower's explanation was very clear--creating this outside of .map(...)causes deleteTodo to get the todo state, not the not the todo I want it to delete from the todos array. #Lars Vonk, #0stone0, and #Sudip Shrestha all said this as well. #Sudip Shrestha and #pilchard also helped correct the deleteTodo function. Again, I really appreciate all the help. The code works now. I'll show the updates so people having a similar issue can compare:
import React from "react";
import "./App.css";
const App = () => {
const [todos, setTodos] = React.useState([]);
const [todo, setTodo] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = {
id: new Date().getTime(),
text: todo.trim(),
completed: false,
};
if (newTodo.text.length > 0) {
setTodos(todos.concat(newTodo));
setTodo("");
} else {
alert("Enter a valid task");
setTodo("");
}
}
// update the state using setState, rathar than mutating it directly #Sudip Shrestha
const deleteTodo = id => {
setTodos(prevState => {
return prevState.filter(todo => todo.id !== id)
});
};
// line 51: button placed inside .map(), as per many suggestions below.
return (
<>
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setTodo(e.target.value)}
placeholder="Add a new task..."
value={todo}
/>
</form>
{todos.map((todo) =>
<div>
ID: {todo.id} Task: {todo.text}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>)}
</>
);
};
export default App;
const button = <button onClick={() => deleteTodo(todo.id)}>Delete</button>
You're creating this button element just once, and the todo variable it refers to is the todo state, which is a string (usually an empty string). Since todo is a string, todo.id is undefined, and deleteTodo can't do anything with that.
You need to create separate buttons for each item, so you should move this code down into your .map:
{todos.map((todo) => (
<div>
ID: {todo.id} Task: {todo.text}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
))}
Now each item has its own button, with its own onClick function. And in those functions, todo is the item of the array.
The button cannot access which todo it has I think you should put the code from the const button where you are referring to it or by changing it to const button = (todo) => <button onClick={ () => deleteTodo(todo.id); }>Delete</button> and access it by doing {button()}
const button = <button onClick={() => deleteTodo(todo.id)}>Delete</button>
This has the same callBack for each todo, you should move this inside your map so that todo.id refers to the iterator of the map():
{todos.map((todo) => (
<React.Fragment>
<div>ID: {todo.id} Task: {todo.text}</div>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</React.Fragment>
))}
Updated Demo:
const { useState } = React;
const App = () => {
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = {
id: new Date().getTime(),
text: todo.trim(),
completed: false,
};
if (newTodo.text.length > 0) {
setTodos([...todos].concat(newTodo));
setTodo("");
} else {
alert("Enter Valid Task");
setTodo("");
}
}
const deleteTodo = (id) => {
let updatedTodos = [...todos].filter((todo) => todo.id !== id);
setTodos(updatedTodos);
}
return (
<div>
<h1>To-do List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setTodo(e.target.value)}
placeholder="Add a new task"
value={todo}
/>
<button type="submit">Add Todo</button>
</form>
{todos.map((todo) => (
<React.Fragment>
<div>ID: {todo.id} Task: {todo.text}</div>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</React.Fragment>
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Try this:
const button = (t) => <button onClick={() => deleteTodo(t.id)}>Delete</button>
and then, in the map
{todos.map((todo) => <div>ID: {todo.id} Task: {todo.text} {button(todo)}</div>)}
this way, the "delete todo" button will be bound to the specific todo ID, avoiding being bound to whatever the current value of todo is in the app.
Its better to update the state using setState. Muting the state directly breaks the primary principle of React's data flow (which is made to be unidirectional), making your app very fragile and basically ignoring the whole component lifecycle.
Also You need to change the delete from string to function and pass the id or place the jsx directly inside map function.
import React, { useState } from 'react'
const App = () => {
const [todos, setTodos] = useState([])
const [todo, setTodo] = useState('')
const handleSubmit = e => {
e.preventDefault()
const newTodo = {
id: new Date().getTime(),
text: todo.trim(),
completed: false,
}
if (newTodo.text.length > 0) {
setTodos([...todos].concat(newTodo))
setTodo('')
} else {
alert('Enter Valid Task')
setTodo('')
}
}
/*
* Changed Here
*/
const deleteTodo = id => {
setTodos(prevState => {
return prevState.filter(todo => todo?.id != id)
})
}
const button = id => <button onClick={() =>
deleteTodo(id)}>Delete</button>
return (
<div>
<h1>To-do List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={e => setTodo(e.target.value)}
placeholder="Add a new task"
value={todo}
/>
<button type="submit">Add Todo</button>
</form>
{todos.map(todo => (
<div key={todo.id}>
ID: {todo.id} Task: {todo.text} {button(todo.id)}
</div>
))}
</div>
)
}
export default App
Problem in:
const button = <button onClick={() => deleteTodo(todo.id)}>Delete</button>
You can use
const Button = (props) => {
return (
<button
className={`btn ${props.className}`}
title={`${props.title}`}
onClick={props.onClick ? () => props.onClick() : null}
>
{props.children}
</button>
);
};
after that, call it like this
<Button className="delete" title="delete" onClick={()=>deleteTodo(todo.id)}>Delete</Button>
As I know React creates a new Virtual DOM and compares it with previous one, then it updates Browser DOM with least number of changes possible without rendering the entire DOM again. (in short)
In React documentation I have also read how key should work.
for demonstrating this, I have created todo app, but when I'm deleting one element, all previous elements are re-rendered again (except if I'm not deleting recently added element)
Here is screenshot:
(In Chrome developer tool paint flashing is active for showing renders)
My questions:
Why do previous elements re-render again?
Why key could not fix this problem?
Here is the entire code:
TodoApp.jsx
import React, { useState } from 'react';
import Input from './Input';
import List from './List';
const TodoApp = () => {
const [inputText, setInputText] = useState('');
const [todos, setTodos] = useState([]);
const onSubmitChange = e => {
e.preventDefault();
setTodos([
...todos,
{ text: inputText, completed: false, id: Math.random() * 1000 },
]);
setInputText('');
};
const onChangeEvent = e => {
setInputText(e.target.value);
};
return (
<div>
{todos.map(todo => {
return (
<List todos={todos} setTodos={setTodos} todo={todo} key={todo.id} />
);
})}
<Input
onSubmit={onSubmitChange}
onChange={onChangeEvent}
inputText={inputText}
/>
</div>
);
};
export default TodoApp;
List.jsx
import React from 'react';
import "../todolist/css/TodoApp.css"
const List = ({ todo, todos, setTodos }) => {
const deleteHandle = () => {
setTodos(todos.filter(el => el.id !== todo.id));
};
const completeHandle = () => {
setTodos(
todos.map(el => {
if (el.id === todo.id) {
return { ...el, completed: !el.completed };
}
return el;
})
);
};
return (
<div className={`${todo.completed ? 'completed' : ''}`}>
<div>{todo.text}</div>
<div className="btns">
<button onClick={deleteHandle} className="btn btn-delete">
Delete
</button>
<button onClick={completeHandle} className="btn btn-complete">
complete
</button>
</div>
</div>
);
};
export default List;
Input.jsx
import React from 'react';
const Input = ({ onSubmit, onChange, inputText }) => {
return (
<form onSubmit={onSubmit}>
<input
onChange={onChange}
value={inputText}
type="text"
name="myInput"
autoComplete="off"
/>
</form>
);
};
export default Input;
I'm starting studying React and I was following this YouTube tutorial of a TO DO LIST using React.
https://www.youtube.com/watch?v=E1E08i2UJGI
My form loads perfectly, but if I write something and press any button I get the message: "completedTask is not a function". The same goes for buttons that call a function 'removeTask' and 'setEdit'.
I don't understand the reason I'm getting such error message. In the tutorial it works when the buttons are clicked. I've read in some forums that it would be related to the fact that you can't use map on Objects (non-array elements), but I didn't understand it very well and I don't know how to fix it. And the most mysterious parte: why does his code work and mine do not?
Could anybody please explain it?
Obs1: I found in another post that return tasks.tasks.map((task, index) solved the problem for 'task.map is not a function' error message in TASK.JS. I'm using it instead of return tasks.map((task, index) but I also didn't understant the reason.
Obs2: I don't think it makes any difference for the error message, but I used the button tag instead using React Icons as the video suggests.
=== TASKLIST.JS ===
import React, { useState } from 'react'
import Task from './Task'
import TaskForm from './TaskForm'
function TaskList() {
const [tasks, setTasks] = useState([]);
const addTask = task => {
if(!task.text || /^\s*$/.test(task.text)) {
return
}
const newTasks = [task, ...tasks];
setTasks(newTasks);
};
const updateTask = (taskId, newValue) => {
if(!newValue.text || /^\s*$/.test(newValue.text)) {
return
}
setTasks(prev => prev.map(item => (item.id === taskId ? newValue : item)));
};
const removeTask = id => {
const removeArr = [...tasks].filter(task => task.id !== id);
setTasks(removeArr)
};
const completedTask = id => {
let updatedTasks = tasks.map(task => {
if (task.id === id) {
task.isComplete = !task.isComplete
}
return task
})
setTasks(updatedTasks);
};
return (
<div>
<h1>CabeƧalho</h1>
<TaskForm onSubmit={addTask}/>
<Task
tasks={tasks}
completedTask={completedTask}
removeTask={removeTask}
updateTask={updateTask} />
</div>
)
}
export default TaskList
=== TASK.JS ===
import React, { useState } from 'react'
import TaskForm from './TaskForm'
function Task(tasks, completedTask, removeTask, updateTask) {
const [edit, setEdit] = useState({
id: null,
value: ''
})
const submitUpdate = value => {
updateTask(edit.id, value)
setEdit({
id: null,
value: ''
})
}
if (edit.id) {
return <TaskForm edit={edit} onSubmit={submitUpdate} />;
}
return tasks.tasks.map((task, index) => (
<div className={task.isComplete ? 'task-row complete' : 'task-row'} key={index}>
{task.text}
<div className="buttons">
<button onClick={() => completedTask(task.id)} className='completed-icon'>done</button>
<button onClick={() => removeTask(task.id)} className='delete-icon'>delete</button>
<button onClick={() => setEdit({id: task.id, value: task.text})} className='edit-icon'>edit</button>
</div>
</div>
))
};
export default Task
=== TASKFORM.JS ===
import React, { useState, useEffect, useRef } from 'react'
function TaskForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus()
})
const handleChange = e => {
setInput(e.target.value);
}
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 1000),
text: input
});
setInput('');
};
return (
<form className="task-form" onSubmit={handleSubmit}>
{props.edit ? (
<>
<input type="text" placeholder="Update your task" value={input} name="text" className="task-input" onChange={handleChange} ref={inputRef}/>
<button className="task-button edit" onChange={handleChange}>Update a task</button>
</>
) : (
<>
<input type="text" placeholder="Add a new task" value={input} name="text" className="task-input" onChange={handleChange} ref={inputRef}/>
<button className="task-button" onChange={handleChange}>Add a task</button>
</>
)}
</form>
)
}
export default TaskForm
Try this:
function Task({ tasks, completedTask, removeTask, updateTask }) {
// ...
}
You can also do this (semantically equivalent):
function Task(props) {
const { tasks, completedTask, removeTask, updateTask } = props;
// ...
}
As mentioned here:
The first parameter will be props object itself. You need to destructure the object.
You can read more about object destructuring here.
i'm trying to work on a todo app with the option of editing.
the goal is to click on the edit button and that'll open an input field, type the new editted text and then have two choices , save the changes or not.
i've managed to write the code for it to open the input field, and to be able to click on the button to not save changes ,but what happens is that it opens the input field for all of the todos ,and whenever i try to update the value of the specific todo i get the error "todos.map is not a function".
Here's the TodoList.js
import Todo from "./Todo";
import AddTodo from "./AddTodo";
import { v4 as uuidv4 } from "uuid";
const TodoList = () => {
//Handlers Add/Remove/RemoveAll/Edit
const addTodoHandler = (input) => {
setTodos([
...todos,
{
name: input,
id: uuidv4(),
},
]);
};
const changeEditMode = (id) => {
setEditMode(!editMode);
console.log(id);
};
const removeTodosHandler = () => {
if (window.confirm("Are you sure you want to delete everything?")) {
setTodos([]);
}
};
const removeTodoHandler = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const updateValue = (id) => {
inputRef.current.focus();
setEditMode(!editMode);
setTodos({ name: inputRef.current.value });
};
//Todo list states.
const inputRef = useRef("");
const [todos, setTodos] = useState([]);
const [editMode, setEditMode] = useState(false);
return (
<div>
<div>
{todos.map((todo) => {
return (
<div>
{editMode ? (
<div>
{" "}
<input
type="text"
defaultValue={todo.name}
ref={inputRef}
></input>
<button onClick={(e) => updateValue(todo.id)}>ok</button>
<button onClick={(e) => setEditMode(!editMode)}>x</button>
</div>
) : (
<div></div>
)}
<Todo name={todo.name} key={todo.id} />
<button onClick={() => removeTodoHandler(todo.id)}>X</button>
<button onClick={(e) => changeEditMode(todo.id)}>Edit</button>
</div>
);
})}
</div>
<AddTodo
handleAddTodo={addTodoHandler}
removeTodosHandler={removeTodosHandler}
revemoveTodoHandler={removeTodoHandler}
/>
</div>
);
};
export default TodoList;
and here's the Todo.js
const Todo = ({ name }) => {
return (
<div>
<div>{name}</div>
</div>
);
};
export default Todo;
Any help appreciated!
Your updateValue function is setting your todos to an object. So it gives you that error because you can't use map method for objects.
In your updateValue method you are setting your todoList to and object.
const updateValue = (id) => {
inputRef.current.focus();
setEditMode(!editMode);
setTodos({ name: inputRef.current.value });
};
But what you have to do is first find out the item with the id and then update the name property of that object and then again set the new array to setTodos setter.
Like this:
const clonedTodos = todos;
const todoIndex = clonedTodos.findIndex((todo) => todo.id === id);
const updatedTodo = {
...clonedTodos[todoIndex],
name: inputRef.current.value,
};
const updatedTodos = [...clonedTodos];
updatedTodos[todoIndex] = updatedTodo;
setTodos(updatedTodos);