Bài 5.4: Thực hành xây dựng ứng dụng Todo list

Bài 5.4: Thực hành xây dựng ứng dụng Todo list
Chào mừng bạn đến với bài thực hành cực kỳ quan trọng trong hành trình trở thành nhà phát triển Front-end! Hôm nay, chúng ta sẽ cùng nhau bắt tay vào xây dựng một ứng dụng web hoàn chỉnh: một Todo List đơn giản nhưng đầy đủ các chức năng cơ bản.
Dự án này sẽ giúp bạn củng cố và áp dụng tất cả các kiến thức về HTML, CSS và JavaScript mà chúng ta đã học một cách trực quan và sinh động nhất. Bạn sẽ thấy cách ba "người bạn" này kết hợp với nhau để tạo ra một ứng dụng tương tác thực sự.
Chúng ta sẽ đi qua từng bước: xây dựng cấu trúc bằng HTML, tạo kiểu dáng bằng CSS, và cuối cùng, thêm "sự sống" cho ứng dụng bằng JavaScript.
Bước 1: Xây dựng cấu trúc với HTML
Mọi ứng dụng web đều bắt đầu với cấu trúc sườn bằng HTML. Chúng ta cần các phần tử cơ bản để người dùng có thể nhập công việc, thêm công việc và hiển thị danh sách các công việc đã thêm.
Hãy tạo file index.html
với nội dung sau:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Todo List</title>
<link rel="stylesheet" href="style.css"> <!-- Liên kết với file CSS -->
</head>
<body>
<div class="todo-container">
<h1>Danh sách công việc của tôi</h1>
<div class="input-area">
<input type="text" id="todo-input" placeholder="Thêm công việc mới...">
<button id="add-button">Thêm</button>
</div>
<ul id="todo-list">
<!-- Các công việc sẽ được thêm vào đây bằng JavaScript -->
</ul>
</div>
<script src="script.js"></script> <!-- Liên kết với file JavaScript -->
</body>
</html>
Giải thích code HTML:
- Chúng ta có một cấu trúc HTML cơ bản với
<!DOCTYPE html>
,<html>
,<head>
, và<body>
. - Trong
<head>
, chúng ta thiết lậpmeta
tags, đặttitle
cho trang và quan trọng là liên kết đến file CSS (style.css
) bằng thẻ<link>
. - Trong
<body>
, chúng ta tạo mộtdiv
với classtodo-container
để bao bọc toàn bộ nội dung ứng dụng, giúp dễ dàng tạo kiểu dáng chung sau này. - Thẻ
<h1>
là tiêu đề hiển thị cho ứng dụng. - Chúng ta nhóm
input
vàbutton
vào mộtdiv
có classinput-area
để dễ dàng quản lý layout sau này. - Thẻ input với
type="text"
vàid="todo-input"
là ô để người dùng nhập nội dung công việc mới. Thuộc tínhplaceholder
cung cấp gợi ý cho người dùng. - Thẻ button với
id="add-button"
là nút mà người dùng nhấn vào để thêm công việc. - Thẻ ul (unordered list) với
id="todo-list"
là nơi tất cả các công việc sẽ được hiển thị dưới dạng các thẻ<li>
(list item). Ban đầu nó trống rỗng, nội dung sẽ được thêm vào động bằng JavaScript. - Cuối cùng, ngay trước thẻ đóng
</body>
, chúng ta liên kết file JavaScript (script.js
) bằng thẻ<script>
. Vị trí này đảm bảo rằng code JavaScript sẽ chạy sau khi toàn bộ cấu trúc HTML đã được tải và sẵn sàng.
Bước 2: Tạo kiểu dáng với CSS
HTML đã cung cấp cấu trúc, nhưng nó trông khá nhàm chán. Bây giờ là lúc chúng ta thêm "gia vị" bằng CSS để làm cho ứng dụng trông đẹp mắt và thân thiện hơn với người dùng.
Tạo file style.css
và thêm các quy tắc CSS cơ bản:
body {
font-family: 'Arial', sans-serif; /* Chọn font chữ */
display: flex; /* Sử dụng flexbox để căn chỉnh nội dung */
justify-content: center; /* Căn giữa theo chiều ngang */
align-items: flex-start; /* Căn chỉnh lên đầu theo chiều dọc */
min-height: 100vh; /* Chiều cao tối thiểu bằng chiều cao màn hình */
background-color: #e0f7fa; /* Màu nền nhẹ nhàng */
margin: 0; /* Bỏ margin mặc định của body */
padding-top: 40px; /* Thêm khoảng trống phía trên */
}
.todo-container {
background-color: #ffffff; /* Nền trắng cho container */
padding: 30px; /* Khoảng đệm bên trong */
border-radius: 10px; /* Bo góc mềm mại */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); /* Đổ bóng nhẹ */
width: 100%;
max-width: 500px; /* Giới hạn chiều rộng tối đa */
text-align: center; /* Căn giữa nội dung text */
}
h1 {
color: #00796b; /* Màu xanh teal cho tiêu đề */
margin-bottom: 25px; /* Khoảng trống dưới tiêu đề */
font-size: 2em; /* Kích thước font lớn hơn */
}
.input-area {
display: flex; /* Sử dụng flexbox để căn chỉnh input và button */
margin-bottom: 20px; /* Khoảng trống dưới khu vực nhập */
}
#todo-input {
flex-grow: 1; /* Cho input chiếm hết không gian còn lại */
padding: 12px; /* Khoảng đệm bên trong input */
border: 1px solid #b2dfdb; /* Viền input */
border-radius: 5px; /* Bo góc input */
font-size: 1em;
margin-right: 10px; /* Khoảng cách giữa input và button */
}
#todo-input::placeholder {
color: #9e9e9e; /* Màu placeholder */
}
#add-button {
padding: 12px 20px; /* Khoảng đệm nút */
background-color: #00796b; /* Màu nền nút */
color: white; /* Màu chữ nút */
border: none; /* Bỏ viền nút */
border-radius: 5px; /* Bo góc nút */
cursor: pointer; /* Đổi con trỏ khi di chuột */
font-size: 1em;
transition: background-color 0.3s ease; /* Hiệu ứng chuyển đổi màu nền */
}
#add-button:hover {
background-color: #004d40; /* Đổi màu khi hover */
}
#todo-list {
list-style: none; /* Loại bỏ dấu đầu dòng mặc định của ul */
padding: 0; /* Bỏ padding mặc định */
margin: 0; /* Bỏ margin mặc định */
}
#todo-list li {
background-color: #e0f2f7; /* Màu nền cho mỗi mục công việc */
padding: 15px; /* Khoảng đệm bên trong li */
margin-bottom: 10px; /* Khoảng cách giữa các mục công việc */
border-radius: 5px; /* Bo góc li */
display: flex; /* Sử dụng flexbox để căn chỉnh nội dung và nút */
justify-content: space-between; /* Tạo khoảng cách giữa nội dung và nút */
align-items: center; /* Căn giữa theo chiều dọc */
word-break: break-word; /* Ngắt dòng nếu nội dung quá dài */
}
#todo-list li span {
flex-grow: 1; /* Cho nội dung công việc chiếm hết không gian */
margin-right: 10px; /* Khoảng cách giữa nội dung và các nút */
text-align: left; /* Căn lề trái cho nội dung */
}
#todo-list li button {
padding: 5px 10px; /* Khoảng đệm cho các nút hành động */
margin-left: 5px; /* Khoảng cách giữa các nút */
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.3s ease;
}
#todo-list li .complete-button {
background-color: #4caf50; /* Màu xanh lá cho nút hoàn thành */
color: white;
}
#todo-list li .complete-button:hover {
background-color: #388e3c;
}
#todo-list li .delete-button {
background-color: #f44336; /* Màu đỏ cho nút xóa */
color: white;
}
#todo-list li .delete-button:hover {
background-color: #d32f2f;
}
.completed {
text-decoration: line-through; /* Gạch ngang chữ */
color: #757575; /* Chuyển màu chữ sang xám */
font-style: italic; /* In nghiêng chữ */
}
.completed .complete-button {
background-color: #ffa726; /* Màu cam khi đã hoàn thành (để đánh dấu là chưa hoàn thành) */
}
.completed .complete-button:hover {
background-color: #fb8c00;
}
Giải thích code CSS:
- Chúng ta áp dụng các kiểu dáng cơ bản cho
body
như font chữ, màu nền, và sử dụng Flexbox để căn giữa nội dung chính. - Class
.todo-container
được tạo kiểu với nền trắng, bo góc, đổ bóng để tạo cảm giác nổi bật và chuyên nghiệp hơn. h1
được tạo kiểu cho tiêu đề.- Khu vực nhập (
.input-area
) sử dụng Flexbox để căn chỉnh ô input và nút "Thêm" trên cùng một hàng. #todo-input
và#add-button
được tạo kiểu về padding, border, màu sắc, và hiệu ứng hover để thân thiện với người dùng.#todo-list
và các mụcli
bên trong được tạo kiểu để loại bỏ dấu đầu dòng mặc định, thêm khoảng đệm, bo góc, và đặc biệt là sử dụng Flexbox để căn chỉnh nội dung công việc và các nút hành động (sẽ thêm bằng JS) trên cùng một hàng.- Các class
.complete-button
và.delete-button
được định nghĩa để tạo kiểu cho các nút hành động. - Class
.completed
được định nghĩa để áp dụng khi một công việc được đánh dấu là hoàn thành, sử dụngtext-decoration: line-through
để gạch ngang chữ và đổi màu chữ. Chúng ta cũng thay đổi màu nút "Hoàn thành" khi công việc đã hoàn thành để người dùng biết có thể bỏ đánh dấu.
Bước 3: Thêm "sự sống" với JavaScript
Đây là bước quan trọng nhất, nơi chúng ta sử dụng JavaScript để thêm logic tương tác cho ứng dụng. JavaScript sẽ lắng nghe các sự kiện (như click nút, nhấn phím), xử lý dữ liệu và thay đổi nội dung trên trang web một cách động.
Tạo file script.js
và thêm code JavaScript:
// 1. Lấy tham chiếu đến các phần tử HTML cần thiết
const todoInput = document.getElementById('todo-input');
const addButton = document.getElementById('add-button');
const todoList = document.getElementById('todo-list');
// 2. Hàm để thêm một công việc mới
function addTodoItem() {
// Lấy giá trị từ input và loại bỏ khoảng trắng thừa ở đầu/cuối
const todoText = todoInput.value.trim();
// Kiểm tra xem input có rỗng không
if (todoText === "") {
alert("Vui lòng nhập công việc!"); // Thông báo cho người dùng nếu rỗng
return; // Dừng hàm, không thêm công việc rỗng
}
// Tạo các phần tử HTML cho công việc mới
const li = document.createElement('li'); // Tạo thẻ <li> mới
const span = document.createElement('span'); // Tạo thẻ <span> để chứa nội dung công việc
span.textContent = todoText; // Gán nội dung từ input vào thẻ <span>
// Tạo nút Xóa
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Xóa';
deleteButton.classList.add('delete-button'); // Thêm class CSS
// Tạo nút Hoàn thành
const completeButton = document.createElement('button');
completeButton.textContent = 'Hoàn thành';
completeButton.classList.add('complete-button'); // Thêm class CSS
// Gắn thẻ <span> và các nút vào thẻ <li>
li.appendChild(span);
li.appendChild(completeButton); // Thêm nút Hoàn thành
li.appendChild(deleteButton); // Thêm nút Xóa
// Gắn thẻ <li> mới vào cuối danh sách <ul>
todoList.appendChild(li);
// Xóa nội dung trong input sau khi đã thêm công việc
todoInput.value = '';
// Đặt focus trở lại vào input để người dùng có thể nhập tiếp
todoInput.focus();
}
// 3. Lắng nghe sự kiện: Khi nút "Thêm" được click
addButton.addEventListener('click', addTodoItem);
// 4. Lắng nghe sự kiện: Khi người dùng nhấn phím trong input
todoInput.addEventListener('keypress', function(event) {
// Kiểm tra xem phím được nhấn có phải là Enter (mã 13) không
if (event.key === 'Enter') {
event.preventDefault(); // Ngăn hành động mặc định của phím Enter (ví dụ: gửi form)
addTodoItem(); // Gọi hàm thêm công việc
}
});
// 5. Lắng nghe sự kiện trên danh sách để xử lý Hoàn thành/Xóa (Sử dụng Event Delegation)
todoList.addEventListener('click', function(event) {
const target = event.target; // Phần tử HTML mà sự kiện click xảy ra trực tiếp trên đó
// Xử lý khi click vào nút Xóa
if (target.classList.contains('delete-button')) {
// event.target là nút Xóa, cha của nó (parentElement) chính là thẻ <li>
const listItem = target.parentElement;
listItem.remove(); // Xóa thẻ <li> khỏi DOM
}
// Xử lý khi click vào nút Hoàn thành
if (target.classList.contains('complete-button')) {
// event.target là nút Hoàn thành, cha của nó (parentElement) chính là thẻ <li>
const listItem = target.parentElement;
// Toggle class 'completed': thêm nếu chưa có, xóa nếu đã có
listItem.classList.toggle('completed');
// Cập nhật văn bản của nút Hoàn thành
if (listItem.classList.contains('completed')) {
target.textContent = 'Chưa hoàn thành';
target.style.backgroundColor = '#ffa726'; // Màu cam
} else {
target.textContent = 'Hoàn thành';
target.style.backgroundColor = '#4caf50'; // Màu xanh lá
}
}
});
Giải thích code JavaScript:
- Bước 1: Chúng ta dùng
document.getElementById()
để lấy các tham chiếu đến các phần tử HTML cóid
làtodo-input
,add-button
vàtodo-list
. Việc này cho phép chúng ta tương tác với các phần tử này trong code JavaScript. - Bước 2: Hàm
addTodoItem()
chứa toàn bộ logic để tạo và thêm một công việc mới.todoInput.value.trim()
lấy giá trị văn bản mà người dùng đã nhập vào ô input và dùng.trim()
để loại bỏ các khoảng trắng thừa ở hai đầu.if (todoText === "")
kiểm tra xem người dùng có nhập gì không. Nếu không, sẽ hiệnalert
và dùngreturn
để dừng hàm, tránh thêm công việc rỗng.document.createElement('li')
,document.createElement('span')
,document.createElement('button')
được dùng để tạo ra các phần tử HTML mới ngay trong bộ nhớ của trình duyệt.span.textContent = todoText;
gán nội dung công việc vào thẻ<span>
vừa tạo. Tương tự cho văn bản của các nút.li.appendChild(...)
dùng để gắn các phần tử con (span, nút hoàn thành, nút xóa) vào thẻ<li>
vừa tạo.todoList.appendChild(li)
dùng để thêm thẻ<li>
hoàn chỉnh vào cuối danh sách<ul>
(#todo-list
) trên trang web, làm cho nó hiển thị ra ngoài giao diện.todoInput.value = '';
vàtodoInput.focus()
dùng để làm sạch ô input và đặt con trỏ chuột trở lại đó, sẵn sàng cho lần nhập tiếp theo.
- Bước 3 & 4:
addButton.addEventListener('click', addTodoItem);
vàtodoInput.addEventListener('keypress', ...)
được sử dụng để lắng nghe các sự kiện.- Khi nút "Thêm" (
addButton
) được click, hàmaddTodoItem
sẽ được thực thi. - Khi người dùng nhấn phím trong ô input (
todoInput
), chúng ta kiểm tra xem phím đó có phải là 'Enter' hay không (event.key === 'Enter'
). Nếu phải, chúng ta cũng gọi hàmaddTodoItem
để thêm công việc một cách tiện lợi.event.preventDefault()
ngăn hành động mặc định của phím Enter (ví dụ: nó có thể cố gắng gửi một form nếu input nằm trong form).
- Khi nút "Thêm" (
- Bước 5:
todoList.addEventListener('click', ...)
sử dụng kỹ thuật Event Delegation. Thay vì thêm listener cho mỗi nút "Xóa" và "Hoàn thành" khi chúng được tạo ra (rất tốn tài nguyên và phức tạp), chúng ta chỉ cần thêm một listener duy nhất cho phần tử cha của chúng là<ul>
(todoList
).- Khi một click xảy ra bất kỳ đâu trong
<ul>
, listener này được kích hoạt. event.target
cho chúng ta biết phần tử chính xác nào đã bị click.- Chúng ta kiểm tra xem
target
có chứa class CSS.delete-button
hoặc.complete-button
hay không (target.classList.contains(...)
). - Nếu là nút "Xóa", chúng ta tìm phần tử cha của nó (
target.parentElement
), đó chính là thẻ<li>
chứa công việc, và dùnglistItem.remove()
để xóa thẻ<li>
đó khỏi trang. - Nếu là nút "Hoàn thành", chúng ta cũng tìm thẻ
<li>
cha và dùnglistItem.classList.toggle('completed')
để thêm classcompleted
vào thẻ<li>
nếu nó chưa có, hoặc xóa class này đi nếu nó đã có. CSS sẽ xử lý việc gạch ngang chữ dựa vào class này. Chúng ta cũng cập nhật văn bản và màu nền của nút Hoàn thành để người dùng biết trạng thái hiện tại.
- Khi một click xảy ra bất kỳ đâu trong
Comments