使用Vue 實現小小項目todolist

一個小項目todolist 各個功能的簡單演示(代碼在結尾)

1. 列表數據渲染
經過在ul 中的li 中v-for 綁定數組lists 進行列表數據渲染css

2. 增長數據
在el-input 上經過 @keyup.enter.native="addItem" 經過按下回車鍵來增長數據。方法綁定對象來獲取輸入框數據,使用push 方法在lists 中插入新的對象。最後將輸入框內容清空。html

3. 刪除數據
在span.close 上經過 @click="remove(index)" 來刪除當前選項。方法放入index 參數,直接經過splice 方法根據index 刪除lists 中對應的對象。vue

4. 編輯數據
在data 中新增屬性 currentItem: null,後在存放內容的label 上綁定方法 @dblclick="currentItem = item" 當雙擊時currentItem 等於當前item,而後在經過v-show 默認隱藏的input 修改框中讓 v-show="currentItem === item" ,只有currentItem 爲當前對象時input 修改框纔出現。input 修改框中默認的value 爲當前對象的內容。node

最後在input 框上綁定方法用來完成修改 @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)" 當按下回車鍵或者失去焦點時修改完成。修改完成後將currentItem 的內容重置爲null。web

5. 標記已完成的事項
在綁定了label 的input上設置 @click="item.completed = !item.completed" 經過點擊事件來改變當前對象的completed 屬性。而且 v-model="item.completed" 根據事項的完成情況來決定是否選中。而後將當前事項的li 父元素也設置 :class="{checked:item.completed}" 根據事項的完成狀況來改變樣式。這樣當點擊label 圖片時將事項標記爲已完成的狀態。element-ui

6. 未完成事項個數
經過computed 來對未完成事項的數量進行監聽,經過this.lists.filter(item => { return !item.completed }).length; 根據條件將原數組中未完成事項返回造成一個新數組,而後返回該數組的長度。數組

