大家好,今天在這裏問大家一個問題,你多久沒有使用純原生的方法寫前端項目了,這裏並不是提倡大家純手工寫前端項目,只是我們偶爾可以練習下使用原生代碼的方式寫一些小項目,鞏固下前端的基礎,基礎越牢固,我們再學習其它框架時才能更快上手。
今天我們要一起動手練習的是一個 TODO LIST 項目,這樣的練習,我們在入門學習JS項目時,應該都練過,不過今天要做的是一個界面精美,帶有動畫效果和本地緩存功能的待辦事項。廢話不多說,我們來一起動手做一個。
一、首先我們先設計項目需求
在做任何項目前,我們應該提前設計下,我們做的是什麼,有哪些功能,界面長啥樣,今天我們要做的練習比較簡單,具體的有以下功能:
首先判斷本地瀏覽器緩存,加載本地的待辦事項內容
一個文本輸入框和輸入添加按鈕,用於輸入待辦事項的內容,並將內容寫入緩存
一個下拉篩選框,用於篩選全部的事項、待辦事項、已完成的事項
一個待辦事項列表顯示在文本輸入框的下方,我們可以進行事項狀態的編輯和刪除,並操作瀏覽器緩存
基於以上這幾個功能,我們設計出的頁面效果如下視頻所示:
二、重溫一些和本案例相關的原生API
完成本案例之前,我們先複習一些和原生API相關的一些知識,如果你都掌握了,請忽略本節內容
1、HTML DOM classList 屬性:
classList 屬性返回元素的類名,作爲 DOMTokenList 對象。
該屬性用於在元素中添加,移除及切換 CSS 類。
classList 屬性是隻讀的,但你可以使用 add() 和 remove() 方法修改它。
2、HTML DOM querySelector() 方法
querySelector() 方法返回文檔中匹配指定 CSS 選擇器的一個元素。注意:querySelector() 方法僅僅返回匹配指定選擇器的第一個元素。如果你需要返回所有的元素,請使用 querySelectorAll() 方法替代。
3、HTML DOM createElement() 方法
document.createElement()是在對象中創建一個對象,要與appendChild() 或insertBefore()方法聯合使用。其中,appendChild()方法在節點的子節點列表末添加新的子節點。insertBefore() 方法在節點的子節點列表任意位置插入新的節點。
4、Window localStorage 屬性
localStorage 和 sessionStorage 屬性允許在瀏覽器中存儲 key/value 對的數據。
localStorage 用於長久保存整個網站的數據,保存的數據沒有過期時間,直到手動去刪除。
localStorage 屬性是隻讀的。
提示: 如果你只想將數據保存在當前會話中,可以使用 sessionStorage 屬性, 該數據對象臨時保存同一窗口(或標籤頁)的數據,在關閉窗口或標籤頁之後將會刪除這些數據。
三、創建基礎的HTML結構
複習完一些原生API後,我們來創建項目的HTML結構,這裏用到一些圖標文件,我們引用了Font Awesome——一套絕佳的圖標字體庫和CSS框架,您可以使用CSS所提供的所有特性對它們進行更改,包括:大小、顏色、陰影或者其它任何支持的效果。然後我們定義了一個Header標籤,描述頁面標題。接下來我們來定義表單內容區域,顯示文本輸入框、下拉篩選框,篩選待辦事項。最後定義待辦事項列表容器。頁面結構其實很簡單,完成後的關鍵代碼如下:
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css"
integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk="
crossorigin="anonymous"
/>
<header>
<h1>前端達人的's Todo List</h1>
</header>
<form>
<input type="text" class="todo-input" />
<button class="todo-button" type="submit">
<i class="fas fa-plus-square"></i>
</button>
<div class="select">
<select name="todos" class="filter-todo">
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="uncompleted">Uncompleted</option>
</select>
</div>
</form>
<div class="todo-container">
<ul class="todo-list"></ul>
</div>
完成後的頁面效果如下圖所示:
四、定義基本的樣式
接下來我們定義樣式,讓背景色以線性漸變的方式進行填充,由橙黃色到橙紅色進行線性填充,頁面高度爲100vh,使用rem定義字體的大小,同時使用彈性盒子進行佈局,讓form表單在其容器內水平垂直居中。還有一點需要強調的是,這裏我自定義了下拉篩選框,這裏我使用了appearance:none的屬性,去掉了原有的邊框和右邊的下三角,使用我自定義的三角,相關的CSS代碼如下:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-image: linear-gradient(120deg, #f6d365 0%, #fda085 100%);
color: white;
font-family: "Poppins", sans-serif;
min-height: 100vh;
}
header {
font-size: 2rem;
}
header,
form {
min-height: 20vh;
display: flex;
justify-content: center;
align-items: center;
}
form input,
form button {
padding: 0.5rem;
font-size: 2rem;
border: none;
background: white;
}
form button {
color: #ff6f47;
background: #f7fffe;
cursor: pointer;
transition: all 0.3s ease;
}
form button:hover {
background: #fd6f47;
color: white;
}
.filter-todo {
padding: 1rem;
}
/*
自定義select選擇框,使用appearance去掉邊框和右邊的三角
*/
select {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
outline: 0;
box-shadow: none;
border: 0 !important;
background-image: none;
}
.select {
margin: 1rem;
position: relative;
overflow: hidden;
}
select {
color: #fd6f47;
font-family: "Poppins", sans-serif;
cursor: pointer;
width: 12rem;
}
/*
使用僞類添加自定義右三角
*/
.select::after {
content: "\25BC";
position: absolute;
top: 0;
right: 0;
padding: 1rem;
background: #ff6f47;
cursor: pointer;
pointer-events: none;
}
/*
定義待辦事項的容器垂直居中
*/
.todo-container {
display: flex;
justify-content: center;
align-items: center;
}
最後完成的效果如下圖所示:
五、編寫核心功能,待辦事項相關的操作
接下來我們繼續,新建一個app.js文件,編寫相關的方法,進行待辦事項的增刪改查,這也是本項目的核心。
首先我們定義 addTodo 函數
首先通過document.querySelector(".todo-button")獲取添加按鈕,接下來爲其添加事件綁定 addTodo 函數
然後我們定義 addTodo 函數,構建待辦事項列表的DOM結構,通過document.createElement 和 appendChild 將文本的輸入框的內容填充至todo-list容器
同時我們需要使用preventDefault()方法,阻止表單的默認提交行爲,防止表單刷新
//獲取添加按鈕,用於事件綁定
const todoButton = document.querySelector(".todo-button");
//獲取todo-list容器,用於追加待辦事項內容
const todoList = document.querySelector(".todo-list");
//綁定點擊事件addTodo方法
todoButton.addEventListener("click", addTodo);
function addTodo(e) {
//防止表單提交刷新
e.preventDefault();
//創建基本的待辦事項列表
const todoDiv = document.createElement("div");
todoDiv.classList.add("todo");
//獲取文本輸入框內容
const newTodo = document.createElement("li");
newTodo.innerText = todoInput.value;
newTodo.classList.add("todo-item");
todoDiv.appendChild(newTodo);
todoInput.value = "";
//創建完成操作按鈕
const completedButton = document.createElement("button");
completedButton.innerHTML = `<i class="fas fa-check"></i>`;
completedButton.classList.add("complete-btn");
todoDiv.appendChild(completedButton);
//創建刪除按鈕
const trashButton = document.createElement("button");
trashButton.innerHTML = `<i class="fas fa-trash"></i>`;
trashButton.classList.add("trash-btn");
todoDiv.appendChild(trashButton);
//將單個列表內容追加至todoList容器
todoList.appendChild(todoDiv);
}
接下來我們來定義更新、刪除方法deleteTodo
爲了方便起見,我將刪除和更新待辦事項合在一個方法裏,實際開發中我們應該定義兩個方法,這裏我使用了獲取當前按鈕的樣式,來判斷用戶是點了更新按鈕還是刪除按鈕,然後我們使用parentElement獲取當前按鈕的父節點DOM元素,然後通過remove()方法將其刪除或者通過toggle()方法切換當前事項的已完成和未完成的狀態的樣式,相關方法代碼如下:
//獲取待辦事項的列表區域
const todoList = document.querySelector(".todo-list");
//綁定操作方法
todoList.addEventListener("click", deleteTodo);
function deleteTodo(e) {
const item = e.target;
// 如果是 trash-btn 刪除按鈕,執行DOM刪除
if (item.classList[0] == "trash-btn") {
const todo = item.parentElement;
// 在刪除動效完成後,移動DOM節點
todo.addEventListener("transitionend", e => {
todo.remove();
});
}
// 如果是 complete-btn 狀態更新按鈕,進行未完成和已完成狀態的切換
if (item.classList[0] == "complete-btn") {
const todo = item.parentElement;
todo.classList.toggle("completed");
}
}
定義完上述兩個方法後,我們需要定義待辦事項列表的相關樣式,樣式代碼比較簡單,這裏就不過多描述了
.todo-list {
min-width: 30%;
list-style: none;
}
.todo {
margin: 0.5rem;
background: white;
font-size: 1.5rem;
color: black;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 1s ease;
}
.todo li {
flex: 1
}
.trash-btn,
.complete-btn {
background: #ff6f47;
color: white;
border: none;
padding: 1rem;
cursor: pointer;
font-size: 1rem;
}
.complete-btn {
background: rgb(11, 212, 162);
}
.todo-item {
padding: 0rem 0.5rem;
}
.fa-trash,
.fa-check {
pointer-events: none;
}
.completed {
text-decoration: line-through;
opacity: 0.5;
}
最後爲了讓刪除的操作有個動畫的效果,讓其順時針旋轉20度,往下移動10rem的距離消失,我們在被刪除的元素上添加一個刪除動效樣式fall,我們在deleteTodo方法裏,添加如下代碼:
....
const todo = item.parentElement;
//添加刪除動效
todo.classList.add("fall");
todo.addEventListener("transitionend", e => {
todo.remove();
});
...
其樣式動效代碼如下:
.fall {
transform: translateY(10rem) rotateZ(20deg);
opacity: 0.5;
}
待辦事項篩選方法 filterTodo
在本節的最後我們來定義 filterTodo 方法,用於篩選待辦事項的狀態,這裏有全部事項、已完成、待完成的事項,首先通過document.querySelector(".filter-todo")方法查找select元素,然後在其元素上綁定change事件,對應filterTodo的方法,我們根據select的value屬性,通過forEach方法進行迭代待辦事項的內容,根據classList.contains方法來判斷待辦事項的元素是否含有相關的狀態樣式,然後通過 style.display 來顯示或隱藏,依據這個思路,我們來看看完成後的代碼:
const filterOption = document.querySelector(".filter-todo");
const todoList = document.querySelector(".todo-list");
function filterTodo(e) {
//獲取待辦事項列表相關所有的子元素
const todos = todoList.childNodes;
todos.forEach(function (todo) {
//依據select的value的值結合當前元素的樣式,進行顯示或隱藏
switch (e.target.value) {
case "all":
todo.style.display = "flex";
break;
case "completed":
if (todo.classList.contains("completed")) {
todo.style.display = "flex";
} else {
todo.style.display = "none";
}
break;
case "uncompleted":
if (!todo.classList.contains("completed")) {
todo.style.display = "flex";
} else {
todo.style.display = "none";
}
}
});
}
六、編寫緩存方法,存儲待辦事項內容
爲了保存待辦事項的內容,不至於因爲瀏覽器的意外關閉,造成數據丟失的悲劇,我們這裏使用緩存進行待辦事項的保存。這裏涉及到緩存的讀取 getTodos() ,緩存的刪除removeLocalTodos(), 緩存的添加 saveLocalTodos() 三個方法。
緩存保存方法 saveLocalTodos()
這個保存方法比較簡單,首先我們定義一個緩存Key值todos和一個對象todos,如果這個KEY值的內容爲空,我們就實例化todos對象爲空數組;如果緩存對象有值,我們則需要先通過KEY值對象讀取緩存的值,然後將其轉換成對象賦值給todos。接下來我們將待辦事項的參數內容通過數組的Push方法,將其添加到這個數組對象裏,最後通過JSON.stringify方法將數組對象轉換成字符串,最後刷新緩存的內容,完成後代碼如下所示:
function saveLocalTodos(todo) {
let todos;
if (localStorage.getItem("todos") === null) {
todos = [];
} else {
todos = JSON.parse(localStorage.getItem("todos"));
}
todos.push(todo);
localStorage.setItem("todos", JSON.stringify(todos));
}
最後別忘記將此方法添加至 addTodo() 方法裏。
刪除緩存方法 removeLocalTodos()
有添加緩存方法就有刪除緩存方法,前面的邏輯和保存方法一直,要判斷緩存KEY值是否存在,如果不存在,則聲明todos爲空數組,如果存在則讀取緩存對象,將其轉換成數組對象賦值給todos對象,後面的邏輯則不一樣,獲取參數DOM對象的值,以及取其在數組對象索引的位置,然後通過splice的方法將其刪除,最後刷新緩存的值,最後完成的代碼如下:
function removeLocalTodos(todo) {
let todos;
if (localStorage.getItem("todos") === null) {
todos = [];
} else {
todos = JSON.parse(localStorage.getItem("todos"));
}
const todoIndex = todo.children[0].innerText;
todos.splice(todos.indexOf(todoIndex), 1);
localStorage.setItem("todos", JSON.stringify(todos));
}
最後別忘記將此方法添加至 deleteTodo() 方法裏
讀取緩存方法 getTodos()
這個方法的前部分邏輯和上兩個方法一致,關於待辦事項的緩存讀取完成後,就是需要將緩存的內容加載出來,呈現在界面上,這裏的代碼實現邏輯和addTodo()的DOM添加邏輯一致,唯一的區別就是嵌套在一個數組對象的迭代中,最後在DOMContentLoaded事件中(初始的 HTML 文檔被完全加載和解析完成之後)添加此方法,最終完成的代碼如下所示:
function getTodos() {
let todos;
if (localStorage.getItem("todos") === null) {
todos = [];
} else {
todos = JSON.parse(localStorage.getItem("todos"));
}
todos.forEach(function (todo) {
const todoDiv = document.createElement("div");
todoDiv.classList.add("todo");
const newTodo = document.createElement("li");
newTodo.innerText = todo;
newTodo.classList.add("todo-item");
todoDiv.appendChild(newTodo);
todoInput.value = "";
const completedButton = document.createElement("button");
completedButton.innerHTML = `<i class="fas fa-check"></i>`;
completedButton.classList.add("complete-btn");
todoDiv.appendChild(completedButton);
const trashButton = document.createElement("button");
trashButton.innerHTML = `<i class="fas fa-trash"></i>`;
trashButton.classList.add("trash-btn");
todoDiv.appendChild(trashButton);
todoList.appendChild(todoDiv);
});
}
七、最終完成後的代碼
到這裏,我們項目就完成了,代碼量也不是太多,通過此項目我們熟悉了基礎的DOM操作、數組操作以及緩存操作,並且通過CSS實現了一個漂亮的界面效果,是不是很有意思的,看似一個簡單的小練習,也能做出不一樣的感覺,這就是作爲一個前端的樂趣。
最後給大家留一個小練習,你可能注意到了,緩存無法記錄已完成事項的狀態,你可以通過修改緩存操作的相關方法,讓本案例更加完善,期待你在留言區進行交流,今天的內容就到這裏,感謝你的閱讀。
本文完成後的項目源碼,請付費解鎖源碼鏈接,感謝你的支持: