如何使用 HTML、CSS 和 Vanilla JavaScript 以及本地存储创建待办事项应用程序

JavaScript/前端
92
0
0
2024-10-22

互联网建立在与数据交互的基础上:从用户获取数据、存储数据、更新和删除数据。待办事项应用程序是练习这些基本技能的最佳工具。

在本教程中,我们将介绍如何使用 HTML、CSS 和 JavaScript 创建功能齐全的待办事项应用程序。用户将能够执行以下操作:

  • 添加任务
  • 编辑任务,
  • 删除任务和
  • 将任务标记为已完成

HTML结构

我们的 HTML 将包含三个部分:

  • 留言区
  • 搜索框部分
  • 任务部分
<div class="container">
<section class="message">
</section>
<section class="search-box">
<input type="text" placeholder="Add Task" id="addTaskInput" />
<button class="add-btn"><i class="fa-solid fa-plus"></i></button>
</section>
<section class="tasks">
<ul>
<!-- <li>
<input type="radio" class="complete" checked />
<span class="content complete">Create a Todo App with JavaScript</span>
<div class="buttons">
<button class="edit-btn">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="delete-btn">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</li> -->
</ul>
</section>
</div>

ul 元素是空的,因为我们将在其中使用 JavaScript 动态添加任务。每个任务将包含以下元素:

  • 用于将任务标记为完成的单选按钮
  • 用于显示任务的 span 元素
  • 一个编辑按钮和一个删除按钮

使用 CSS 设计样式

我们将从主体样式开始,以确保所有元素水平居中:

body {
background: #000;
height: 100vh;
display: flex;
justify-content: center;
color: #fff;
}

包含所有部分的容器元素将具有以下样式:

.container {
padding: 60px 50px;
margin-top: 100px;
width: 500px;
height: 500px;
position: relative;
}

接下来设置消息部分的样式,以确保它始终位于容器元素的中心。

.message{
text-align: center;
position: absolute;
top: 50%;
left: 30%;
}

对于搜索框部分,使用 Flexbox 确保子元素居中对齐且间隔均匀。

.search-box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
}

对于输入,添加以下样式:

.search-box input {
width: 100%;
height: 30px;
border-radius: 20px;
padding: 10px 10px;
background: rgb(41, 39, 39);
border: none;
color: #fff;
margin-left: 30px;
}

为了确保我们的任务垂直堆叠,请将 flex-direction 设置为 column,并添加一些填充和边距以确保各个任务之间的空间。

.tasks {
margin-top: 40px;
display: flex;
flex-direction: column;
}
.tasks li {
margin-top: 10px;
padding: 20px 10px;
background: rgb(28, 25, 28);
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 5px;
border-radius: 10px;
position: relative;
}

使用 flex-basis 确保用于显示任务的 span 元素占据宽度的 60%,而按钮仅占据 20%。

.tasks li span {
display: flex;
flex-basis: 60%;
}
.tasks li .buttons {
display: flex;
flex-basis: 20%;
}

去掉默认的按钮样式,并在编辑和删除按钮上添加透明背景,以确保图标可见:

.tasks li .buttons button {
background: transparent;
border: none;
}

将以下样式添加到图标中

.buttons button i {
color: rgb(175, 16, 16);
font-size: 20px;
}

对于单选按钮,我们将具有以下自定义样式:

.tasks input[type="radio"] {
appearance: none;
width: 20px;
height: 20px;
border: 1px solid #999;
border-radius: 50%;
outline: none;
box-shadow: 0 0 5px #999;
transition: box-shadow 0.3s ease;
}
.tasks input[type="radio"]:checked {
background-color: #bfb9b9;
}

最后,添加这些样式,这些样式将使用 JavaScript 动态添加。

.strike-through {
-webkit-text-stroke: 2px rgb(179, 186, 179);
text-decoration: line-through;
}
.complete {
background-color: #bfb9b9;
}

现在我们的应用程序看起来像这样:

JavaScript 功能

为了让用户能够添加任务,我们将使用 JavaScript。让我们首先使用 DOM(文档对象模型)获取以下 HTML 元素:

const message = document.querySelector(".message");
const tasksContainer = document.querySelector(".tasks");
const ulElement = tasksContainer.querySelector("ul");
const taskBtn = document.querySelector(".add-btn");

接下来,让我们初始化一些变量:

let html = "";
let allTasks = [];

该变量html将存储包含代表每个任务的 HTML 标记的 html 字符串。 该allTasks数组将存储所有任务,每个任务都有一个 id(时间戳)、一个名称和一个完成值,该值可以是 true 或 false。示例任务如下所示:

{
id:1700000,
name: "Name of task",
completed:false
}

添加新任务

好吧,首先向添加任务按钮添加单击事件侦听器。在事件侦听器函数中,我们将从用户获取输入值,将其传递给函数addTask(),并将输入值设置为空字符串。

如果用户没有输入值,我们将返回:这将防止在用户没有输入任何值时向列表中添加空任务或执行不必要的操作

const taskBtn = document.querySelector(".add-btn");
taskBtn.addEventListener("click", function () {
let newTaskInput = document.getElementById("addTaskInput");
const newTask =newTaskInput.value;
console.log(newTask);
if (!newTask) return;
addTask(newTask);
newTaskInput.value = "";
});

