changed 4 years ago
Linked with GitHub

【Vue電商】Todo List 練習

tags: vue

任務:todo list
Udemy:https://www.udemy.com/course/vue-hexschool/learn/lecture/10271404#overview

基本功能:

  • 按下新增button(或enter時)時會新增代辦事項
  • 顯示代辦事項
  • 點擊checkbox,代表完成代辦事項,該代辦事項會被劃掉(切換劃線的css style)
  • 點擊「全部」(預設是在全部):
    會顯示全部代辦事項;
    點擊「進行中」:
    會顯示未完成代辦事項;
    點擊「已完成」:
    會顯示完成代辦事項;
  • 按下「x」,會刪除該代辦事項
  • 按下「清除所有任務」,會刪除所有代辦事項
  • 右下會顯示還有多少筆任務未完成

進階功能:

  • double click代辦事項時,會切換為輸入欄
  • 在輸入欄按enter後:會修改代辦事項的title
  • 在輸入欄按esc後:會退出修改代辦事項的title

按下新增button(或enter時)時會新增代辦事項

  1. 在最上方的輸入欄,即是<input>使用v-model,此v-model是連接vue的data裏的newTodo,讓我們能夠在newTodo儲存使用者所輸入的代辦事項。

  2. 在「新增」button設定@click="addTodo",以及在<input>欄位設定@keyup.enter="addTodo"。作用是當使用者按下新增或enter鍵時,就執行addTodo函式。

<!-- 把使用者輸入的代辦事項儲存到vue的newTodo --> <!-- 當使用者按下新增或enter鍵時,就執行addTodo --> <input type="text" class="form-control" placeholder="準備要做的任務" v-model="newTodo" @keyup.enter="addTodo"> <div class="input-group-append"> <button class="btn btn-primary" type="button" @click="addTodo">新增</button> </div>

Vue:

