動手練一練,純手工碼一個不一樣的待辦事項(TODO LIST)

大家好,今天在這裏問大家一個問題,你多久沒有使用純原生的方法寫前端項目了,這裏並不是提倡大家純手工寫前端項目,只是我們偶爾可以練習下使用原生代碼的方式寫一些小項目,鞏固下前端的基礎,基礎越牢固,我們再學習其它框架時才能更快上手。

今天我們要一起動手練習的是一個 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實現了一個漂亮的界面效果,是不是很有意思的,看似一個簡單的小練習,也能做出不一樣的感覺,這就是作爲一個前端的樂趣。

最後給大家留一個小練習,你可能注意到了,緩存無法記錄已完成事項的狀態,你可以通過修改緩存操作的相關方法,讓本案例更加完善,期待你在留言區進行交流,今天的內容就到這裏,感謝你的閱讀。

本文完成後的項目源碼,請付費解鎖源碼鏈接,感謝你的支持: 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章