TodoMVC In Javascript Dom Scripting
I created this simple implementation based on dom programming for guiding a friend started with web development.
This is actually just a way of different implementations. It is written in pure js, and data driven rendering. Very fast forward way of coding.
code sctructure like this:
// data (runtime and reactive)
const state = {}
// set data and trigger render()
function setState() {}
// rendering
function render() {
renderA();
addEventsToA();
renderB();
addEventsToB();
// ...
addEventsToGlobal();
}
// first time of render()
render();
So the final implementation is:
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked>
<label>Taste JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web">
</li>
</ul>
</section>
<!-- This footer should be hidden by default and shown when there are todos -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count"><strong>0</strong> item left</span>
<!-- Remove this if you don't implement routing -->
<ul class="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">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script>
<script src="js/app.js"></script>
</body>
</html>
js/app.js
(function (window) {
"use strict";
// Your starting point. Enjoy the ride!
const state = {
todos: JSON.parse(localStorage.getItem("todomvc.todos")) || [], // [{id, title, completed}]
filters: [],
currentFilter: location.hash || "#/",
get isAllCompleted() {
return !this.todos.find((todo) => !todo.completed);
},
get activeCount() {
return this.todos.filter((todo) => !todo.completed).length;
},
get hasCompleted() {
return !!this.todos.find((todo) => todo.completed);
},
};
function setState(newState) {
Object.assign(state, newState);
afterStateChange(state, newState);
renderPage();
}
const app = {
newInput: document.querySelector(".new-todo"),
toggleAllBtn: document.querySelector(".toggle-all"),
todoList: document.querySelector(".todo-list"),
todoItemTemplate: document.querySelector(".todo-list li").cloneNode(true),
filterItems: document.querySelectorAll(".filters a"),
todoCount: document.querySelector(".todo-count strong"),
clearCompletedBtn: document.querySelector(".clear-completed"),
};
function afterStateChange(state, newState) {
if (newState.hasOwnProperty("todos")) {
localStorage.setItem("todomvc.todos", JSON.stringify(state.todos));
}
}
function renderPage() {
// render toggle all
app.toggleAllBtn.checked = state.isAllCompleted;
app.toggleAllBtn.addEventListener("change", (e) => {
setState({
todos: state.todos.map((todo) => {
todo.completed = e.target.checked;
return todo;
}),
});
});
// render new todo
app.newInput.addEventListener("keypress", (e) => {
if (e.key === "Enter" && e.target.value.trim()) {
state.todos.push({
id: Date.now(),
title: e.target.value,
completed: false,
});
setState({ todos: [...state.todos] });
app.newInput.value = "";
}
});
// render todo list
app.todoList.innerHTML = "";
state.todos
.filter((todo) => {
if (state.currentFilter === "#/") {
return true;
} else if (state.currentFilter === "#/active") {
return !todo.completed;
} else if (state.currentFilter === "#/completed") {
return todo.completed;
} else {
return true;
}
})
.forEach((todo, index) => {
const li = app.todoItemTemplate.cloneNode(true);
li.classList.toggle("completed", todo.completed);
li.querySelector(".toggle").checked = todo.completed;
li.querySelector("label").innerText = todo.title;
li.querySelector("label").addEventListener("dblclick", (e) => {
li.classList.toggle("editing", true);
li.querySelector(".edit").value = todo.title;
li.querySelector(".edit").focus();
});
li.querySelector(".edit").value = todo.title;
li.querySelector(".edit").addEventListener("blur", (e) => {
e.preventDefault();
todo.title = e.target.value;
setState({ todos: [...state.todos] });
});
li.querySelector(".edit").addEventListener("keypress", (e) => {
if (e.key === "Enter") {
li.classList.toggle("editing", false);
}
});
li.querySelector(".toggle").addEventListener("change", (e) => {
todo.completed = e.target.checked;
setState({ todos: [...state.todos] });
});
li.querySelector(".destroy").addEventListener("click", () => {
state.todos.splice(index, 1);
setState({ todos: [...state.todos] });
});
app.todoList.appendChild(li);
});
// render counts
app.todoCount.innerText = state.activeCount;
// render filters
app.filterItems.forEach((anchor) => {
const url = new URL(anchor.href);
anchor.classList.toggle("selected", url.hash === state.currentFilter);
});
// render clear completed
app.clearCompletedBtn.hidden = !state.hasCompleted;
app.clearCompletedBtn.addEventListener("click", (e) => {
e.preventDefault();
setState({ todos: state.todos.filter((todo) => !todo.completed) });
});
// global hash change
window.addEventListener("hashchange", (e) => {
const url = new URL(e.newURL);
setState({ currentFilter: url.hash });
});
}
renderPage();
})(window);