定义addTask()函数。

function addTask(task) {
}

在函数内部,我们想要执行以下操作:

  • 使用当前时间戳定义任务 ID
  • 将任务对象添加到allTasks数组中
  • 将 html 变量分配给任务 HTML 标记
  • 将 附加htmlulElement

更新函数如下。

function addTask(task) {
taskId = new Date().getTime();
allTasks.push({ id: taskId, task: task, completed: false });
html = ` <li data-id =${taskId}>
<input type="radio" class="complete-btn" />
<span class="content">${task}</span>
<div class="buttons">
<button class = "edit-btn" ><i class="fa-solid fa-pen-to-square"></i>Edit</button>
<button class="delete-btn"><i class="fa-solid fa-trash"></i>Delete</button>
</div>
</li>`;
ulElement.innerHTML += html;
editTask();
}

正如您所看到的,代表任务的每个 li 元素都有一个作为数据属性值添加的唯一 id ( data-id = ${taskId}):这将允许我们在编辑或删除任务时检索 id。

删除任务

定义一个函数,名为removeTask()

function removeTask(){
}

在函数内部removeTask(),我们想要获取 li 元素的 data 属性并从 DOM 中删除任务。

function removeTask(){
deleteBtn = document.querySelectorAll(".delete-btn");
deleteBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = this.closest("li");
const taskId = liElement.getAttribute("data-id");
liElement.remove();
});
});
}

让我们分解一下removeTask()函数中发生了什么。

  • 由于所有删除按钮都具有相同的类,因此我们使用该querySelectorAll属性来选择所有按钮。
  • 使用 forEach 迭代每个按钮
  • 对于每个按钮,我们使用最接近按钮的 li 元素this.closest("li)(其中 this 指的是单击的按钮)。
  • liElement然后我们从 DOM 中删除。
  • 最后,我们获取 li 元素的 data 属性值并将其存储在名为 的变量中taskId。我们在实现本地存储时会用到这个值

编辑任务

定义一个名为 的函数editTask()。在这个函数中,我们想要执行与删除按钮相同的步骤:即:

  • 获取所有编辑按钮
  • 使用forEach()方法迭代并获取最接近的li元素
  • 获取 data-id 属性
  • allTasks使用 id 在数组中查找任务
  • 更新 DOM 中的任务名称

更新editTask()函数如下:

function editTask() {
editBtn = document.querySelectorAll(".edit-btn");
editBtn.forEach((element) => {
element.addEventListener("click", function (event) {
const liElement = event.target.closest("li");
const taskId = liElement.getAttribute("data-id");
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIdIndex !== -1) {
const currentTask = allTasks[taskIdIndex].task;
const newTask = prompt("Edit Task:", currentTask);
if (
newTask !== null &&
newTask !== "" &&
newTask !== currentTask
) {
allTasks[taskIdIndex].task = newTask;
const contentElement = liElement.querySelector(".content");
contentElement.textContent = newTask;
}
}
});
});
}

让我们分解一下editTask()上面函数中发生的事情:

  • 从 data 属性获取任务 id 后,我们使用该findIndex()方法检查该 id 是否存在于allTaksks数组中。
  • 当传递给数组时,该findIndex()方法查找满足指定条件的第一个元素的索引。如果没有找到元素,则返回-1
  • 如果taskIndex不是-1,我们使用该taskIndex值来获取当前任务,代码如下allTasks[taskIndex].task const newTask = prompt("Edit Task", currentTask);:显示一个提示对话框,其中包含消息“编辑任务:”,并将输入值设置为当前任务内容(currentTask)。
  • 然后新值存储在newTask变量中。
  • if 语句验证用户输入的新值。
  • allTasks[taskIndex].task = newTask:更新数组中的新任务名称。
  • 最后,我们使用以下代码更新当前 li 元素的 span 内容:contentElement.textContent = new Task;

现在,如果您单击任何任务的编辑按钮,您应该会看到此提示。

将任务标记为完成

要将任务标记为完成,我们将以下 CSS 类应用于单选按钮和 li 元素中的内容。

.tasks input[type="radio"]:checked {
background-color: #bfb9b9;
}
.strike-through {
-webkit-text-stroke: 2px rgb(179, 186, 179);
text-decoration: line-through;
}
.complete {
background-color: #bfb9b9;
}

创建completeTask()函数,如下所示:

function completeTask() {
completeBtn = document.querySelectorAll(".complete-btn");
completeBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = event.target.closest("li");
console.log(liElement);
const contentSpan = liElement.querySelector(".content");
contentSpan.classList.add("strike-through");
const taskId = liElement.getAttribute("data-id");
const taskIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIndex !== -1) {
allTasks[taskIndex].completed = this.checked;
}
});
});
}

在该completeTask()函数中,我们执行以下操作:

  • 将事件侦听器附加到单选按钮,对于每个按钮,我们从最近的 li 元素的 data 属性中获取任务 id。
  • 将删除线 CSS 类添加到当前 li 元素的范围
  • 使用该findIndex()方法从数组中获取当前任务的索引allTasks,然后将按钮的状态更新为选中。