var app = new Vue({ el: '#app', data: { // 存放使用者輸入的新代辦事項 newTodo: "", // 所有代辦事項 todos: [], }, }
  1. addTodo函式的作用是用.push方法,把新增的代辦事項加進todos裏。每筆代辦事項都需要有:
  • id
  • title
  • completed (該事項是否完成)

因此,addTodo的函式應該是這樣:

methods:{ addTodo: function(){ this.todos.push({ id: Math.floor(Date.now()), title: this.newTodo, // 預設是false completed: false, }) // 清空輸入欄 this.newTodo = ""; }

為什麼需要id?

因為每個代辦事項左邊都有個checkbox,該代辦事項的id需要與該checkbox的id相同,才可以使我們點擊該代辦事項的title時,就可以對應點擊到該checkbox。因此,每個代辦事項都要有一個獨有的id。

Date.now()方法產生id

Date.now()會產生一組數字,該數字是自 1970/01/01 00:00:00 UTC 起經過的毫秒數。之後用Math.floor()(取小於這個數的最大整數)去取整數。

顯示代辦事項

<!-- 使用v-for撈取所有在todos裏的代辦事項 --> <ul class="list-group list-group-flush text-left" v-for="item in todos"> <li class="list-group-item"> <div class="d-flex" @dblclick="editTodo(item)"> <div class="form-check"> <!-- 用:id和:for 綁定該代辦事項的id --> <!-- 用v-model來操控該代辦事項是否已被完成 --> <input type="checkbox" class="form-check-input" :id="item.id" v-model="item.completed"> <label class="form-check-label" :for="item.id"> {{ item.title }} </label> </div> ... </ul>

在checkbox使用v-model

在checkbox使用v-model,會得出boolean值。因此可以操控該代辦事項的complated狀態是true還是false

詳情:https://vuejs.org/v2/guide/forms.html

v-bindv-on縮寫

v-bind縮寫:
:。例如::class="":id=""

v-on縮寫:
@。例如:@click=""@dblclick=""@keyup.enter=""

防止輸入空白值(自己做的時候漏掉這部分)

為了防止使用者新增空白的代辦事項,我們需要在addTodo函式裏做判斷。以下修改一下addTodo函式:

addTodo: function(){ // 用trim方法刪掉多餘的空白 const value = this.newTodo.trim(); // 避免輸入空白的代辦事項 if(!value){ return }; this.todos.push({ // 以下是自己的寫法,自己寫時是用new Date // id: Math.floor(new Date), // 但老師建議這樣寫 id: Math.floor(Date.now()), title: value, completed: false, }) // 清空輸入欄 this.newTodo = ""; }

按下「x」後會刪除資料

先寫以下做法:

這個做法是把該事項的index直接傳進函式裏,並用它來刪掉資料。

<li class="list-group-item" v-for="(item, key) in filterTodo"> ... <button type="button" class="close ml-auto" aria-label="Close" @click="deleteTodo(key)">...</button> </li>
... methods:{ deleteTodo: function(key){ this.todos.splice(key, 1); } }
​​​​但以上做法會之後出現bug,請在讀完頁籤分類效果的filterTodo部分後,再跳回來以下部分!

為什麼會有bug?因為當我們轉到其他頁籤時,該頁籤的陣列資料(filterTodo)會不同於原始陣列(todos)。

例如以下情況,我新增了1,2,3,4,完成了2,3,之後我按下已完成頁籤,想刪除3,你會發現3並沒有被刪掉,反而是2被刪除:

這是因為在filterTodo陣列裏,第2筆資料是「3」,但是在todos這個原始陣列裏,第2筆資料是「2」,所以反而是「2」被刪掉了。

因此,要改用以下的寫法:

<!-- 改為傳送item.id --> <button type="button" class="close ml-auto" aria-label="Close" @click="deleteTodo(item.id)">...</button>
methods:{ deleteTodo: function(id){ let todoIndex = this.todos.findIndex(item => item.id === id); this.todos.splice(todoIndex, 1); } }

這個方法就是把該代辦事項的id傳進來deleteTodo,然後在deleteTodo裏,利用傳進來的id,找尋此id在原始陣列todos裏的index(我命名為todoIndex),最後利用todoIndex在todos陣列裏刪除該筆代辦事項。

這個做法就成功解決那個bug了。

把已完成的代辦項目加上劃線的class

當使用者完成該代辦事項時,該項目會有劃線效果:

CSS樣式:

.completed { text-decoration: line-through }
<!-- 切換completed CSS樣式 --> <label class="form-check-label" :for="item.id" :class="{'completed': item.completed}"> {{ item.title }} </label>

當該代辦事項的completed狀態是true時,就會加上completed CSS樣式。

頁籤分類效果(自己做時這裏有卡關)

  • 點擊進行中:就只會顯示未完成的項目。
  • 點擊已完成:就只會顯示已完成的項目。
  • 點擊全部:就會顯示全部項目(一載入網頁後,預設是全部)

以上這些功能,可以大概猜出這裏需要一個過濾功能,例如:

  • 點擊已完成 > 只撈已完成的資料 (completed狀態是 true)
  • 點擊進行中 > 只撈進行中的資料 (completed狀態是 false)

但我們不需要每個狀態都開一個新陣列,例如開一個叫activeTodoscompletedTodos,把完成和未完成的項目從todos裏面抽取來,並分別push進這兩個陣列裏,這是很累贅的做法。

我們可以這樣想:

  • todos 陣列
    這是原本放置所有代辦事項的陣列,我們不要動這個陣列
  • filterTodos 陣列
    這是一個會變動的陣列。它要做的事就是,看你點擊到哪個頁籤,它就按哪個頁籤去過濾todos陣列,並回傳過濾好的資料。

如何看你點擊哪個頁籤?我們可以在data裏設定一個變數,該變數會顯示目前使用者在哪個頁籤,我們把該變數取命為visibility

data: { newTodo: "", todos: [], visibility: 'all', }

在HTML的部分,設定當該頁籤被點擊時,就會更改visibility的變數值:

<li class="nav-item"> <a class="nav-link" href="#" :class="{ 'active' : visibility === 'all'}" @click.prevent="visibility = 'all'" >全部</a> </li> <li class="nav-item"> <a class="nav-link" href="#" :class="{ 'active' : visibility === 'active'}" @click.prevent="visibility = 'active'" >進行中</a> </li> <li class="nav-item"> <a class="nav-link" href="#" :class="{ 'active' : visibility === 'completed'}" @click.prevent="visibility = 'completed'" >已完成</a> </li>

別忘記click後面要加上.prevent,因為<a>本身有連結功能,預設是會跳到href裏的地址,所人要用.prevent取消跳轉到連結,這等同於之前學JS時的.preventDefault()

另外,在頁籤的部分,當該頁籤被點擊後,就會被加上active這個CSS樣式,例如我點擊了「進行中」後,「進行中」被框起來,以及文字變灰色:

所以以上程式碼中,也用:class綁定active這個CSS樣式。

回到Vue的部分,這時候我們會用到computed,重溫computed特性:

  • 有緩存機制,資料沒變動的話,就不會重新渲染

methods沒有緩存機制不管資料有沒有更新,它都會重新渲染

在我們的情況,因為當todos陣列(即是放置所有代辦項目的那個陣列)裏有項目被checked,即是被標記為完成,理論上,「進行中」和「已完成」頁面就需要被更新,「進行中」會顯示少了一個項目,「已完成」會顯示多了一個項目。

反之,todos陣列裏沒有變動,就不用重新渲染畫面。

所以這裏用computed

computed: { filterTodo: function(){ if(this.visibility === 'all'){ return this.todos; } else if(this.visibility === 'active'){ return this.todos.filter( item => item.completed === false); }else if(this.visibility === 'completed'){ return this.todos.filter( item => item.completed === true); } } }

之後我們要改成在filterTodo裏撈資料,而非原始陣列todos

<li class="list-group-item" v-for="(item, key) in filterTodo"> ... </li>

雙擊編輯代辦事項(自己做時這裏有卡關)

接下來要做的功能就是當使用者雙擊該代辦事項時,就會切換成輸入欄位,讓使用者修改該代辦事項的title,如下圖:

這個效果有幾個功能要做:

  • 修改該代辦事項的title
  • 隱藏原本的代辦事項,並打開input輸入欄
  • 修改完後,按enter儲存,按esc就退出和不儲存修改

修改該代辦事項的title

利用@dblclick=""來設定雙擊事件,並且執行editTodo函式。

html的部分:

<div class="d-flex" @dblclick="editTodo(item)">

Vue的部分,我們將該正在被修改的那筆代辦事項以及它的title暫存起來:

data: { newTodo: "", todos: [], visibility: 'all', // 以下變數會暫時存放該正在被修改的代辦事項 cacheTodo: {}, cacheTitle: '' }
methods:{ ..., editTodo: function(item){ // 把傳進來的item資料存起來 // 才可以知道目前哪筆資料正在被編輯中 this.cacheTodo = item; //cacheTitle會用來作編輯時預設顯示的內容 this.cacheTitle = item.title; // 以上暫存的做法可以達成一次只能編輯一個item, // 因為每當item被雙擊時,cacheTodo 和 cacheTitle 都會被更新, // 並指向該被雙擊的item。因此不會出現同時修改多個item的情況 } }

以下會再解釋為什麼要暫存該筆代辦事項的資料。

隱藏原本的代辦事項,並打開input輸入欄

雙擊後,就要隱藏原本的代辦事項,並打開input輸入欄。我們可用v-if去寫出這個效果:

<li class="list-group-item" v-for="(item, key) in filterTodo"> <div class="d-flex" @dblclick="editTodo(item)" v-if="item.id !== cacheTodo.id"> <div class="form-check"> <input type="checkbox" class="form-check-input" :id="item.id" v-model="item.completed"> <!-- 切換completed CSS樣式 --> <label class="form-check-label" :for="item.id" :class="{'completed': item.completed}"> {{ item.title }} </label> </div> <button type="button" class="close ml-auto" aria-label="Close" @click="deleteTodo(key)"> <span aria-hidden="true">&times;</span> </button> </div> <!-- 如果這個item的id與cacheTodo的id相同,就代表這個item正在被編輯中,因此要顯示以下的input欄位 --> <input type="text" class="form-control" v-if="item.id === cacheTodo.id"> </li>

以上程式碼解釋了為什麼剛剛我們需要暫存該筆事項,因為在這裏的隱藏/顯示功能中,需要用該筆事項去做判斷

如果這個代辦事項的id 與 暫存中的代辦事項id相同,就代表這個代辦事項正在被編輯,所以要隱藏原本的資料欄,並打開輸入欄位。同一道理,如果該代辦事項的id 不等於 暫存的id,那就意味該筆代辦事項並沒有正在被修改,所以就隱藏輸份欄,只顯示資料欄。

這裏用v-if去寫出效果,v-if的用法就是只要條件是true,就會渲染該元素。

v-if VS v-show

題外話:

  • v-if:條件是true才會渲染
  • v-show:不管條件是否true都會渲染,並用CSSdisplay屬性去隱藏或顯示。

所以:

  • 如果經常需要切換,就用v-show
  • 如果不常切換,就用v-if

詳細看:https://vuejs.org/v2/guide/conditional.html

雙擊修改時,預設會先顯示原本的title

重看上面那張gif,可見雙擊代辦事項,並切換到輸入欄後,輸入欄會預設顯示該代辦項目原本的title(這個gif例子中就是「1」),而非空白:

所以,我們需要在輸入欄位,預設會顯示該筆事項的title。這裏使用v-model去把title顯示出來:

<input type="text" class="form-control" v-if="item.id === cacheTodo.id" v-model="cacheTitle">

按esc後取消編輯,按enter儲存編輯

<input type="text" class="form-control" v-if="item.id === cacheTodo.id" v-model="cacheTitle" @keyup.esc="cancelEdit" @keyup.enter="finishEdit(item)" >

Vue部分:

methods:{ ... cancelEdit: function(){ this.cacheTodo = {}; } }

以上做法就是清空暫存的代辦事項,作用就是退出編輯。

methods:{ finishEdit: function(item){ // 更改該item的title item.title = this.cacheTitle; // 清空cacheTodo,否則input欄位不會隱藏起來 this.cacheTodo = {}; // 問題:以下這段不加也行?因為都是看<input>和<div>都只是看item.id去判斷。而下次修改另一筆資料時,cacheTitle會被替換掉 this.cacheTitle = ''; } }

顯示還剩下多少未完成項目

<div class="card-footer d-flex justify-content-between"> <span>還有 {{ countActiveTodo }} 筆任務未完成</span> </div>
computed:{ ... countActiveTodo: function(){ return this.todos.filter( item => item.completed === false).length; } }

刪除所有資料

<div class="card-footer d-flex justify-content-between"> <span>還有 {{ countActiveTodo }} 筆任務未完成</span> <a href="#" @click.prevent="deleteAllTodo">清除所有任務</a> </div>
methods:{ ... deleteAllTodo: function(){ this.todos = []; } }

完整程式碼

https://codepen.io/alysachan/pen/bGevJBV

Select a repo