hocvietcode.com
  • Trang chủ
  • Học lập trình
    • Lập trình C/C++
    • Cấu trúc dữ liệu và giải thuật
    • Lập trình HTML
    • Lập trình Javascript
      • Javascript cơ bản
      • ReactJS framework
      • AngularJS framework
      • Typescript cơ bản
      • Angular
      • NodeJS
    • Lập trình Mobile
      • Lập Trình Dart Cơ Bản
        • Dart Flutter Framework
    • Cơ sở dữ liệu
      • MySQL – MariaDB
      • Micrsoft SQL Server
      • Extensible Markup Language (XML)
      • JSON
      • MongoDB
    • Lập trình PHP
      • Lập trình PHP cơ bản
      • Laravel Framework
    • Lập trình Java
      • Java Cơ bản
      • Spring Boot
    • Lập trình C#
      • Lập Trình C# Cơ Bản
      • ASP.NET Core MVC
    • Lập trình Python
    • Git và Github
    • Machine Learning
  • WORDPRESS
    • WordPress cơ bản
    • WordPress nâng cao
    • Chia sẻ WordPress
  • Kiến thức hệ thống
    • Microsoft Azure
    • Docker
    • Linux
    • Mạng và truyền thông
  • Chia sẻ IT
    • Tin học văn phòng
      • Microsoft Word
      • Microsoft Excel
    • Marketing
      • Google Adwords
      • Facebook Ads
      • Kiến thức khác
    • Chia sẻ phần mềm
    • Review công nghệ
    • Công cụ – tiện ích
      • Kiểm tra bàn phím online
      • Kiểm tra webcam online
Đăng nhập
  • Đăng nhập / Đăng ký

Please enter key search to display results.

Home
  • Javascript cơ bản
JavaScript cơ bản – Bài 18: Dự án thực hành: ứng dụng quản lý công việc học tập

JavaScript cơ bản – Bài 18: Dự án thực hành: ứng dụng quản lý công việc học tập

  • 17-10-2026
  • Toanngo92
  • 0 Comments

Loạt bài JavaScript cơ bản · Bài 18/18 — Mục lục học tập

Đây là bài tổng hợp của loạt JavaScript cơ bản. Bạn sẽ xây dựng danh sách công việc học tập: thêm việc mới, đánh dấu hoàn thành, xóa việc và giữ dữ liệu sau khi tải lại trang. Ứng dụng chạy trong trình duyệt, không cần framework và không có máy chủ lưu dữ liệu.

Mục lục

  • Yêu cầu và mô hình dữ liệu
  • Mã hoàn chỉnh
  • Đọc chương trình theo luồng
  • Kịch bản tự kiểm tra
  • Bài tập mở rộng
  • Học tiếp sau loạt cơ bản

Yêu cầu và mô hình dữ liệu

  • Công việc có text là chuỗi và done là boolean.
  • Nội dung sau khi bỏ khoảng trắng không được rỗng; tối đa 120 đơn vị độ dài chuỗi.
  • Mỗi lần thay đổi đều cập nhật giao diện và thử lưu vào localStorage.
  • Nếu lưu thất bại, người dùng vẫn thao tác được và có thông báo.

Mảng tasks là nguồn dữ liệu chính. Hàm render tạo giao diện từ mảng; loadTasks và saveTasks phụ trách lưu trữ. Bản nhỏ này dùng vị trí trong mảng bên trong các callback và dựng lại listener sau mỗi render. Khi thêm tính năng lọc hoặc sắp xếp độc lập, nên bổ sung định danh ổn định cho từng công việc.

Mã hoàn chỉnh

Lưu toàn bộ mã dưới đây thành index.html trong một thư mục riêng. Mở bằng máy chủ tĩnh cục bộ của trình soạn thảo và giữ cùng địa chỉ để kiểm tra lưu trữ. Đây là mã để tự chạy trên máy, không dán thẻ script trực tiếp vào nội dung bài WordPress.

