Completing Tasks with Checkbox inputs

In this demo we are showing the ability to complete tasks. To accomplish this, we'll use a checkbox input. First, we create a Checkbox as a styled.input. Checkboxes are inputs with attribute type="checkbox", so we'll render this <Checkbox/> as self-closing tag as the first child in the TaskRow.

We want to add space for this Checkbox in our grid, so we'll adjust our grid-template-columns to be 40px 1fr 100px, leaving 40px for the Checkbox to live in the first column. This doesn't look perfect by default, so we'll have to tweak some other styles: adding a height: 20px to the Checkbox, and adding some padding: 5px to the TaskName will help align the content in the row. Normally, you could just align-items: center inside the grid, but this has some odd behavior with the Checkbox resizing for that style.

Once the checkbox is looking good in the row, we'll now add stateful functionality to it. The attribute in the task we'll use is complete to determine whether or not the checkbox is checked or not. We'll start by modifying our tasks.js data to add a complete: true attribute to the first task. Now, for our checkbox to be checked, we can set the checked attribute on Checkbox to be equal to task.complete. We can simplify this further by destructuring {complete} off of task below the first two set state calls.

When the checkbox is clicked, we call a new function toggleComplete, which will call onSave, spreading the task attributes and adding a new attribute complete set to the event's e.target.checked property. This is all we need to get this working.

Lifing checkbox state up

You might be wondering why we don't have local setState for the checkbox. The reason is because we are directly "lifting" the state of the Checkbox "above" to be in the task object managed in App.js. While it makes sense to have local task state editing and name, because these are not persisted to the task object and just represent the local editing of the task until it saved, checked is an attribute that should directly trigger a "save" on the parent task object, so it does not (and should not if we are writing clean code) have local state managed here.

Completed task row dynamic styling

You can also see that in the TaskRow, we've added an odd syntax ${({complete}) =>. As a reminder, in styled components we are in a template literal (wrapped in backticks ""), and so we can start writing a javascript expression inside styled-components to create dynamic style. Here, we are passing the completeprop into the styled component, so we are accessing it here in thestyled` block. If it is complete, set an opacity of 0.3 on the whole task row. This gives the visual that the complete task is faded out.

Your next mission

Next time we will add a keyboard shortcut so users can save an edit by hitting the Enter key.

Menu
Lesson 20/20
import { useState } from "react";
import styled from "styled-components";
import Button, { IconButton } from "./Button";

const TaskName = styled.div`
  padding: 5px; 
  font-size: 18px; 
`;

const TaskInput = styled.input``;

const TaskRow = styled.div`
  display: grid; 
  // Checkbox lives in 40px first column
  grid-template-columns: 40px 1fr 100px; 
  gap: 5px; 
  margin: 10px 0px; 
  width: 100%; 
  // Dynamic styling completed row
  ${({ complete }) => complete && "opacity: 0.3;"}
`;

const RowActions = styled.div`
  display: grid; 
  grid-template-columns: 1fr 1fr; 
  gap: 4px; 
`;

// Adding a new Checkbox input
const Checkbox = styled.input`
  cursor: pointer; 
  height: 20px; 
`;

function Task({ task, onSave, onDestroy }) {
  const [editing, setEditing] = useState(task.isNew);
  const [name, setName] = useState(task.name);
  // Destructing the complete attribute off the task
  const { complete } = task;

  function saveEdit() {
    onSave({
      ...task,
      isNew: false,
      name,
    });
    setEditing(!editing);
  }

  function cancelEdit() {
    if (task.isNew) {
      onDestroy(task.id);
      return;
    }
    setName(task.name);
    setEditing(false);
  }

  function editName({ target: { value } }) {
    setName(value);
  }

  function toggleComplete(e) {
    // Lifting state of complete up to parent task
    onSave({
      ...task,
      complete: e.target.checked,
    });
  }

  return (
    <TaskRow complete={complete}>
      <Checkbox checked={complete} type="checkbox" onChange={toggleComplete} />
      {editing ? (
        <TaskInput placeholder={name} autoFocus onChange={editName} />
      ) : (
        <TaskName> {name} </TaskName>
      )}
      <RowActions>
        {editing ? (
          <>
            <Button onClick={saveEdit}> Save </Button>
            <Button onClick={cancelEdit}> Cancel </Button>
          </>
        ) : (
          <>
            <IconButton onClick={() => setEditing(true)}> ✏️ </IconButton>
            <IconButton onClick={() => onDestroy(task.id)}> 🗑️ </IconButton>
          </>
        )}
      </RowActions>
    </TaskRow>
  );
}

export default Task;