本地存储功能

即使添加任务后,刷新页面后它们也会消失。为了持久存储,我们将添加本地存储功能。

本地存储是一个允许您在浏览器中存储数据的对象。数据以键值对的字符串形式存储。即使关闭浏览器后,存储在浏览器中的数据仍然存在。只有清除缓存后,它才会被删除。

将此功能添加到我们的项目中将允许添加的数据即使在刷新或关闭页面后也能保留。 要将数据存储在本地存储中,可以使用 setItem,如下所示。

localStorage.setItem("task", "New task");

存储此数据后,使用 Chrome 开发工具,您可以在“应用程序”选项卡下看到这些数据。

要获取存储在本地存储中的项目,请使用以下密钥:

localStorage.getItem("tasks")

从本地存储中删除项目

localStorage.clear();

添加任务到本地存储

让我们实现在本地存储中添加任务的功能。由于我们已经拥有数组中的所有任务allTasks,因此我们需要做的就是将数据添加到本地存储中,如下所示:

  localStorage.setItem("tasks", JSON.stringify(allTasks));

由于本地存储中存储的数据是字符串格式,因此我们习惯JSON.stringify将任务对象转换为字符串进行存储。

更新addTask()函数如下:

function addTask(task) {
// the rest of the code
localStorage.setItem("tasks", JSON.stringify(allTasks));
}

现在返回并添加一些任务,您应该在浏览器中看到它们。

从本地存储加载

我们还需要从本地存储加载任务。创建一个名为 的函数loadFromStorage()。该函数将检查本地存储中是否有任务,如果找到,任务将使用该函数呈现在页面上renderTasks()

function loadFromStorage() {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
allTasks = JSON.parse(storedTasks);
renderTasks();
}
}

创建该renderTasks()函数并添加以下代码。

function renderTasks() {
ulElement.innerHTML = ""; // Clear existing tasks
allTasks.forEach((task) => {
const completedClass = task.completed
? "complete strike-through"
: "";
const html = `
<li data-id="${task.id}" class="${completedClass}">
<input type="radio" class="complete-btn" ${
task.completed ? "checked" : ""
} />
<span class="content">${task.task}</span>
<div class="buttons">
<button class="edit-btn"><i class="fa-solid fa-pen-to-square"></i>Edit</button>
<button class="delete-btn"><i class="fa-solid fa-trash"></i>Delete</button>
</div>
</li>`;
ulElement.innerHTML += html;
});
editTask();
completeTask();
removeTask();
}

让我们分解一下该renderTasks()函数的作用: `

  • ulElement.innerHTML = "" `:清除页面上任何现有任务
  • 然后,我们使用该forEach()方法迭代allTasks数组并将每个任务的 HTML 标记添加到ulElement.
  • const completedClass=task.completed? "complete strike-through": "":是一个条件,用于检查是否task.completed为 true 并添加“完整删除线”CSS 类。如果task.completed为 false,则不会应用 CSS 类。
  • 最后,我们将附加 editTask、completeTask 和 removeTask 事件侦听器。

更新本地存储中的任务

要更新本地存储中的任务,请更新editTask()函数如下:

function editTask() {
editBtn = document.querySelectorAll(".edit-btn");
editBtn.forEach((element) => {
element.addEventListener("click", function (event) {
const liElement = event.target.closest("li");
const taskId = liElement.getAttribute("data-id");
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIdIndex !== -1) {
console.log(allTasks[taskIdIndex]);
console.log(allTasks[taskIdIndex].task);
const currentTask = allTasks[taskIdIndex].task;
const newTask = prompt("Edit Task:", currentTask);
if (
newTask !== null &&
newTask !== "" &&
newTask !== currentTask
) {
allTasks[taskIdIndex].task = newTask;
const contentElement = liElement.querySelector(".content");
contentElement.textContent = newTask;
localStorage.setItem("tasks", JSON.stringify(allTasks)); //update this line
}
}
});
});
}

该行将localStorage.setItem("tasks",JSON.stringify(allTasks); 确保任务更新后更新任务的当前状态。 要从本地存储中删除任务,请创建一个deleteTask()函数并添加以下代码;

function deleteTask(id) {
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === id
);
if (taskIdIndex !== -1) {
allTasks.splice(taskIdIndex, 1);
localStorage.setItem("tasks", JSON.stringify(allTasks));
}
}

deleteTask()上面的函数中,我们使用任务的 id 来检查它是否存在于数组中allTasks。如果找到,我们使用该splice()方法从数组中删除该任务allTasks

更新removeTasks()函数如下:

function removeTask() {
deleteBtn = document.querySelectorAll(".delete-btn");
deleteBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = this.closest("li");
console.log(this);
const taskId = liElement.getAttribute("data-id");
liElement.remove();
deleteTask(taskId); //add this line
});
});
}

最后要做的事情是,如果用户没有待处理的任务,则向用户显示一条消息:

function updateMessage() {
if (ulElement.children.length === 0) {
message.innerHTML = "You are all caught up";
} else {
message.innerHTML = "";
}
}