Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 27 additions & 66 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,69 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Template • TodoMVC</title>
<!-- TodoMVC Boilerplate CSS files -->
<link rel="stylesheet" href="css/index.css" />
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
placeholder="What needs to be done?"
autofocus
class="new-todo"
data-todo="new"
/>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<section class="main" data-todo="main">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
data-todo="toggle-all"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list" data-todo="list"></ul>
</section>
<!-- This footer should be hidden by default and shown when there are todos -->
<footer class="footer" data-todo="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count" data-todo="count"
><strong>0</strong> items left</span
>
<!-- Remove this if you don't implement routing -->
<ul class="filters" data-todo="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed" data-todo="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://twitter.com/1Marc">Marc Grabanski</a></p>
<p>
Project on GitHub:
<a href="https://github.com/1Marc/todomvc-vanillajs-2022"
>Vanilla JS TodoMVC 2022</a
>
</p>
</footer>
<!-- Scripts here. Don't remove ↓ -->
<script type="module" src="js/app.js"></script>
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Template • TodoMVC</title>
<!-- TodoMVC Boilerplate CSS files -->
<link rel="stylesheet" href="css/index.css" />
<script type="importmap">
{
"imports": {
"lit-html": "./node_modules/lit-html/lit-html.js"
}
}
</script>
</head>
<body>
<section class="todoapp"></section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://twitter.com/1Marc">Marc Grabanski</a></p>
<p>
Project on GitHub:
<a href="https://github.com/1Marc/todomvc-vanillajs-2022">Vanilla JS TodoMVC 2022</a>
</p>
</footer>
<!-- Scripts here. Don't remove ↓ -->
<script type="module" src="js/app.js"></script>
</body>
</html>
255 changes: 138 additions & 117 deletions js/app.js
Original file line number Diff line number Diff line change
@@ -1,125 +1,146 @@
import { delegate, getURLHash, insertHTML, replaceHTML } from "./helpers.js";
import { render as litRender, html, nothing } from "lit-html";
import { getURLHash } from "./helpers.js";
import { TodoStore } from "./store.js";

const Todos = new TodoStore("todo-modern-vanillajs");

