VanillaJS To Do UI
- GitHub: https://github.com/travisluong/fullstackbook-todo-vanillajs
- YouTube: Full Stack NestJS + VanillaJS Tutorial
- YouTube: Full Stack FastAPI + VanillaJS Tutorial
- YouTube: Full Stack Spring Boot + VanillaJS Tutorial
HTML
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Stack Book To Do</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script src="app.js"></script>
</head>
<body>
<div class="layout">
<h1 class="title">To Do</h1>
<div class="container">
<div class="mainInputContainer">
<input class="mainInput" placeholder="What needs to be done?" />
</div>
<div class="toDoContainer">
</div>
<div class="filters">
<button class="filterBtn all">All</button>
<button class="filterBtn active">Active</button>
<button class="filterBtn completed">Completed</button>
</div>
</div>
</div>
</body>
</html>
JavaScript
app.js
function FullStackBookToDo() {
const td = {}
td.API_URL = "http://localhost:8000";
td.elements = {}
this.init = async function () {
document.addEventListener("DOMContentLoaded", async function () {
td.getElements()
await td.fetchToDos()
await td.renderToDos()
td.bindEvents()
});
}
td.getElements = function () {
td.elements.toDoContainer = document.querySelector(".toDoContainer")
td.elements.addToDoBtn = document.querySelector(".addToDoBtn")
td.elements.deleteBtn = document.querySelector(".deleteBtn")
td.elements.checkboxes = document.querySelectorAll(".toDoCheckbox")
td.elements.mainInput = document.querySelector(".mainInput")
td.elements.allFilter = document.querySelector(".filterBtn.all")
td.elements.activeFilter = document.querySelector(".filterBtn.active")
td.elements.completedFilter = document.querySelector(".filterBtn.completed")
}
td.fetchToDos = async function (completed) {
let path = "todos"
if (completed !== undefined) {
path += `?completed=${completed}`
}
const todos = await fetch(`${td.API_URL}/${path}`).then((res) => res.json());
td.todos = todos
}
td.renderToDos = async function () {
td.elements.toDoContainer.innerHTML = "";
td.todos.forEach((todo) => {
const div = document.createElement("div")
div.className = "todoRow"
const checkbox = document.createElement("input")
checkbox.type = "checkbox"
checkbox.className = "toDoCheckbox"
checkbox.dataset.id = todo.id
checkbox.checked = todo.completed ? "checked" : ""
const input = document.createElement("input")
input.type = "text"
input.value = todo.name
input.className = "todoInput"
input.dataset.id = todo.id
const btn = document.createElement("button")
btn.className = "deleteBtn"
btn.dataset.id = todo.id
const img = document.createElement("img")
img.className = "deleteImg"
img.dataset.id = todo.id
img.src = "/material-symbols_delete-outline-sharp.svg"
btn.appendChild(img)
div.appendChild(checkbox)
div.appendChild(input)
div.appendChild(btn)
td.elements.toDoContainer.appendChild(div)
})
}
td.bindEvents = function () {
document.addEventListener("click", function (event) {
if (event.target.className === "toDoCheckbox") {
td.handleClick(event.target.dataset.id)
}
if (event.target.className === "deleteImg") {
td.handleDelete(event.target.dataset.id)
}
})
document.addEventListener("input", function (event) {
if (event.target.className === "toDoInput") {
td.handleChange(event.target.dataset.id, event.target.value)
}
})
td.elements.mainInput.addEventListener("keypress", function (event) {
if (event.key === "Enter") {
td.addToDo(event.target.value)
}
})
td.elements.allFilter.addEventListener("click", async function (event) {
await td.fetchToDos()
td.elements.allFilter.classList.add("filterActive")
td.elements.activeFilter.classList.remove("filterActive")
td.elements.completedFilter.classList.remove("filterActive")
td.renderToDos()
})
td.elements.activeFilter.addEventListener("click", async function (event) {
await td.fetchToDos(false)
td.elements.allFilter.classList.remove("filterActive")
td.elements.activeFilter.classList.add("filterActive")
td.elements.completedFilter.classList.remove("filterActive")
td.renderToDos()
})
td.elements.completedFilter.addEventListener("click", async function (event) {
await td.fetchToDos(true)
td.elements.allFilter.classList.remove("filterActive")
td.elements.activeFilter.classList.remove("filterActive")
td.elements.completedFilter.classList.add("filterActive")
td.renderToDos()
})
}
td.handleClick = async function (id) {
const idx = td.todos.findIndex((t) => t.id === parseInt(id))
td.todos[idx].completed = !td.todos[idx].completed
await fetch(`${td.API_URL}/todos/${id}`, {
method: "PUT",
body: JSON.stringify(td.todos[idx]),
headers: {
'Content-Type': 'application/json'
}
}).then((res) => res.json())
}
td.handleChange = _.debounce(async function (id, value) {
const idx = td.todos.findIndex((t) => t.id === parseInt(id))
td.todos[idx].name = value
await fetch(`${td.API_URL}/todos/${id}`, {
method: "PUT",
body: JSON.stringify(td.todos[idx]),
headers: {
'Content-Type': 'application/json'
}
}).then((res) => res.json())
}, 500)
td.addToDo = async function (name) {
const data = { name: name, completed: false }
const res = await fetch(`${td.API_URL}/todos`, {
method: "POST",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
const json = await res.json()
td.todos.push(json)
td.renderToDos()
td.elements.mainInput.value = ""
}
td.handleDelete = async function (id) {
await fetch(`${td.API_URL}/todos/${id}`, {
method: "DELETE"
})
const idx = td.todos.findIndex((t) => t.id === parseInt(id))
td.todos.splice(idx, 1)
td.renderToDos()
}
}
const app = new FullStackBookToDo()
app.init()
CSS
style.css
.layout {
width: 300px;
margin: 20px;
}
.container {
width: 300px;
border: 1px solid black;
}
.title {
text-align: center;
font-size: 24px;
margin: 10px;
font-family: Arial, Helvetica, sans-serif;
}
.mainInputContainer {
width: 100%;
margin: 20px 0;
}
.mainInput {
padding: 5px;
border: 1px solid black;
margin: auto;
display: block;
width: 260px;
height: 40px;
}
.filters {
display: flex;
justify-content: space-between;
padding: 20px;
margin-top: 20px;
border-top: 1px solid black;
}
.filterBtn {
background: none;
border: none;
cursor: pointer;
}
.filterActive {
text-decoration: underline;
}
.todoInput {
padding: 5px;
border: 1px solid black;
width: 194px;
height: 40px;
margin: 5px;
}
.todoRow {
display: flex;
flex-direction: row;
align-items: center;
margin: 5px 20px;
}
.deleteBtn {
background: none;
border: 0;
cursor: pointer;
}