示例 用 Vue 實作 ToDoList(CRUD)。

適用:

  • 略知 Vue.js 結構
  • 知道取 API 資料

用 Vue.js 實作一個「一眼瞬間」版的 ToDoList。 為什麼叫「一眼瞬間」呢?因為 F5 刷新就沒啦! 先來做這個最基本連 localStorage 都沒有的 ToDoList,當做 Vue.js 應用練習。
再多加一項初始載入指定的 API(Axios GET)。


# 完整栗子 >> Codepen Demo (opens new window)


# 練習素材:


# 拆解功能:

  1. (data)資料格式定義,每筆待辦項有:'id' / 'title' / 'completed'

    • id 值不可重複
    • completed 為 true / false
  2. 可以讀取(R)初始資料(API GET)

  3. 可以新增(C)待辦項並即時顯示(預設狀態為「未完成」)

  4. 可以編輯(U)目前的待辦項

    • 更名
    • 點擊時變更完成狀態

  5. 可以刪除(D)待辦項

  6. 可以一次刪除所有待辦項

  7. 頁籤(Tab)依完成狀態分類清單:全部 / 進行中 / 已完成

  8. 計數目前未完成的項,且有變動時即時更新計數


# CODE

(下列 HTML 的部分這邊只截取有應用到 Vue.js 的部分,也就是要觸發事件 / 進行資料綁定的,完整版請看 Codepen)


<div class="container" id="app">
  <input class="form-control" type="text" placeholder="準備要做的任務" 
          v-model="newTodo" @keyup.enter="addTodo"/>
  <div class="input-group-append">
    <button class="btn" type="button" @click="addTodo">新增</button>
    <template v-for="(item, index) in visibilityList">
      <li class="nav-item" :key="index"></li>
      <a class="nav-link" href="#" :class="{ active: visibility == item.value }" 
          @click="(visibility = item.value)">{{ item.name }}
      </a>
    </template>
    <template v-for="(item) in filteredTodos">
      <li class="list-group-item" :key="item.id" @dblclick="editTodo(item)">
        <div v-if="item.id !== cacheTodo.id">
          <input class="form-check-input" :id="item.id" type="checkbox" 
                  @click="changeComplated(item.id)" 
                  v-model="item.completed"/>
          <label class="form-check-label" :class="{ completed: item.completed }"
                  :for="item.id"> {{ item.title }}
          </label>
          <button class="close" type="button" aria-label="Close" 
                  @click="removeTodo(item)">×
          </button>
        </div>
        <input class="form-control" type="text" 
            v-model="cacheTitle" v-if="item.id == cacheTodo.id" 
            @keyup.esc="cancelEdit" @keyup.enter="doneEdit(item)"/>
      </li>
    </template>
  </div>
  <div class="card-footer">
      {{ `還有${activeTodosLength}筆任務未完成` }}
      <a href="#" @click="cleanTodo">清除所有任務</a>
  </div>
</div>
var urlAPI = "https://eudora-hsj.github.io/Vue-practice/data/todolist.json"
var app = new Vue({
    el: "#app",
    data() {
        return {
            newTodo:'',
            todos: [],
            visibilityList: [
                { name: "全部", value: "all" },
                { name: "進行中", value: "active" },
                { name: "已完成", value: "completed" }
            ],
            visibility: 'all',
            cacheTodo: {},
            cacheTitle: '',
        }
    },
    created() {
        this.getList(urlAPI)
    },
    methods: {
        getList(url) {
            axios
                .get(url)
                .then((res) => {
                    this.todos = res.data.data
                })
                .catch((err) => {
                    console.log(err)
                })
        },
        addTodo() {
            let newTodoStr = this.newTodo.trim()
            if (!newTodoStr) {
                return
            }
            this.newTodo = ""
            let submitData = {
                id: Math.floor(Date.now()),
                title: newTodoStr,
                completed: false
            }
            this.todos.push(submitData)
        },
        removeTodo(item) {
            this.todos.splice(this.getIndex(item.id), 1)
        },
        editTodo(item) {
            this.cacheTodo = item
            this.cacheTitle = item.title
        },
        cancelEdit() {
            this.cacheTodo = {}
        },
        doneEdit(item) {
            item.title = this.cacheTitle
            this.cacheTitle = ""
            this.cacheTodo = {}
        },
        cleanTodo() {
            this.todos.splice(0, this.todos.length)
        },
        getIndex(id) {
            return this.todos.findIndex((el) => el.id == id)
        },
        changeComplated(id){
            let index = this.getIndex(id)
            this.todos[index].completed = !this.todos[index].completed
        }
    },
    computed: {
        filteredTodos() {
            let nowTab = this.visibility
            switch (nowTab) {
                case "all":
                    return this.todos.filter((item) => true)
                case "active":
                    return this.todos.filter((item) => !item.completed)
                case "completed":
                    return this.todos.filter((item) => item.completed)
            }
        },
        getNewKey() {
            return Math.max(...this.todos.map((el) => +el.id))
        },
        activeTodosLength() {
            return this.todos.filter((item) => !item.completed).length
        }
    }
})

# 分段說明:

# Vue 基本結構及使用到的屬性:

