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 the
styled` 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.
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;