By the end of this lesson, lesson 20's to-do list is a web page. In it you can:
add a task with a form,
click a task to mark it done,
show only the open tasks,
see how many are left.
The class that keeps the tasks is the one you wrote in lesson 20.
Step 2 of 17 · Concept
Four files, one of them old
file
its job
index.html
the form, the filter buttons, the list
style.css
the look: done tasks are grey
script.js
the state, render() and the handlers
tasks.js
Task and TaskList, from lesson 20, unchanged
You write only script.js, in four steps.
Step 3 of 17 · Concept
The same class runs in the page
import{ Task, TaskList }from"./tasks.js";
script.js imports TaskList as lesson 8's modules do: the ./ and the .js are needed in a page too.
tasks.js knows nothing of files or pages. It only keeps tasks in an array and changes them. So it runs the same
in Node and in the browser.
Step 4 of 17 · Quiz
What must change in tasks.js?
Lesson 20's tool had four modules. What must change in tasks.js to run it in a page?
Two tries
Step 5 of 17 · Demo
Run the starter
Run it. The page has its form and buttons, but the list is empty: render() is still to write. Output shows
Loaded 3 tasks, the three tasks script.js makes.
Step 6 of 17 · Concept
One TaskList, one filter
The page's state is two variables:
list, a TaskList of three tasks, one done;
filter, "all" or "open": which tasks to show.
Every handler changes them, then calls render().
Step 7 of 17 · Trace
Watch one task arrive
Someone typed Water plants and pressed Add. This is the submit handler you will write. Press Next.
The submit event put the handler in the callback queue. The stack is empty, so it runs next.
The handler's frame is on the stack. Line 2 kept the page from loading again.
add's frame is on top. It made a Task and pushed it: list.tasks has 4, the page still shows 3.
add returned, and line 4 emptied the field. Line 5 is next.
Line 5 called render(): its frame is on top. The page still shows 3 tasks.
render() rebuilt the list: four li, Water plants the last.
render() returned. The handler's frame ends at line 6.
The handler returned, and the stack is empty. The page shows four tasks.
Step 8 of 17 · Exercise
Step 1: render the list
Write render(). It empties #tasks, then adds one li per task of list.tasks, its text the title. A done task's
li gets the class done, so it is grey.
render() will run again after every change. Empty the list first, with tasks.replaceChildren(), or each run adds the tasks once more.
Lesson 22's render(): replaceChildren(), then a for...of over list.tasks that makes an li, sets its textContent and appends it. An if on task.done adds the class.
for(const task of list.tasks){
Step 9 of 17 · Exercise
Step 2: add from the form
Write the submit handler, as the trace showed it:
Keep the page from loading again.
Add the field's text, trimmed, to list.
Empty the field, then call render().
list.add(text) is lesson 20's: it makes the Task and pushes it. The page changes only when render() runs.
event.preventDefault(); first. Then list.add(input.value.trim());, input.value = ""; and render();.
list.add(input.value.trim());
Step 10 of 17 · Quiz
Which task does it mark?
The filter shows the open tasks: shown is list.open(), Write report then Call Bo. Each button gets a
handler in render()'s loop. Which task does a click on the second button mark done?
Change render(): a done task stays text, and an open task's li holds a button with its title. A click on it
sets its own task.done to true. Then it calls render().
Lesson 20's done(n) takes a number. A number changes with the filter; the handler can keep the task itself instead, as the quiz showed.
Move li.textContent = task.title; into an if (task.done): an open task's text is its button's. In the else: document.createElement("button"), its textContent, a click handler, then li.append(button).
button.addEventListener("click",()=>{
Step 12 of 17 · Exercise
Step 4: a filter
Make All and Open work:
A click on allButton sets filter to "all" and renders; openButton sets "open".
In render(), loop over list.open() when filter is "open", else over list.tasks.
The list is rebuilt with the open tasks only. The check then clicks the first one, as in the quiz.
render() decides which tasks to show each time it runs, from filter. The click handlers only change filter, then call render().
In render(), let shown = list.tasks;, then an if on filter === "open" that makes it list.open(); loop over shown. Two click handlers, one per button.
let shown = list.tasks;
Step 13 of 17 · Concept
Log the state while you click
When a page does not do what you expect, log the state in a handler:
console.log("open:", list.open().length);
Each line shows in Output as you click in the Page tab. Log numbers and text, such as list.open().length or
task.title: an element logs only as a short tag, such as <li.done>.
Step 14 of 17 · Check yourself
Check yourself: a count
Make #count say how many tasks are open, of all of them, after every change. The check adds Water plants, marks
Write report done and shows the open tasks. Then it must read:
2 open of 4
Every change ends in render(), so one line there, after the loop, keeps the count right.
Step 15 of 17 · Challenge · optional
Challenge: nothing to do
Optional.
This index.html has <p id="empty">Nothing to do</p>, and it always shows. Show it only while no task is
open. The check marks every task done, then adds one more.
An element's hidden property keeps it from showing while it is true, as the attribute hidden does in HTML. You can set it: empty.hidden = true;.
Find #empty once, at the top. In render(), set empty.hidden from a comparison: it is hidden while more than 0 tasks are open.
empty.hidden = list.open().length >0;
Step 16 of 17 · Concept
Carry on in the Playground
You can now write JavaScript for the console and for a web page. One class runs in both.
The Playground runs Node programs, as lessons 1–20 did. Build a tool of your own there.
The TypeScript course, coming later, adds types. They catch some of lesson 5's silent mistakes before a run.
Ideas for this page: a Remove button per task, the tags after each title, a filter per tag.
script.js imports lesson 20's tasks.js unchanged: it knows nothing of files or pages.
The state is a TaskList and a filter; every handler changes it, then calls render().
A handler made in render()'s loop keeps its own task, whatever the filter shows.
Log numbers and text in a handler to see the state in Output.
Next: the Playground, then the TypeScript course.
Review
2 questions from this lesson, in a new order each time. Answer from memory: 2 of 2 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
// step 1: an li per task, class done if done
}
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
// step 1: an li per task, class done if done
}
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
// step 1: an li per task, class done if done
}
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
// step 1: an li per task, class done if done
}
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
li.textContent = task.title;
if (task.done) {
li.classList.add("done");
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
// step 2: no reload, add the trimmed title,
// empty the field, then render
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
li.textContent = task.title;
if (task.done) {
li.classList.add("done");
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
// step 2: no reload, add the trimmed title,
// empty the field, then render
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
li.textContent = task.title;
if (task.done) {
li.classList.add("done");
}
// step 3: an open task's title in a button,
// whose click marks that task done
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
li.textContent = task.title;
if (task.done) {
li.classList.add("done");
}
// step 3: an open task's title in a button,
// whose click marks that task done
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
// step 4: allButton and openButton set filter,
// then render
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
for (const task of list.tasks) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
// step 4: allButton and openButton set filter,
// then render
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
let shown = list.tasks;
if (filter === "open") {
shown = list.open();
}
for (const task of shown) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
allButton.addEventListener("click", () => {
filter = "all";
render();
});
openButton.addEventListener("click", () => {
filter = "open";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty" hidden>Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
function render() {
tasks.replaceChildren();
let shown = list.tasks;
if (filter === "open") {
shown = list.open();
}
for (const task of shown) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
allButton.addEventListener("click", () => {
filter = "all";
render();
});
openButton.addEventListener("click", () => {
filter = "open";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
const count = document.querySelector("#count");
function render() {
tasks.replaceChildren();
let shown = list.tasks;
if (filter === "open") {
shown = list.open();
}
for (const task of shown) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
count.textContent =
`${list.open().length} open of ${list.tasks.length}`;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
allButton.addEventListener("click", () => {
filter = "all";
render();
});
openButton.addEventListener("click", () => {
filter = "open";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To do</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>To do</h1>
<form id="add-form">
<label>New task
<input id="title" placeholder="Water plants">
</label>
<button>Add</button>
</form>
<p>Click an open task to mark it done.</p>
<button id="all">All</button>
<button id="open">Open</button>
<ul id="tasks"></ul>
<p id="count"></p>
<p id="empty">Nothing to do</p>
<script type="module" src="script.js"></script>
</body>
</html>
.done {
color: gray;
}
#tasks button {
margin: 4px;
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { Task, TaskList } from "./tasks.js";
const list = new TaskList([
new Task("Buy milk", ["home"], true),
new Task("Write report", ["work"]),
new Task("Call Bo", ["phone"]),
]);
let filter = "all";
const form = document.querySelector("#add-form");
const input = document.querySelector("#title");
const tasks = document.querySelector("#tasks");
const allButton = document.querySelector("#all");
const openButton = document.querySelector("#open");
const count = document.querySelector("#count");
function render() {
tasks.replaceChildren();
let shown = list.tasks;
if (filter === "open") {
shown = list.open();
}
for (const task of shown) {
const li = document.createElement("li");
if (task.done) {
li.textContent = task.title;
li.classList.add("done");
} else {
const button = document.createElement("button");
button.textContent = task.title;
button.addEventListener("click", () => {
task.done = true;
render();
});
li.append(button);
}
tasks.append(li);
}
count.textContent =
`${list.open().length} open of ${list.tasks.length}`;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
list.add(input.value.trim());
input.value = "";
render();
});
allButton.addEventListener("click", () => {
filter = "all";
render();
});
openButton.addEventListener("click", () => {
filter = "open";
render();
});
console.log(`Loaded ${list.tasks.length} tasks`);
render();