• Vue3.x Composition API - ToDoList 案例


    Vue3.x Composition API - ToDoList 案例

    ToDoList 功能列表

    • 添加代办事项
    • 删除待办事项
    • 编辑待办事项
    • 切换待办事项
    • 存储待办事项

    创建项目

    npm

    npm init vite-app to-do-list
    
    • 1

    yarn

    yarn create vite-app to-do-list
    
    • 1

    添加待办事项

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    删除待办事项

    HTML 添加删除事件

    <button class="destroy" @click="remove(todo)">button>
    
    • 1

    添加删除方法

    import { ref } from 'vue';
    
    const useRemove = (todos) => {
      const remove = (todo) => {
        const index = todos.value.indexOf(todo);
        todos.value.splice(index, 1);
      };
      return {
        remove,
      };
    };
    
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
    
        return {
          todos,
          ...useAdd(todos),
          ...useRemove(todos),
        };
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    编辑待办事项

    • 双击待办事项,展示编辑文本框
    • 按回车或者编辑文本框失去焦点修改数据
    • 按 ESC 取消编辑
    • 把编辑文本框清空按回车,删除这一项
    • 显示编辑文本框的时候获取焦点

    HTML

    <section class="main">
      <ul class="todo-list">
        <li
          v-for="todo in todos"
          :key="todo"
          :class="{ editing: todo === editingTodo}"
        >
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label @dblclick="editTodo(todo)">{{ todo.text }}label>
            <button class="destroy" @click="remove(todo)">button>
          div>
          <input
            class="edit"
            type="text"
            v-model="todo.text"
            @keyup.enter="doneEdit(todo)"
            @blur="doneEdit(todo)"
            @keyup.esc="cancelEdit(todo)"
          />
        li>
      ul>
    section>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    js

    const useEdit = (remove) => {
      let beforeEditingText = '';
      const editingTodo = ref(null);
      const editTodo = (todo) => {
        beforeEditingText = todo.text;
        editingTodo.value = todo;
      };
    
      const doneEdit = (todo) => {
        if (!editingTodo.value) return;
        todo.text = todo.text.trim();
        todo.text || remove(todo);
        editingTodo.value = null;
      };
    
      const cancelEdit = (todo) => {
        editingTodo.value = null;
        todo.text = beforeEditingText;
      };
      return {
        editingTodo,
        editTodo,
        doneEdit,
        cancelEdit,
      };
    };
    
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
        const { remove } = useRemove(todos);
    
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove),
        };
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    编辑文本框获取焦点

    当你双击文本时,并未自动获取焦点,这时我们需要特殊处理,比如添加自定义指令

    <input
      class="edit"
      type="text"
      v-model="todo.text"
      v-editing-focus="todo === editingTodo"
      @keyup.enter="doneEdit(todo)"
      @blur="doneEdit(todo)"
      @keyup.esc="cancelEdit(todo)"
    />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
        const { remove } = useRemove(todos);
    
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove),
        };
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus();
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    切换待办事项的状态

    • 点击 checkbox,改变所有待办项状态
    • All/Active/Completed
      • 显示未完成待办项个数
      • 如果没有待办项,隐藏 main 和 footer
    • 移除所有完成的项目

    点击 checkbox,改变所有待办项状态

    <section class="main" v-show="count">
      <input id="toggle-all" class="toggle-all" v-model="allDone" type="checkbox" />
      <label for="toggle-all">Mark all as completelabel>
      <ul class="todo-list">
        <li
          v-for="todo in filteredTodos"
          :key="todo"
          :class="{ editing: todo === editingTodo, completed: todo.completed }"
        >
          <div class="view">
            <input class="toggle" type="checkbox" v-model="todo.completed" />
            <label @dblclick="editTodo(todo)">{{ todo.text }}label>
            <button class="destroy" @click="remove(todo)">button>
          div>
          <input
            class="edit"
            type="text"
            v-model="todo.text"
            v-editing-focus="todo === editingTodo"
            @keyup.enter="doneEdit(todo)"
            @blur="doneEdit(todo)"
            @keyup.esc="cancelEdit(todo)"
          />
        li>
      ul>
    section>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    import { computed, ref } from 'vue';
    const useFilter = (todos) => {
      const allDone = computed({
        get() {
          return !todos.value.filter((todo) => !todo.completed).length;
        },
        set(value) {
          todos.value.forEach((todo) => {
            todo.completed = value;
          });
        },
      });
      return {
        allDone,
      };
    };
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
        const { remove } = useRemove(todos);
    
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos),
        };
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus();
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    All/Active/Completed

    • 查看所有、选中、未选中
    • 显示未完成待办项个数
    • 如果没有待办项,隐藏 main 和 footer
    <section class="main" v-show="count">
      <input id="toggle-all" class="toggle-all" v-model="allDone" type="checkbox" />
      <label for="toggle-all">Mark all as completelabel>
      <ul class="todo-list">
        <li
          v-for="todo in filteredTodos"
          :key="todo"
          :class="{ editing: todo === editingTodo, completed: todo.completed }"
        >
          <div class="view">
            <input class="toggle" type="checkbox" v-model="todo.completed" />
            <label @dblclick="editTodo(todo)">{{ todo.text }}label>
            <button class="destroy" @click="remove(todo)">button>
          div>
          <input
            class="edit"
            type="text"
            v-model="todo.text"
            v-editing-focus="todo === editingTodo"
            @keyup.enter="doneEdit(todo)"
            @blur="doneEdit(todo)"
            @keyup.esc="cancelEdit(todo)"
          />
        li>
      ul>
    section>
    <footer class="footer" v-show="count">
      <span class="todo-count">
        <strong>{{ remainingCount }}strong> {{ remainingCount > 1 ? 'items' :
        'item' }} left
      span>
      <ul class="filters">
        <li><a href="#/all">Alla>li>
        <li><a href="#/active">Activea>li>
        <li><a href="#/completed">Completeda>li>
      ul>
    footer>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    import { computed, onMounted, onUnmounted, ref } from 'vue';
    
    const useFilter = (todos) => {
      const allDone = computed({
        get() {
          return !todos.value.filter((todo) => !todo.completed).length;
        },
        set(value) {
          todos.value.forEach((todo) => {
            todo.completed = value;
          });
        },
      });
    
      // 通过计算属性,过滤数据
      const filteredTodos = computed(() => filter[type.value](todos.value));
      // 通过计算属性,获取选中事项的数量
      const remainingCount = computed(() => filter.active(todos.value).length);
      // 通过计算属性,获取所有事项的数量
      const count = computed(() => todos.value.length);
    
      const onHashChange = () => {
        const hash = window.location.hash.replace('#/', '');
        if (filter[hash]) {
          type.value = hash;
        } else {
          type.value = 'all';
          window.location.hash = '';
        }
        console.log(type.value);
      };
    
      // 添加监听hash改变的事件
      onMounted(() => {
        window.addEventListener('hashchange', onHashChange);
        onHashChange();
      });
    
      // 移除监听hash改变的事件
      onUnmounted(() => {
        window.removeEventListener('hashchange', onHashChange);
      });
    
      return {
        allDone,
        count,
        filteredTodos,
        remainingCount,
      };
    };
    
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
        const { remove } = useRemove(todos);
    
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos),
        };
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus();
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    移除所有完成的项目

    <footer class="footer" v-show="count">
      <span class="todo-count">
        <strong>{{ remainingCount }}strong> {{ remainingCount > 1 ? 'items' :
        'item' }} left
      span>
      <ul class="filters">
        <li><a href="#/all">Alla>li>
        <li><a href="#/active">Activea>li>
        <li><a href="#/completed">Completeda>li>
      ul>
      <button
        class="clear-completed"
        @click="removeCompleted"
        v-show="count > remainingCount"
      >
        Clear completed
      button>
    footer>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    const useRemove = (todos) => {
      const remove = (todo) => {
        const index = todos.value.indexOf(todo);
        todos.value.splice(index, 1);
      };
      const removeCompleted = () => {
        todos.value = todos.value.filter((todo) => !todo.completed);
      };
      return {
        remove,
        removeCompleted,
      };
    };
    export default {
      name: 'App',
      setup() {
        const todos = ref([]);
        const { remove, removeCompleted } = useRemove(todos);
    
        return {
          todos,
          remove,
          removeCompleted,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos),
        };
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus();
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    存储待办事项

    src/utils/useLocalStorage.js

    function parse(str) {
      let value;
      try {
        value = JSON.parse(str);
      } catch {
        value = null;
      }
      return value;
    }
    
    function stringify(obj) {
      let value;
      try {
        value = JSON.stringify(obj);
      } catch {
        value = null;
      }
      return value;
    }
    
    export default function useLocalStorage() {
      function setItem(key, value) {
        value = stringify(value);
        window.localStorage.setItem(key, value);
      }
    
      function getItem(key) {
        let value = window.localStorage.getItem(key);
        if (value) {
          value = parse(value);
        }
        return value;
      }
    
      return {
        setItem,
        getItem,
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue';
    import useLocalStorage from './utils/useLocalStorage';
    const storage = useLocalStorage();
    
    // 5. 存储待办事项
    const useStorage = () => {
      const KEY = 'TODOKEYS';
      const todos = ref(storage.getItem(KEY) || []);
      watchEffect(() => {
        storage.setItem(KEY, todos.value);
      });
      return todos;
    };
    export default {
      name: 'App',
      setup() {
        const todos = useStorage();
        const { remove, removeCompleted } = useRemove(todos);
    
        return {
          todos,
          remove,
          removeCompleted,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos),
        };
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus();
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    源码

  • 相关阅读:
    Scala基础【异常、隐式转换、泛型】
    自动驾驶——基础架构
    AVR单片机开发11——1602液晶屏幕
    SpringCloud相关
    Python也能在web界面写爬虫了
    c++ virtual关键字
    Xenomai 再探
    Read Committed
    基于SpringBoot的电影购票系统
    CFAR检测MATLAB仿真
  • 原文地址:https://blog.csdn.net/qq_32090185/article/details/126467621