<!doctype html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Công việc học tập</title>
  <style>
    body { font: 16px/1.6 system-ui, sans-serif; max-width: 680px; margin: 40px auto; padding: 0 16px; color: #182638; }
    form { display: flex; gap: 8px; flex-wrap: wrap; }
    label[for="task"] { width: 100%; }
    input[type="text"] { flex: 1; min-width: 160px; padding: 10px; }
    button { padding: 8px 12px; cursor: pointer; }
    ul { padding: 0; list-style: none; }
    li { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid #ddd; }
    li label { flex: 1; overflow-wrap: anywhere; }
    .done { text-decoration: line-through; color: #596575; }
    #storage-message { color: #9c3b12; }
  </style>
</head>
<body>
  <h1>Công việc học tập</h1>
  <form id="task-form">
    <label for="task">Công việc mới</label>
    <input id="task" type="text" maxlength="120" required placeholder="Ví dụ: luyện 3 bài vòng lặp">
    <button type="submit">Thêm</button>
  </form>
  <p id="message" role="status"></p>
  <p id="storage-message" role="status"></p>
  <p id="summary" aria-live="polite"></p>
  <ul id="task-list"></ul>
  <script>
    "use strict";
    const KEY = "hvc-basic-tasks-v1";
    const form = document.querySelector("#task-form");
    const input = document.querySelector("#task");
    const message = document.querySelector("#message");
    const storageMessage = document.querySelector("#storage-message");
    const summary = document.querySelector("#summary");
    const list = document.querySelector("#task-list");
    let tasks = loadTasks();

    function loadTasks() {
      try {
        const raw = localStorage.getItem(KEY);
        if (raw === null) return [];
        const data = JSON.parse(raw);
        const valid = Array.isArray(data) && data.length <= 100 && data.every((item) =>
          item !== null && typeof item === "object" &&
          typeof item.text === "string" && item.text.trim().length > 0 &&
          item.text.length <= 120 && typeof item.done === "boolean"
        );
        if (!valid) throw new Error("Dữ liệu không đúng cấu trúc");
        return data.map((item) => ({ text: item.text, done: item.done }));
      } catch (error) {
        storageMessage.textContent = "Không đọc được dữ liệu cũ. Bạn có thể tạo danh sách mới.";
        console.warn(error);
        return [];
      }
    }

    function saveTasks() {
      try {
        localStorage.setItem(KEY, JSON.stringify(tasks));
        storageMessage.textContent = "";
      } catch (error) {
        storageMessage.textContent = "Chưa lưu được. Dữ liệu hiện tại chỉ được giữ đến khi tải lại trang.";
        console.warn(error);
      }
    }

    function render() {
      list.replaceChildren();
      tasks.forEach((task, index) => {
        const item = document.createElement("li");
        const label = document.createElement("label");
        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = task.done;
        const text = document.createElement("span");
        text.textContent = " " + task.text;
        text.classList.toggle("done", task.done);
        label.append(checkbox, text);

        checkbox.addEventListener("change", () => {
          tasks[index].done = checkbox.checked;
          saveTasks();
          render();
          const nextCheckbox = list.querySelectorAll('input[type="checkbox"]')[index];
          if (nextCheckbox) nextCheckbox.focus();
        });

        const removeButton = document.createElement("button");
        removeButton.type = "button";
        removeButton.textContent = "Xóa";
        removeButton.setAttribute("aria-label", "Xóa công việc: " + task.text);
        removeButton.addEventListener("click", () => {
          tasks.splice(index, 1);
          saveTasks();
          render();
          input.focus();
          message.textContent = "Đã xóa công việc.";
        });

        item.append(label, removeButton);
        list.append(item);
      });
      const doneCount = tasks.filter((task) => task.done).length;
      summary.textContent = tasks.length === 0
        ? "Danh sách đang trống. Hãy thêm công việc đầu tiên."
        : "Đã hoàn thành " + doneCount + "/" + tasks.length + " công việc.";
    }

    form.addEventListener("submit", (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text.length === 0 || text.length > 120) {
        message.textContent = "Nhập nội dung từ 1 đến 120 ký tự.";
        input.focus();
        return;
      }
      if (tasks.length >= 100) {
        message.textContent = "Ví dụ giới hạn 100 công việc. Hãy xóa bớt trước khi thêm.";
        return;
      }
      tasks.push({ text, done: false });
      saveTasks();
      render();
      form.reset();
      input.focus();
      message.textContent = "Đã thêm công việc.";
    });

    render();
  </script>
</body>
</html>

Đọc chương trình theo luồng

  1. Trang đọc dữ liệu cũ, kiểm tra cấu trúc và gọi render lần đầu.
  2. Người dùng submit form: chương trình chặn điều hướng, kiểm tra nội dung rồi thêm object vào mảng.
  3. saveTasks ghi chuỗi JSON; render dựng lại danh sách bằng các phần tử DOM.
  4. Checkbox thay đổi done; nút Xóa bỏ phần tử rồi lưu và dựng lại giao diện.

Nội dung công việc được gán bằng textContent, nên văn bản người dùng nhập không được phân tích thành thẻ HTML. Ứng dụng không dùng localStorage.clear và chỉ thao tác khóa riêng của ví dụ.

Kịch bản tự kiểm tra

  • Danh sách mới hiển thị thông báo trống.
  • Thêm hai việc, đánh dấu một việc: thống kê là 1/2.
  • Tải lại: hai việc và trạng thái hoàn thành vẫn còn khi lưu trữ hoạt động.
  • Xóa một việc: số lượng và dữ liệu lưu thay đổi theo.
  • Nhập chỉ khoảng trắng: không tạo mục rỗng.
  • Nhập chữ có ký tự < và >: giao diện hiển thị văn bản.
  • Thao tác được bằng bàn phím; lỗi đọc hoặc ghi dữ liệu có thông báo.

Bài tập mở rộng

Tự bổ sung bộ lọc Tất cả / Chưa xong / Đã xong. Trước khi lọc, thêm id ổn định vào mỗi công việc và dùng id để cập nhật, vì chỉ số của danh sách đã lọc có thể khác chỉ số trong mảng gốc. Sau đó thử thêm tính năng sửa nội dung và đếm số việc còn lại.

Tổng kết: bạn đã kết hợp dữ liệu, hàm, DOM và sự kiện thành một ứng dụng hoàn chỉnh ở mức nhập môn. Hãy tự dựng lại từng phần trước khi học tiếp Promise, Async/Await hoặc thư viện giao diện.

Học tiếp sau loạt cơ bản

  • Giới thiệu tổng quan về ES6
  • Khái niệm bất đồng bộ, Async/Await trong JavaScript

← Bài trước: Lưu dữ liệu với localStorage, JSON và try…catch

Xem chuyên mục JavaScript cơ bản

Bài viết liên quan:

JavaScript cơ bản – Bài 16: Biểu mẫu, preventDefault và kiểm tra dữ liệu nhập
JavaScript cơ bản – Bài 17: Lưu dữ liệu với localStorage, JSON và try…catch
JavaScript cơ bản – Bài 15: Sự kiện và addEventListener: làm bộ đếm tương tác
JavaScript cơ bản – Bài 13: Xử lý chuỗi và template literal
JavaScript cơ bản – Bài 14: DOM: chọn phần tử, đổi nội dung và tạo danh sách
JavaScript cơ bản – Bài 12: Đối tượng, thuộc tính và danh sách đối tượng
JavaScript cơ bản – Bài 10: Mảng, chỉ số và cách duyệt danh sách
JavaScript cơ bản – Bài 11: Xử lý mảng với map, filter, find và reduce
JavaScript cơ bản – Bài 08: Hàm, tham số, return và arrow function
JavaScript cơ bản – Bài 09: Phạm vi biến và cách tránh lỗi ReferenceError
JavaScript cơ bản – Bài 07: Vòng lặp for, while, break và continue
JavaScript cơ bản – Bài 06: Rẽ nhánh với if, else và switch

THÊM BÌNH LUẬN Cancel reply

Dịch vụ thiết kế Wesbite

NỘI DUNG MỚI CẬP NHẬT

CSS cơ bản – 20 bài từ HTML đến website responsive với Flexbox, Grid và mobile-first

CSS cơ bản – Bài 05: Đơn vị và kích thước: px, rem, %, min và max

CSS cơ bản – 20 bài từ HTML đến website responsive với Flexbox, Grid và mobile-first

CSS cơ bản – Bài 02: Selector và class: chọn đúng phần tử cần sửa

CSS cơ bản – 20 bài từ HTML đến website responsive với Flexbox, Grid và mobile-first

CSS cơ bản – Bài 01: Kết nối CSS với HTML và sửa quy tắc đầu tiên

CSS cơ bản – 20 bài từ HTML đến website responsive với Flexbox, Grid và mobile-first

CSS cơ bản – Bài 20: Hoàn thiện website và sửa lỗi bằng DevTools

CSS cơ bản – 20 bài từ HTML đến website responsive với Flexbox, Grid và mobile-first

CSS cơ bản – Bài 03: Vì sao CSS không có tác dụng? Cascade và specificity

Giới thiệu

hocvietcode.com là website chia sẻ và cập nhật tin tức công nghệ, chia sẻ kiến thức, kỹ năng. Chúng tôi rất cảm ơn và mong muốn nhận được nhiều phản hồi để có thể phục vụ quý bạn đọc tốt hơn !

Liên hệ quảng cáo: [email protected]

Kết nối với HỌC VIẾT CODE

© hocvietcode.com - Tech888 Co .Ltd since 2019

Đăng nhập

Trở thành một phần của cộng đồng của chúng tôi!
Registration complete. Please check your email.
Đăng nhập bằng google
Đăng kýBạn quên mật khẩu?

Create an account

Welcome! Register for an account
The user name or email address is not correct.
Registration confirmation will be emailed to you.
Log in Lost your password?

Reset password

Recover your password
Password reset email has been sent.
The email could not be sent. Possible reason: your host may have disabled the mail function.
A password will be e-mailed to you.
Log in Register
×