var app = new Vue({
    el: "#app",,
    data(){    // 定義資料 
    },
    created(){ // 初始完成 Vue 建立後要執行...
    },
    methods:{ // 定義在Vue或頁面中要使用的方法(function)
    },
    computed:{ // 進行計算的方法
    }
})

# 定義資料

  • 定義初始資料

    data() {
        return {
            todos: [],
            visibilityList: [  // 3種檢視清單(Tab)
                { name: "全部", value: "all" },
                { name: "進行中", value: "active" },
                { name: "已完成", value: "completed" }
                // name是頁面顯示文字,value是程式中操控的值 
            ],
            visibility: 'all',   // 當前檢視中的清單Tab,預設為全部
            cacheTodo: {},  // 暫存的Todo 
            cacheTitle: '' //暫存的項目名稱
        }
    }
    

# 讀取指定的 API 載入初始資料

  • 將 API 網址存成一變數

    var urlAPI = "https://eudora-hsj.github.io/Vue-practice/data/todolist.json"
    

    並且觀察一下 JSON 檔資料結構:

    {
        "data":
            [
                {
                    "id": "1",
                    "title": "買牛奶",
                    "completed": false
                },
                {
                    ...
                }
        ]
    }
    
  • 在 methods 裡定義讀取資料的 function(Axios)

    methods:{ 
        getList(url) {
            axios
                .get(url)
                .then((res) => {
                    this.todos = res.data.data
                })
                .catch((err) => {
                    console.log(err)
                })
        },
    }
    
  • 並且在 Vue 初始建立完成時執行 getList(url)

    this.getList(urlAPI)
    

# 可以新增(C)待辦項

  • 在對應的 DOM @dbclick / @keyup.enter 觸發事件
    <button class="btn btn-primary" type="button" @click="addTodo">新增</button>
    
    <input class="form-control" type="text" 
           v-model="newTodo" @keyup.enter="addTodo"/>
    
  • 定義的方法
    methods: {
        addTodo() {
            let newTodoStr = this.newTodo.trim()
            if (!newTodoStr) {
                return
            }
            this.newTodo = ""
            let submitData = {
                id: Math.floor(Date.now()),
                title: newTodoStr,
                completed: false
            }
            this.todos.push(submitData)
        }
    }
    

# 可以編輯(U)目前的待辦項

  • 在對應的 DOM @dbclick 觸發事件
    <li class="list-group-item" :key="item.id" @dblclick="editTodo(item)">
        (略)
    </li>
    
  • 定義的方法
    methods: {
        editTodo(item) {
            this.cacheTodo = item
            this.cacheTitle = item.title
        }
    }
    

# 可以刪除(D)待辦項

  • 在對應的 DOM @click 觸發事件
    <button class="close" type="button" @click="removeTodo(item)">
    </button>
    
  • 定義的方法
    刪除該項,並以 index 與 item 的 id 對應
    methods: {
        removeTodo(item) {
            this.todos.splice(this.getIndex(item.id), 1)
        }
    }
    

# 可以一次刪除所有待辦項

  • 在對應的 DOM @click 觸發事件
    <a href="#" @click="cleanTodo">清除所有任務</a>
    
  • 定義的方法
    filter 篩選出 completed 為 false 的資料並回傳筆數
    methods: {
        cleanTodo() {
            this.todos.splice(0, this.todos.length)
            }
        }
    }
    
  • 補充資料: JS-splice() | MDN (opens new window)

# 頁籤(Tab)依完成狀態分類清單:全部 / 進行中 / 已完成

  • Tab 頁籤內容抓取自 data 裡的 visibilityList

  • @click,visibility 等於該 Tab 的 Value (all / active / completed )

  • 並且寫有當前Tab樣式的 active 要維持在 visibility 對應的 Tab

    <template v-for="(item, index) in visibilityList">
        <li class="nav-item" :key="index">
            <a class="nav-link" href="#" 
              :class="{'active' : visibility == item.value }"
              @click="visibility=item.value">
                  {{item.name}}
            </a>
      </li>
    </template>
    
  • 清單項目抓取自 filteredTodos 的結果(篩選出當前 Tab 的項目)

      <template v-for="(item) in filteredTodos">
        <li class="list-group-item">
         (略)
        </li>
      </template>
    
  • 計算的方法
    根據當前的 visibility 回傳對應狀態的項目

    computed: {
        filteredTodos() {
            let nowTab = this.visibility
            switch (nowTab) {
                case "all":
                    return this.todos.filter((item) => true)
                case "active":
                    return this.todos.filter((item) => !item.completed)
                case "completed":
                    return this.todos.filter((item) => item.completed)
            }
        },
    }
    

# 計數目前未完成的項,且有變動時即時更新計數

  • 在對應的 DOM 綁定顯示資料
    <div>{{`還有 ${activeTodosLength} 筆任務未完成`}}</div>
    
  • 計算的方法
    filter 篩選出 completed 為 false 的資料並回傳筆數
    computed: {
        activeTodosLength() {
            return this.todos.filter((item) =>            
                !item.completed).length
            }
        }
    }
    

# 參考資料:


⮩ 本文同步發表在第 12 屆 iT 邦幫忙鐵人賽 《For 前端小幼苗的圖解筆記》 (opens new window)