7. 清除已完成事項
使用方法removeCompleted 來清除已完成的事項。
先根據remaining 的值來判斷此時是否有已完成的事項,無則警告框彈出,有則詢問是否刪除。刪除方法爲更新lists 的值,經過
this.lists = this.lists.filter(element => { return !element.completed;}
過濾completed 爲true 的事項,返回未完成事項組成的數組。app

8. 事項全選狀態的改變
設置一個向下箭頭符號用來改變全部事項的全選狀態,並根據事項全選和未全選的狀況來反饋箭頭符號的狀態。
在全選框checkbox 中綁定computed 計算屬性 v-model="toggleAll"。在get 方法中根據未完成數remaining 是否爲0來判斷此時全部事項是否全選。
在set 方法中監聽toggleAll 數值,對全部事項狀態進行改變,當toggle 爲true 時遍歷lists 的全部對象,將computed 屬性修改成true,反之亦然。svg

9. 根據哈希值hash 來顯示事項
在data 新增屬性filterStatus 來獲取當前哈希值,在Vue 實例外使用方法函數

//當路由hash 值發生變化以後,會自動調用該函數
window.onhashchange = function(){
  const hash = window.location.hash.substr(2) || "all";
  app.filterStatus = hash;
}

而後在computed 中新增計算屬性filterItems 用來監聽filterStatus,並根據filterStatus 的數值來調用array.filter() 方法過濾數組並返回。這裏使用了switch 語句。
回到li 標籤中,將 v-for="(item, index) in lists" 更改成 v-for="(item, index) in filterItems",默認所有顯示。
對於All Active Completed 三個選項的class 屬性是否改變也與filterStatus 的值相關聯。

10.使用自定義組件實現自動聚焦
將自動聚焦指令綁定到input.editInput 上實現自動聚焦。

Vue.directive("app-focus", {
  inserted(el, binding){
    el.focus();
  },
  update(el, binding){    //更新以後也能調用
    el.focus();
  }
})

此處須要調用update 方法否則沒法做用。具體緣由暫不清楚,有懂的看官歡迎來評論告知!
本弱雞目前也還不清楚怎麼讓此處的el-input 實現自動聚焦功能,autofocus 和自定義指令都無效。。。

11. 調用localStorage 實現本地存儲
在Vue 實例外對localStorage.getItemlocalStorage.setItem 方法進行封裝。這兩個方法都是根據特定的key 值進行數據儲存和提取的。
使用watch 監聽lists 的變化,此處要監聽對象屬性的改變須要開啓深度監聽deep:true。當lists 數組的內容改變時調用 localStorage.setItem 將newValue 存入localStorage。
關於提取localStorage 的數據。將data 中的lists 變爲lists:itemStorage.fetch(),這樣就能夠將localStorage 內JSON 格式的數據賦值給lists 數組。

注意:代碼中的link 和script 須要本身從新引入,主輸入框和警告框用了elementUI 組件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./node_modules/element-ui/lib/theme-chalk/index.css">
    <title>Document</title>
</head>

<style>
body, html, ul, h1{
    margin: 0;
    padding: 0;
    font-size: 12px;    
    color: black;
}
div#app{
    max-width: 600px;
    margin: 0 auto;
}
h1.title{    
    padding: 10px 0;
    font-size: 80px;
    font-weight: normal;
    color: brown;
    opacity: 0.5;
}
div.todolist{
    border: 1px solid gainsboro;
    box-shadow: 2px 0 5px lightgray;
}
div.header{
    position: relative;
    border-bottom: 1px solid gainsboro;
}
input.el-input__inner{
    padding: 25px 0px 25px 60px;
    border: none;
    outline: none;
    font-size: 24px;
    text-indent: 5px;
}
input.el-input__inner::placeholder{
    color: lightgrey;
}
div.el-input span.el-input__suffix{
    font-size: 24px;
    margin: 2px 10px 0px 0px;
}
input#toggle-all{
    position: absolute;
    border: none;
    opacity: 0;
}
label.toggle-all-label{
    position: absolute;
    display: block;
    box-sizing: border-box;
    top: 1px;
    left: 1px;
    z-index: 1;
    width: 48px;
    height: 48px;
    font-size: 32px;
    color: gainsboro;
    transform: rotate(90deg);
    -webkit-user-select: none;
}
label.toggle-all-label::before{
    content: "❯";
    position: absolute;
    left: 16px;
}
input#toggle-all:checked + label.toggle-all-label{
    color: gray;
}
div.body ul li{
    position: relative;
    padding: 10px 10px 10px 65px;
    border-bottom: 1px solid lightsteelblue;
    font-size: 20px;
    font-weight: normal;
    font-family: 楷體;
    color: rgb(36, 78, 121);
}
div.body ul li:last-child{
    border-bottom: none;
}
div.body ul li.checked{
    color: lightgray;
    text-decoration: line-through;
}
input.toggle{
    position: absolute;
    border: none;
    opacity: 0;
}
label.toggle-label{
    position: absolute;
    top: 2px;
    left: 4px;
    z-index: 1;
    width: 40px;
    height: 40px;
    border-radius: 20px;
    background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
}
input.toggle:checked + label.toggle-label{
    background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
span.close{
    position: absolute;
    right: 10px;
    top: 12px;
    font-weight: bold;
    color: firebrick;
    opacity: 0;
    transition: opacity .2s;
    cursor: pointer;
}
div.body ul li:hover span.close{
    opacity: 1;
}
input.editInput{
    position: absolute;
    top: 0;
    left: 0;
    z-index: 2;
    width: 89.1%;
    padding: 10px 0 10.5px 65px;
    font-size: 20px;
    font-weight: normal;
    font-family: 楷體;
    border: none;
    outline: none;
    box-shadow: inset 0px -1px 5px 0px rgba(0, 0, 0, 0.3);
}
div.footer{
    padding: 12px;
    border-top: 1px solid lightgray;
    overflow: hidden;
}
span.itemNum{
    margin-right: 20%;
}
a.operate{
    display: inline-block;
    padding: 5px 8px;
    margin-right: 2px;
    border: 1px solid transparent;
    border-radius: 5px;
    text-decoration: none;
}
a.operate:hover{
    border: 1px solid lightgray;
}
a.checked{
    border: 1px solid lightgray;
}
a.clearAll{
    margin-left: 10%;
    text-decoration: none;
}
a.clearAll:hover{
    text-decoration: underline;
    cursor: pointer;
}
span.itemNum, a.operate, a.clearAll{
    font-size: 14px;
    font-weight: normal;
    color: gray;
}
</style>
<body>
<div id="app">
  <div style="padding: 0 10px; text-align: center;">
    <h1 class="title">todos</h1>
  </div>
  <div class="todolist">
    <div class="header">
      <input type="checkbox" id="toggle-all" v-model="toggleAll">
      <label for="toggle-all" class="toggle-all-label"></label>
      <el-input v-model="input" suffix-icon="el-icon-edit" placeholder="What needs to be done..." 
      @keyup.enter.native="addItem"></el-input>
    </div>
    <div class="body">
      <ul style="list-style-type: none;">
        <li v-for="(item, index) in filterItems" :key="item.id" :class="{checked:item.completed}">
          <label @dblclick="currentItem = item">{{ item.title }}</label>
          <input type="checkbox" :id="'toggle' + item.id" class="toggle" v-model="item.completed" 
          @click="item.completed = !item.completed">
          <label :for="'toggle' + item.id" class="toggle-label"></label>
          <span class="close" @click="remove(index)">×</span>
          <input type="text" class="editInput" :value="item.title" v-show="currentItem === item" 
          @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)" 
          v-app-focus="item === currentItem">
        </li>
      </ul>
    </div>
    <div class="footer">
      <span class="itemNum">{{ remaining }} item<span v-show="false">s</span> left</span>
      <a href="#/" :class="{operate:true, checked:filterStatus === 'all'}">All</a>    
      <a href="#/active" :class="{operate:true, checked:filterStatus === 'active'}">Active</a>    
      <a href="#/completed" :class="{operate:true, checked:filterStatus === 'completed'}" >Completed</a>    
      <a class="clearAll" @click="removeCompleted">Clear Completed</a>
    </div>
  </div>
    