const App = {
$: {
input: document.querySelector('[data-todo="new"]'),
toggleAll: document.querySelector('[data-todo="toggle-all"]'),
clear: document.querySelector('[data-todo="clear-completed"]'),
list: document.querySelector('[data-todo="list"]'),
showMain(show) {
document.querySelector('[data-todo="main"]').style.display = show ? "block" : "none";
},
showFooter(show) {
document.querySelector('[data-todo="footer"]').style.display = show ? "block" : "none";
},
showClear(show) {
App.$.clear.style.display = show ? "block" : "none";
},
setActiveFilter(filter) {
document.querySelectorAll(`[data-todo="filters"] a`).forEach((el) => {
if (el.matches(`[href="#/${filter}"]`)) {
el.classList.add("selected");
} else {
el.classList.remove("selected");
}
});
},
displayCount(count) {
replaceHTML(
document.querySelector('[data-todo="count"]'),
`
<strong>${count}</strong>
${count === 1 ? "item" : "items"} left
`
);
},
},
init() {
Todos.addEventListener("save", App.render);
App.filter = getURLHash();
window.addEventListener("hashchange", () => {
App.filter = getURLHash();
App.render();
});
App.$.input.addEventListener("keyup", (e) => {
if (e.key === "Enter" && e.target.value.length) {
Todos.add({
title: e.target.value,
completed: false,
id: "id_" + Date.now(),
});
App.$.input.value = "";
}
});
App.$.toggleAll.addEventListener("click", (e) => {
Todos.toggleAll();
});
App.$.clear.addEventListener("click", (e) => {
Todos.clearCompleted();
});
App.bindTodoEvents();
App.render();
},
todoEvent(event, selector, handler) {
delegate(App.$.list, selector, event, (e) => {
let $el = e.target.closest("[data-id]");
handler(Todos.get($el.dataset.id), $el, e);
});
},
bindTodoEvents() {
App.todoEvent("click", '[data-todo="destroy"]', (todo) => Todos.remove(todo));
App.todoEvent("click", '[data-todo="toggle"]', (todo) => Todos.toggle(todo));
App.todoEvent("dblclick", '[data-todo="label"]', (_, $li) => {
$li.classList.add("editing");
$li.querySelector('[data-todo="edit"]').focus();
});
App.todoEvent("keyup", '[data-todo="edit"]', (todo, $li, e) => {
let $input = $li.querySelector('[data-todo="edit"]');
if (e.key === "Enter" && $input.value) Todos.update({ ...todo, title: $input.value });
if (e.key === "Escape") {
$input.value = todo.title;
App.render();
}
});
App.todoEvent("blur", '[data-todo="edit"]', (todo, $li, e) => {
const title = $li.querySelector('[data-todo="edit"]').value;
Todos.update({ ...todo, title });
});
},
createTodoItem(todo) {
const li = document.createElement("li");
li.dataset.id = todo.id;
if (todo.completed) {
li.classList.add("completed");
}
insertHTML(
li,
`
<div class="view">
<input data-todo="toggle" class="toggle" type="checkbox" ${todo.completed ? "checked" : ""}>
<label data-todo="label"></label>
<button class="destroy" data-todo="destroy"></button>
</div>
<input class="edit" data-todo="edit">
`
);
li.querySelector('[data-todo="label"]').textContent = todo.title;
li.querySelector('[data-todo="edit"]').value = todo.title;
return li;
},
render() {
const count = Todos.all().length;
App.$.setActiveFilter(App.filter);
App.$.list.replaceChildren(...Todos.all(App.filter).map((todo) => App.createTodoItem(todo)));
App.$.showMain(count);
App.$.showFooter(count);
App.$.showClear(Todos.hasCompleted());
App.$.toggleAll.checked = Todos.isAllCompleted();
App.$.displayCount(Todos.all("active").length);
},
editId: "", // used to show edit field for a todo

init() {
Todos.addEventListener("save", App.render);
App.filter = getURLHash();
window.addEventListener("hashchange", () => {
App.filter = getURLHash();
App.render();
});
App.render();
},

createTodoItem(todo) {
const classes = [];
if (todo.completed) {
classes.push("completed");
}
if (App.editId === todo.id) {
classes.push("editing");
}
return html` <li class="${classes.join(" ")}">
<div class="view">
<input
class="toggle"
type="checkbox"
.checked=${todo.completed}
@click="${() => Todos.toggle(todo)}"
/>
<label @dblclick="${() => App.editOn(todo)}">${todo.title}</label>
<button class="destroy" @click="${() => Todos.remove(todo)}"></button>
</div>
<input
class="edit"
value="${todo.title}"
@keyup="${(evt) => App.saveEdit(evt, todo)}"
@blur="${(evt) => App.saveEdit(evt, todo)}"
/>
</li>`;
},

saveEdit(e, todo) {
App.editId = "";
if ((e.type === "blur" || e.key === "Enter") && e.target.value.length) {
Todos.update({
id: todo.id,
title: e.target.value,
});
}
},

saveNew(e) {
if (e.key === "Enter" && e.target.value.length) {
Todos.add({
title: e.target.value,
completed: false,
id: "id_" + Date.now(),
});
e.target.value = "";
}
},

editOn(todo) {
App.editId = todo.id;
App.render();
},

displayCount() {
const count = Todos.all("active").length;
return count
? html`<span class="todo-count">
<strong>${count}</strong>
${count === 1 ? "item" : "items"} left
</span>`
: nothing;
},

clearButton() {
return Todos.hasCompleted()
? html` <button class="clear-completed" @click="${() => Todos.clearCompleted()}">
Clear completed
</button>`
: nothing;
},

list() {
return Todos.all().length
? html`<section class="main">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
.checked=${Todos.isAllCompleted()}
@click=${() => Todos.toggleAll()}
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
${Todos.all(App.filter).map((todo) => App.createTodoItem(todo))}
</ul>
</section>`
: nothing;
},

base() {
return html`
<header class="header">
<h1>todos</h1>
<input
placeholder="What needs to be done?"
autofocus
class="new-todo"
@keyup="${(evt) => App.saveNew(evt)}"
/>
</header>
${App.list()}
<footer class="footer">
${App.displayCount()}
<ul class="filters">
<li>
<a class="${App.filter === "" ? "selected" : ""}" href="#/">All</a>
</li>
<li>
<a class="${App.filter === "active" ? "selected" : ""}" href="#/active">Active</a>
</li>
<li>
<a class="${App.filter === "completed" ? "selected" : ""}" href="#/completed"
>Completed</a
>
</li>
</ul>
${App.clearButton()}
</footer>
`;
},
render() {
litRender(App.base(), document.querySelector(".todoapp"));
},
};

App.init();
Loading