Reputation: 115
I am new to react and I am making a simple todo app using react js and material ui. I have separate components to take in user input (TodoInput.js) which sends props to a component that renders individual todo tasks and displays a checkbox (TodoCards.js). What I want to do is display the total number of completed tasks onto the page which is updated when the user completes a todo by checking a checkbox. To achieve this, I have an array that stores all the user's completed tasks. At the moment whenever a checkbox is checked, all tasks are added to this array. I ran into a problem where I am unsure of how to only push values into this new array when the checkbox of that specific task is checked. Any guidance or explanations towards the right direction is greatly appreciated.
TodoInput.js
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { TextField, Button } from '@material-ui/core';
import { TodoCards } from '../UI/TodoCards';
import { Progress } from '../UI/Progress';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
width: '25ch',
textAlign: 'center'
},
},
}));
export default function TodoInput() {
const classes = useStyles();
const [userInput, setUserInput] = useState({
id: '',
task: ''
});
const [todos, setTodos] = useState([])
//state for error
const [error, setError] = useState({
errorMessage: '',
error: false
})
//add the user todo with the button
const submitUserInput = (e) => {
e.preventDefault();
//add the user input to array
//task is undefined
if (userInput.task === "") {
//render visual warning for text input
setError({ errorMessage: 'Cannot be blank', error: true })
console.log('null')
} else {
setTodos([...todos, userInput])
console.log(todos)
setError({ errorMessage: '', error: false })
}
console.log(loadedTodos)
}
//set the todo card to the user input
const handleUserInput = function (e) {
//make a new todo object
setUserInput({
...userInput,
id: Math.random() * 100,
task: e.target.value
})
//setUserInput(e.target.value)
//console.log(userInput)
}
const loadedTodos = [];
for (const key in todos) {
loadedTodos.push({
id: Math.random() * 100,
taskName: todos[key].task
})
}
return (
<div>
<Progress taskCount={loadedTodos.length} />
<form className={classes.root} noValidate autoComplete="off" onSubmit={submitUserInput}>
{error.error ? <TextField id="outlined-error-helper-text" label="Today's task" variant="outlined" type="text" onChange={handleUserInput} error={error.error} helperText={error.errorMessage} />
: <TextField id="outlined-basic" label="Today's task" variant="outlined" type="text" onChange={handleUserInput} />}
<Button variant="contained" color="primary" type="submit">Submit</Button>
{userInput && <TodoCards taskValue={todos} />}
</form>
</div>
);
}
TodoCards.js
import React, { useState } from 'react'
import { Card, CardContent, Typography, FormControlLabel, Checkbox } from '@material-ui/core';
import { CompletedTasks } from './CompletedTasks';
export const TodoCards = ({ taskValue }) => {
const [checked, setChecked] = useState(false);
//if checked, add the task value to the completed task array
const completedTasks = [];
const handleChecked = (e) => {
setChecked(e.target.checked)
for (const key in taskValue) {
completedTasks.push(taskValue[key])
}
console.log(completedTasks.length)
}
return (
< div >
<CompletedTasks completed={completedTasks.length} />
<Card>
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={handleChecked}
/>
}
label={individual.task} />
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
CompletedTasks.js (displays the total number of completed tasks)
import React from 'react'
import InsertEmoticonOutlinedIcon from '@material-ui/icons/InsertEmoticonOutlined';
import { Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing(2),
marginTop: '20px',
textAlign: 'center',
color: theme.palette.text.secondary,
},
}));
export const CompletedTasks = ({ completed }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<InsertEmoticonOutlinedIcon fontSize="large" />
<Typography variant="h6">
Completed tasks:{completed}
</Typography>
</div>
)
}
Upvotes: 1
Views: 3504
Reputation: 203466
One issue I see here is that you start with a boolean type checked
state in TodoCards
, and only ever store a single boolean value of the last checkbox interacted with. There's no way to get a count or to track what's previously been checked.
Use an object to hold the completed "done" checked values, then count the number of values that are checked (i.e. true
) after each state update and rerender. Use the task's id
as the key in the checked
state.
export const TodoCards = ({ taskValue = [] }) => {
const [checked, setChecked] = useState({});
const handleChecked = id => e => {
const { checked } = e.target;
setChecked((values) => ({
...values,
[id]: checked
}));
};
return (
<div>
<CompletedTasks
completed={Object.values(checked).filter(Boolean).length}
/>
<Card>
{taskValue.map(({ id, task }) => {
return (
<CardContent key={id}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[id]}
onClick={handleChecked(id)}
/>
}
label={task}
/>
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
Upvotes: 2
Reputation: 115
In the hopes that someone else may find this useful, I was able to come up with a solution thanks to the suggestions given. Below is the updated code for the TodoCards.js component:
import React, { useState } from 'react'
import { Card, CardContent, Typography, FormControlLabel, Checkbox } from '@material-ui/core';
import { CompletedTasks } from './CompletedTasks';
export const TodoCards = ({ taskValue }) => {
const [checked, setChecked] = useState(false);
//if checked, add the task value to the completed task array
const [completedTasks, setCompletedTasks] = useState([]);
const handleChecked = key => {
setCompletedTasks([...completedTasks, key])
completedTasks.push(key)
console.log(completedTasks.length)
setChecked(true)
};
if (taskValue.length === completedTasks.length) {
console.log('all tasks complete')
}
return (
<div>
<CompletedTasks completed={completedTasks.length} />
<Card>
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={() => handleChecked(individual)}
/>
}
label={individual.task}
/>
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
Only the checked todo items are pushed into the new array (completedTasks) and this is updated using useState.
Upvotes: 0
Reputation: 8422
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={() => handleChecked(individual)}
/>
}
label={individual.task} />
</Typography>
</CardContent>
)
})}
const handleChecked = (key) => {
//setChecked(e.target.checked)
completedTasks.push(key)
console.log(completedTasks.length)
}
Because you are not modifying any state so the changes won't be updated to the UI, you need to use a state to store your completedTasks
.
const [completedTasks, setCompletedTasks] = useState([]);
const handleChecked = (key) => {
setCompletedTasks([...completedTasks, key])
}
Please note that this is only a guide so you can get to the right way, not a complete working example
Upvotes: 1