</div>

    <script src="./node_modules/vue/dist/vue.js"></script>
    <script src="./node_modules/element-ui/lib/index.js"></script>
<script>
//自定義個storage 的key
const STORAGEKEY = "items-vuejs";    
//拓展功能:將數據保存在本地
const itemStorage = {
  //獲取數據
  fetch(){
    let data = localStorage.getItem(STORAGEKEY) || "[]";//經過key 獲取數據,當數據爲空時返回空數組
    //經過JSON.parse 將JSON 字符串轉換爲JSON 格式
    return JSON.parse(data);
  },
  //保存數據(傳入要保存的數據)
  save(items){
    localStorage.setItem(STORAGEKEY, JSON.stringify(items));    //經過JSON 形式保存
  }
}
const lists = [        //初始化數據(使用localStorage 後無關緊要)
    {id:1, title:"吃飯", completed:false,},
    {id:2, title:"學習", completed:true,},
    {id:3, title:"休息", completed:true,},
];
Vue.directive("app-focus", {
  inserted(el, binding){
    el.focus();
  },
  update(el, binding){    //更新以後也能調用
    el.focus();
  }
})
var app = new Vue({
  el:"#app",
  data() {
      return {
        input:"",
        lists:itemStorage.fetch(),
        currentItem:null,
        filterStatus:"all",
      }
  },
  computed: {
    remaining(){
      return this.lists.filter(item => { return !item.completed }).length;
    },
    toggleAll:{
      get:function(){
        return this.remaining === 0? true:false;
      },
      set:function(newValue){
        this.lists.forEach(element => {
          element.completed = newValue;
        });
      }
    },
    filterItems(){
      switch (this.filterStatus) {
          case "active":
            return this.lists.filter(item => !item.completed);
            break;
          case "completed":
            return this.lists.filter(item => item.completed)
          default:
            return this.lists;
            break;
        }
    }
  },
  methods: {
    addItem(event){
      let inputVal = event.target.value.trim();
      if(inputVal === ""){
        this.$message.error({ message:"Writing something here..."} );
      }else{
        this.lists.push({
          id:this.lists.length + 1, 
          title:inputVal, 
          completed:false, 
        });
        event.target.value = "";
      }
    },
    remove(index){
      this.lists.splice(index, 1);
    },
    removeCompleted(){
      if(this.remaining === this.lists.length){
        this.$message.warning({
          message:"無已完成的事項。"
        });
        return;
      }else{
        this.$confirm("將清空因此已完成選項,是否繼續?", "提示", {
        confirmButtonText:"清空",
        cancelButtonText:"取消",
        type: "warning"
      }).then(() => {
        this.lists = this.lists.filter(element => { return !element.completed;}    //返回未完成的
        );
        this.$message.success({
          message:"清空成功!"
        });
      }).catch(() => {
        this.$message.info({
          message:"取消清空。"
        });
      });
      }
    },
    editTitle(item, event){
      let value = event.target.value;
      if(value.trim() === ""){
        this.$message.warning({
          message:"修改後的內容不能爲空。"
        });
      }else{
        item.title = event.target.value;
        this.currentItem = null;
      }
    },
  },
  watch: {
    //深度監聽,當對象中的屬性發生改變後,使用deep:true 選擇則能夠實現監聽
    lists:{
      handler: function(newValue, oldValue){    //回調函數
        //數組變化時,將數據保存到本地
        itemStorage.save(newValue);
      }    ,
      deep:true
    }
  },
})

//當路由hash 值發生變化以後,會自動調用該函數
window.onhashchange = function(){
  const hash = window.location.hash.substr(2) || "all";
  app.filterStatus = hash;
}
//頁面刷新時清除掉上一次的哈希值,避免因地址欄的哈希值致使按鈕點擊無效果
window.onload = function(){
  window.location.hash = "";
}
</script>
</body>

</html>
相關文章
相關標籤/搜索