1

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>
    )
}

3 Answers 3

2

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 >
  )
}

Edit push-values-in-to-new-array-only-when-material-ui-checkbox-is-checked

Sign up to request clarification or add additional context in comments.

9 Comments

Thank you for your help, it gave me a lot of insight into new solutions and gave me another change to rethink about my code. I managed to solve the issue by pushing the completed todos to a new array. I just thought I would mention that when I was exploring your code I wasn't able to get the number of completed tasks to update correctly and I wasn't sure why this was occuring.
@sol_s When this happens it's usually missing the fact that the handlers are curried/higher order functions. I wasn't able to test this code but I'm fairly confident it is close to correct/working as I suspect it should be. What wasn't working for you? I don't mind going through the code with you. Regarding the other solution, I didn't like it since it doesn't seem to account for unchecking a completed todo, i.e. to undo it if maybe it's not actually complete. It looks like it would uncheck the checkbox and add the key to the completed array again.
@sol_s Handlers == the callback functions passed as props. Higher Order Functions. I guess these are technically just curried functions which are basically functions that return functions. In my answer const handleChecked = index => e => {....} is a function that takes an index argument and returns a function that takes an event object. It allows attaching a handler like onClick={handleChecked(i)} versus onClick={() => handleChecked(i)}, which saves an anonymous callback function.
Given that checked is an array of boolean values (const [checked, setChecked] = useState(taskValue.map(() => false));), then checked.filter(Boolean).length should filter the array for truthy elements, and then return the length of the array., i.e. [true, false, false].filter(Boolean).length should return 1 (try this in the browser console).
"could this be resolveable by checking the index of the todo that has been added to the new array?" I suspect not since the index in the new array has no correlation to the index of the array being mapped. You would need use objects and GUIDs or maintain a checked array that is the same length as the data (so the indices can match).
|
1

To do this, you first need to only push the checked key to your array:


  • First send your key to the eventHandler:
{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>
                    )
                })}
  • Then in your Handler, push it into the array:
    const handleChecked = (key) => {
        //setChecked(e.target.checked)
        completedTasks.push(key)
        console.log(completedTasks.length)
    }
  • BUT

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

Comments

0

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.

1 Comment

Careful, completedTasks.push(key) is mutating the completedTasks state. It's overwritten by the setCompletedTasks([...completedTasks, key]) state update you enqueued a line above.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.