Xây dựng ứng dụng To Do List với Javascript
Trong bài viết này mình sẽ xây dựng ứng dụng ToDoList bằng Javascript, thông qua cơ chế DOM để thao tác với các thẻ HTML.
Ở bài viết trước mình đã sử dụng React JS để xây dựng ứng dụng To Do List. Hôm này mình sẽ sử dụng Javascript thuần để xây dựng ứng dụng ToDoList. Dự định của mình là sẽ xây dựng một series bài viết về việc xây dựng ứng dụng To Do List bằng các công nghệ khác nhau chạy trên nền tảng khác nhau.
Ví dụ :
- Xây dựng ứng dụng di động todoList sử dụng Flutter, SQLite.
- Xẩy dựng ứng dụng web toDoList sử dụng ( Redux/useContext/Typescript/Vue )
- ...
ToDoList App là gì ?
ToDoList App là một ứng dụng quản lý danh sách các công việc cần được thưc hiện. Đây là ứng dụng đầu tiên mình làm khi mình học bất cứ 1 công nghệ nào mới như Flutter, React, Vue, Node,... Bài viết này mình sẽ xây dựng ứng dụng ToDoList bằng React JS.
Preview ToDoList App
Code HTML :
<div class="app">
<div class="header">
<h2>To Do List - React</h2>
<div class="input">
<input type="text" placeholder="Title..." />
<span class="addBtn">Add</span>
</div>
</div>
<div class="body">
<ul>
<li>
<span class="checkedIcon"></span>
<span class="label"> Learn English </span>
<span class="close"> × </span>
</li>
<li class="checked">
<span class="checkedIcon"></span>
<span class="label"> Run </span>
<span class="close"> × </span>
</li>
</ul>
</div>
</div>
Code CSS :
* {
box-sizing: border-box;
font-family: "Handlee", cursive;
}
html {
height: 100%;
}
body {
background-color: #f3f1f5;
}
.app {
height: 100vh;
width: 100%;
margin: auto;
padding: 1rem 0;
}
.header {
background-color: #22577a;
padding: 1rem 2rem;
color: white;
text-align: center;
font-size: 2rem;
height: 200px;
}
.header:after {
content: "";
display: table;
clear: both;
}
.header h2 {
margin: 1rem 0;
}
input {
margin: 0;
border: none !important;
border-radius: 0;
width: 75%;
padding: 10px;
float: left;
font-size: 16px;
}
input:focus {
outline: none;
}
.addBtn {
padding: 10px;
width: 25%;
background: #9d9d9d;
color: #fff;
font-weight: 600;
float: left;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: 0.3s;
border-radius: 0;
}
.addBtn:hover {
background-color: #c8c6c6;
}
.body {
height: calc(100% - 220px);
overflow-y: auto;
/*padding-right: 0.5rem;*/
margin: 10px 0;
}
.body::-webkit-scrollbar {
width: 0.5rem;
}
.body::-webkit-scrollbar-thumb {
background: #11324d;
border-radius: 0.5rem;
}
.body::-webkit-scrollbar-thumb:hover {
background: #6b7aa1;
}
ul {
margin: 0;
padding: 0;
}
ul li {
margin: 0.25rem;
cursor: pointer;
position: relative;
padding: 12px 0;
background: #eee;
font-size: 18px;
transition: 0.2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul li:hover .close {
display: flex;
}
ul li:nth-child(odd) {
background: #f9f9f9;
}
ul li:hover {
background: #ddd;
}
ul li.checked .checkedIcon {
content: "";
position: absolute;
border-color: #fff;
border-style: solid;
border-width: 0 2px 2px 0;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 15px;
width: 7px;
}
ul li.checked {
background-color: #7f7c82;
color: #fff;
}
ul li.checked .label {
text-decoration: line-through;
}
.label {
position: absolute;
left: 40px;
right: 40px;
}
.close {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
font-weight: 700;
font-size: 1.2rem;
align-items: center;
justify-content: center;
display: none;
}
@media only screen and (max-width: 800px) {
.app {
width: calc(100% - 2rem);
}
.header h2 {
font-size: 1.5rem;
}
}
@media only screen and (max-width: 1200px) and (min-width: 800px) {
.app {
width: 700px;
}
}
@media only screen and (min-width: 1200px) {
.app {
width: 900px;
}
}
Giao diện trên điện thoại :
Giao diện trên desktop :
Cấu trúc project :
- todolist-javascript
- index.html
- style.css
- script.js
Khai báo các constants :
const TO_DO_DONE = 'TO_DO_DONE';
const TO_DO_PROCESSING = 'TO_DO_PROCESSING';
const LABEL_TO_DO_LIST_STORAGE = 'dataToDoListJavascript';
Khởi tạo giá trị cho toDoList:
Ta sử dụng event onload được đặt trong thẻ body để thực hiện việc khởi tạo giá trị cho toDoList. Giá trị khởi tạo được lấy ở localStorage ( nơi lưu các toDoList mà bạn đã thêm trước đó ).
Kiến thức về event onload : https://www.w3schools.com/jsref/event_onload.asp
// khai bao event onload trong thẻ body
<body onload="onLoad()">
// function onload
const onLoad = () => {
try {
const dataToDoList = localStorage.getItem(LABEL_TO_DO_LIST_STORAGE);
const dataInitial = JSON.parse(dataToDoList);
toDoList = Array.isArray(dataInitial) ? dataInitial : [];
console.log(toDoList)
toDoList.forEach((toDoItem, index) => {
addLiEl(toDoItem);
})
} catch (e) {
}
}
Các hàm thực hiện việc thêm 1 toDo khi có sử kiện click button addToDo:
// html code
<span onclick="clickAddToDo()" id="addToDo" class="addBtn">Add</span>
// javascript code
const clickAddToDo = () => {
const toDoInput = document.getElementById('toDoInput');
const toDoText = toDoInput.value;
if (toDoText !== '') {
const dataToDo = addToDo(toDoText);
addLiEl(dataToDo);
toDoInput.value = '';
}
}
Hàm nhận biết sự kiện nhấn Enter để thực hiện việc thêm mới 1 toDo:
// html code
<input onkeydown="onkeydownInput(event)" id="toDoInput" type="text" placeholder="Title..." />
// javascript code
const onkeydownInput = (event) => {
if (event.key === 'Enter') {
clickAddToDo();
}
}
Hàm cập nhật dataToDoList lưu vào localStorage :
const updateDataLocalStorage = (toDoList) => {
localStorage.setItem(LABEL_TO_DO_LIST_STORAGE, JSON.stringify(toDoList))
}
Hàm thực hiện việc thêm mới 1 toDo vào toDoList và lưu vào localStorage:
const addToDo = (toDo) => {
const now = new Date();
const dataToDo = {
id: now.valueOf(),
label: toDo,
status: TO_DO_PROCESSING,
createdAt: now,
};
toDoList.push(dataToDo)
updateDataLocalStorage(toDoList);
return dataToDo;
}
Hàm thực hiện việc thêm mới 1 Li Element toDo :
const addLiEl = (dataToDo) => {
const id = dataToDo.id;
const toDoText = dataToDo.label;
const liEle = document.createElement('li')
liEle.setAttribute('id', id.toString());
if (dataToDo.status === TO_DO_DONE) {
liEle.setAttribute('class', 'checked');
}
liEle.setAttribute('onclick', `toggleStatusToDo(${id})`)
// span checkIcon
const spanChecked = document.createElement('span');
spanChecked.setAttribute('class', 'checkedIcon');
liEle.appendChild(spanChecked);
// span label
const spanLabel = document.createElement('span');
spanLabel.setAttribute('class', 'label');
spanLabel.innerText = toDoText;
liEle.appendChild(spanLabel);
// span label
const spanClose = document.createElement('span');
spanClose.setAttribute('class', 'close');
spanClose.setAttribute('onclick', `clickRemoveToDo(${id})`)
spanClose.innerText = '×';
liEle.appendChild(spanClose);
document.getElementById('toDoList').prepend(liEle);
}
Hàm xử lý việc xóa 1 toDo :
// close element khi tao moi 1 li element
const spanClose = document.createElement('span');
spanClose.setAttribute('class', 'close');
spanClose.setAttribute('onclick', `clickRemoveToDo(${id})`)
// javascript code
const clickRemoveToDo = (id) => {
const liEle = document.getElementById(id);
liEle.remove();
removeToDo(id);
}
const removeToDo = (id) => {
toDoList = toDoList.filter((toDoItem, index) => {
return toDoItem.id !== id;
})
updateDataLocalStorage(toDoList);
}
Hàm xử lý việc thay đổi trạng thái của 1 toDo :
// khi bao su kien vao li element
liEle.setAttribute('onclick', `toggleStatusToDo(${id})`)
// javascript code
const toggleStatusToDo = (id) => {
const toDoToggle = toDoList.find((toDoItem, index) => {
return toDoItem.id === id;
})
if (toDoToggle) {
const liEle = document.getElementById(id);
if (toDoToggle.status === TO_DO_DONE) {
liEle.classList.remove('checked');
} else {
liEle.classList.add('checked')
}
}
onChangeToDoStatus(id);
}
const onChangeToDoStatus = (id) => {
toDoList = toDoList.map((toDoItem, index) => {
if (toDoItem.id === id) {
return {
...toDoItem,
status: toDoItem.status === TO_DO_DONE ? TO_DO_PROCESSING : TO_DO_DONE
}
} else {
return toDoItem;
}
})
updateDataLocalStorage(toDoList);
}
Chi tiết các File trong Project
index.html :
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="icon" href="profile_circle.png" />
<link href="https://fonts.googleapis.com/css2?family=Handlee&display=swap" rel="stylesheet">
<title>To Do List App - Javascript | Tran Dinh Thang</title>
<link rel="stylesheet" href="style.css">
</head>
<body onload="onLoad()">
<div class="app">
<div class="header">
<h2>To Do List - Javascript</h2>
<div class="input">
<input onkeydown="onkeydownInput(event)" id="toDoInput" type="text" placeholder="Title..." />
<span onclick="clickAddToDo()" id="addToDo" class="addBtn">Add</span>
</div>
</div>
<div class="body">
<ul id="toDoList">
</ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
style.css :
* {
box-sizing: border-box;
font-family: "Handlee", cursive;
}
html {
height: 100%;
}
body {
background-color: #f3f1f5;
}
.app {
height: 100vh;
width: 100%;
margin: auto;
padding: 1rem 0;
}
.header {
background-color: #22577a;
padding: 1rem 2rem;
color: white;
text-align: center;
font-size: 2rem;
height: 200px;
}
.header:after {
content: "";
display: table;
clear: both;
}
.header h2 {
margin: 1rem 0;
}
input {
margin: 0;
border: none !important;
border-radius: 0;
width: 75%;
padding: 10px;
float: left;
font-size: 16px;
}
input:focus {
outline: none;
}
.addBtn {
padding: 10px;
width: 25%;
background: #9d9d9d;
color: #fff;
font-weight: 600;
float: left;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: 0.3s;
border-radius: 0;
}
.addBtn:hover {
background-color: #c8c6c6;
}
.body {
height: calc(100% - 220px);
overflow-y: auto;
/*padding-right: 0.5rem;*/
margin: 10px 0;
}
.body::-webkit-scrollbar {
width: 0.5rem;
}
.body::-webkit-scrollbar-thumb {
background: #11324d;
border-radius: 0.5rem;
}
.body::-webkit-scrollbar-thumb:hover {
background: #6b7aa1;
}
ul {
margin: 0;
padding: 0;
}
ul li {
margin: 0.25rem;
cursor: pointer;
position: relative;
padding: 12px 0;
background: #eee;
font-size: 18px;
transition: 0.2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul li:hover .close {
display: flex;
}
ul li:nth-child(odd) {
background: #f9f9f9;
}
ul li:hover {
background: #ddd;
}
ul li.checked .checkedIcon {
content: "";
position: absolute;
border-color: #fff;
border-style: solid;
border-width: 0 2px 2px 0;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 15px;
width: 7px;
}
ul li.checked {
background-color: #7f7c82;
color: #fff;
}
ul li.checked .label {
text-decoration: line-through;
}
.label {
position: absolute;
left: 40px;
right: 40px;
}
.close {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
font-weight: 700;
font-size: 1.2rem;
align-items: center;
justify-content: center;
display: none;
}
@media only screen and (max-width: 800px) {
.app {
width: calc(100% - 2rem);
}
.header h2 {
font-size: 1.5rem;
}
}
@media only screen and (max-width: 1200px) and (min-width: 800px) {
.app {
width: 700px;
}
}
@media only screen and (min-width: 1200px) {
.app {
width: 900px;
}
}
script.js :
const TO_DO_DONE = 'TO_DO_DONE';
const TO_DO_PROCESSING = 'TO_DO_PROCESSING';
const LABEL_TO_DO_LIST_STORAGE = 'dataToDoListJavascript';
let toDoList = [];
const onLoad = () => {
try {
const dataToDoList = localStorage.getItem(LABEL_TO_DO_LIST_STORAGE);
const dataInitial = JSON.parse(dataToDoList);
toDoList = Array.isArray(dataInitial) ? dataInitial : [];
console.log(toDoList)
toDoList.forEach((toDoItem, index) => {
addLiEl(toDoItem);
})
} catch (e) {
}
}
const onkeydownInput = (event) => {
if (event.key === 'Enter') {
clickAddToDo();
}
}
const clickAddToDo = () => {
const toDoInput = document.getElementById('toDoInput');
const toDoText = toDoInput.value;
if (toDoText !== '') {
const dataToDo = addToDo(toDoText);
addLiEl(dataToDo);
toDoInput.value = '';
}
}
const addLiEl = (dataToDo) => {
const id = dataToDo.id;
const toDoText = dataToDo.label;
const liEle = document.createElement('li')
liEle.setAttribute('id', id.toString());
if (dataToDo.status === TO_DO_DONE) {
liEle.setAttribute('class', 'checked');
}
liEle.setAttribute('onclick', `toggleStatusToDo(${id})`)
// span checkIcon
const spanChecked = document.createElement('span');
spanChecked.setAttribute('class', 'checkedIcon');
liEle.appendChild(spanChecked);
// span label
const spanLabel = document.createElement('span');
spanLabel.setAttribute('class', 'label');
spanLabel.innerText = toDoText;
liEle.appendChild(spanLabel);
// span label
const spanClose = document.createElement('span');
spanClose.setAttribute('class', 'close');
spanClose.setAttribute('onclick', `clickRemoveToDo(${id})`)
spanClose.innerText = '×';
liEle.appendChild(spanClose);
document.getElementById('toDoList').prepend(liEle);
}
const addToDo = (toDo) => {
const now = new Date();
const dataToDo = {
id: now.valueOf(),
label: toDo,
status: TO_DO_PROCESSING,
createdAt: now,
};
toDoList.push(dataToDo)
updateDataLocalStorage(toDoList);
return dataToDo;
}
const clickRemoveToDo = (id) => {
const liEle = document.getElementById(id);
liEle.remove();
removeToDo(id);
}
const removeToDo = (id) => {
toDoList = toDoList.filter((toDoItem, index) => {
return toDoItem.id !== id;
})
updateDataLocalStorage(toDoList);
}
const toggleStatusToDo = (id) => {
const toDoToggle = toDoList.find((toDoItem, index) => {
return toDoItem.id === id;
})
if (toDoToggle) {
const liEle = document.getElementById(id);
if (toDoToggle.status === TO_DO_DONE) {
liEle.classList.remove('checked');
} else {
liEle.classList.add('checked')
}
}
onChangeToDoStatus(id);
}
const onChangeToDoStatus = (id) => {
toDoList = toDoList.map((toDoItem, index) => {
if (toDoItem.id === id) {
return {
...toDoItem,
status: toDoItem.status === TO_DO_DONE ? TO_DO_PROCESSING : TO_DO_DONE
}
} else {
return toDoItem;
}
})
updateDataLocalStorage(toDoList);
}
const updateDataLocalStorage = (toDoList) => {
localStorage.setItem(LABEL_TO_DO_LIST_STORAGE, JSON.stringify(toDoList))
}
Source Code :
https://github.com/trandinhthangdev/todolist-javascript
Demo :
...