Vue2-TodoList案例

news/2024/7/10 22:56:04 标签: javascript, 开发语言, 前端, es6, vue.js

TodoList案例

    • 组件化编码流程(通用)
    • 整体思路
      • 1、分析结构
      • 2、拆html和css
      • 3、初始化列表
      • 4、实现添加列表功能
      • 5、实现勾选功能
      • 6、实现删除功能
      • 7、实现底部统计功能
      • 8、实现全选框的交互
        • (1)每个todo控制全选框
        • (2)全选框控制todo全选或者取消全选:使用点击事件实现。
        • (3)全选框控制todo全选或者取消全选:使用v-model实现。
      • 9、点击右下角按钮删除全部已完成任务
    • 案例完整代码展示
      • 1、App.vue
      • 2、TodolistHeader.vue
      • 3、TodolistList.vue
      • 4、TodolistItem.vue
      • 5、TodolistFooter.vue
    • 三、总结

组件化编码流程(通用)

  1. 实现静态组件:抽取组件,使用组件实现静态页面效果

  2. 展示动态数据:

    ①数据的类型、名称是什么?

    ②数据保存在哪个组件?

  3. 交互——从绑定事件监听开始

整体思路

TodoList类似于我们手机里的待办事项或者备忘录

请添加图片描述

1、分析结构

确定组件名称TodolistHeader,TodolistList,TodolistItem,TodolistFooter(下文会简写成Header、List、Item、Footer)和个数,还有嵌套关系(TodolistList里面包含TodolistItem,TodolistList和TodolistHello\TodolistFooter为兄弟组件),然后引入相应的组件。

  • 一个命名问题:此处命名不直接用Header、Footer等简单的命名是因为这些命名本身在标签具有特殊的含义(类似于关键字),并且Vue推荐我们使用多单词命名。为了达到和Vue开发者工具里的名字一样,这里使用的“大驼峰”命名(在驼峰命名的基础上首字母大写)。

请添加图片描述

请添加图片描述

2、拆html和css

一般的项目是写好html和css的,我们要把他们拆到相应的组件里,可以先全部放到App.vue中,然后打开开发者工具,观察他们的结构,分别放到对应的组件里面,style标签中加上scoped属性(保证当前样式只在当前组件中使用,防止样式名字不能重复使用)

3、初始化列表

在List中定义一个数组todos存储每个item为对象,然后在List的标签中写<Item></Item>并用v-for遍历数组中的每个对象生成结构,且通过自定义属性todo把每个对象传给子组件Item

在Item中使用props配置项接收(实现父子组件通信),然后input框里的checked绑定todo.done(可以初始化页面)

props配置项不熟悉的可以点击此处复习props

4、实现添加列表功能

上一步我们是在List中定义了todos数组,而用户的输入是在Header中,也就是说我们要在Header中收集数据,然后传给List,但是兄弟组件如何通信?全局事件总线、消息订阅与发布、Vuex都可以,但是现在还没学。

之前我们学的props配置项,可以实现父组件给子组件传数据(父组件里写子组件标签并配置属性,子组件使用props接收),现在我们想实现兄弟组件HeaderList的通信,可以借助App这个共同的父亲。

(1)把List中的数据todos定义在App里,然后传给List一份(通过props接收,由于是直接传到vc上,模板不会报错)。
(2)在App组件中定义一个函数,函数里面写个参数

javascript">	addTodo(x) {
            //借助这个函数,拿到Header中用户输入的东西
            this.todos.unshift(x);
       }

(3)把这个函数传给HeaderHeaderprops接收一下,然后在Header中调用这个函数,把用户的输入传给这个函数

javascript">const todoObj = { id: nanoid(), title: this.title, done: false };
this.addTodo(todoObj);

(4)由于addTodo是定义在App上的,所以App就直接拿到了函数的参数,然后就可以直接添加在todos中,这样的话todos改变,模板重新解析,传给Listtodos也改变,List模板重新解析,v-for一遍历,页面就多了一个。

5、实现勾选功能

选中:done=true,不选中:done=false。思路:简单来说就是让App拿到要修改的数据的id,找到这个数据然后把done属性取反。

我们先在App组件里定义一个函数,用来接收当前操作对象的id,然后函数里的逻辑是找到这个id,然后done属性取反(记住,数据源在哪里,修改数据的方法就写在哪里

javascript">changeTodo(id) {
            this.todos.forEach((todo) => {
                if (todo.id === id) todo.done = !todo.done;
            })
        }

然后把changeTodo这个函数通过标签和props传给List,再传给Item
然后在Item组件里定义一个函数handleChange,用来获取当前操作的多选框的id

javascript">handleChange(id) {
            // 不能像下面这样直接修改props的数据,数据源在哪里,我们就去哪里改
            // 如下代码也能实现功能,但是不建议这么写,理由就是不能直接改props传来的东西
            // this.todo.done = !this.todo.done;   
            //不报错是因为,改的不是整个对象(对象的地址),地址没变就不会报错

            //通知App要修改的数据的id是哪个
            this.changeTodo(id);
        }

handleChange函数通过点击事件来触发,实参就是当前input节点的todo的id

<input type="checkbox" :checked="todo.done" @click="handleChange(todo.id)" />

当然,这个勾选功能不用这么绕来绕去也可以实现,比如我直接在handleChange

javascript">this.todo.done = !this.todo.done;   

又或者我用v-model,一行代码就搞定了,不用在App再定义函数,然后传来传去

<input type="checkbox" v-model="todo.done" />

但是不建议这么写,理由就是不能直接改props传来的东西,记住,数据源在哪里,修改数据的方法就配置在哪里,千万记住props是只读的,如果改它,就会改源数据,这样所有用到数据的组件都会受到影响。

6、实现删除功能

和上面的类似,也是需要传递函数,不能动props。
首先App定义一个删除函数

javascript">deleteTodo(id) {
            this.todos = this.todos.filter((todo) => {
                return todo.id !== id;
            })
            // this.todos = this.todos.filter(todo => todo.id !== id);
        }

把这个函数传给List,再传给Item
Item中的按钮来个点击事件

<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>

把当前input的这个id传给App,调用App中的deleteTodo函数让App去操作

javascript">handleDelete(id) {
    if (confirm('确定要删除吗?'))  //点确定是true,取消是false
        this.deleteTodo(id);
}

7、实现底部统计功能

(1)App中的todos数据传给Footer一份
(2)全部比较简单,直接插值语法读数组todos的长度就行。已完成这里可以用计算属性

<span>已完成{{ doneTotal }}</span> / 全部{{ todos.length }}

(3)计算属性这里有多种实现方式:

写法1:forEach遍历

javascript">let count = 0;
this.todos.forEach(todo => {
    if (todo.done) count++;
});
return count;

写法2:for of遍历

javascript">let count = 0;
//for in 拿到的是数组的索引,for of才能拿到元素
for (let todo of this.todos) {
    if (todo.done === true)
        count++;
}
return count;

写法3:filter过滤

javascript">let count = this.todos.filter(todo => todo.done)
return count.length;

写法4:ES6的reduce

  • 对reduce不熟悉的可以点击此处复习reduce(点击目录里的数组核心方法即可)
javascript">const count = this.todos.reduce((pre, todo) => {
    return pre + (todo.done ? 1 : 0)
}, 0);
return count;
//return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);

8、实现全选框的交互

(1)每个todo控制全选框

当已完成和全部相等且不等于0时,勾选全选框,写在计算属性中

<input type="checkbox" :checked="isAll" " />
javascript">isAll(){
  return this.total === this.doneTotal && this.total !== 0;
  }

如果全部是0个,就隐藏Footer(用v-show实现)

(2)全选框控制todo全选或者取消全选:使用点击事件实现。

App中定义一个函数,用来接收Footer中全选框的状态值,然后把每个数据的done值都改成和全选框的状态值一样(选择or不选择)

javascript">checkAllTodos(isDone) {
            this.todos.forEach(todo => todo.done = isDone);
        },

Footer的全选框中,加入点击事件并定义一个函数

<input type="checkbox" :checked="isAll" @click="handleCheckAll" />

在函数中调用App中的函数,把状态传过去

javascript">handleCheckAll(e) {
            // this.checkAllTodos(!this.isAll);  
            this.checkAllTodos(e.target.checked);  //直接拿dom的值比较合适
        },

(3)全选框控制todo全选或者取消全选:使用v-model实现。

第一步一样,在App中定义一个函数,用来接收Footer中全选框的状态值,然后把每个数据的done值都改成和全选框的状态值一样(选择or不选择)

javascript">checkAllTodos(isDone) {
            this.todos.forEach(todo => todo.done = isDone);
        },

在Footer的全选框中,加入v-model(没有value绑定的是checked)绑定isAll计算属性

<input type="checkbox" v-model="isAll" />

isAll中加入setter,把每个更新的值传给App中的函数。这样就不用再另外定义methods了,只用计算属性就能搞定:item影响全选框(getter读isAll)全选框影响item(setter改isAll)两种情况

javascript">isAll: {
            get() {
                return this.total === this.doneTotal && this.total !== 0;
            },
            set(val) {
                this.checkAllTodos(val);
            }
        },

9、点击右下角按钮删除全部已完成任务

App中定义一个方法并传给Footer

javascript">//5.删除已完成任务
clearAllTodo() {
      this.todos = this.todos.filter((todo) => {
        return !todo.done
      })
    },

Footer中调用这个方法

<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
javascript">clearAll() {
      this.clearAllTodo()//点击调用App里边的方法,把所有true删掉
    },

案例完整代码展示

1、App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <TodolistHeader :addTodo="addTodo" />
        <TodolistList
          :todos="todos"
          :changeTodo="changeTodo"
          :deleteTodo="deleteTodo"
        />
        <TodolistFooter
          :todos="todos"
          :checkAllTodo="checkAllTodo"
          :clearAllTodo="clearAllTodo"
        />
      </div>
    </div>
  </div>
</template>

<script>
import TodolistHeader from './components/TodolistHeader.vue'
import TodolistFooter from './components/TodolistFooter.vue'
import TodolistList from './components/TodolistList.vue'
export default {
  name: 'App',
  components: { TodolistHeader, TodolistFooter, TodolistList },
  data() {
    return {
      todos: [
        //通过App把todos传给List
        //id不用数值是因为数值是有尽头的,而字符串没有尽头
        { id: '001', title: '吃饭', done: true },
        { id: '002', title: '睡觉', done: true },
        { id: '003', title: '打豆豆', done: true },
      ],
    }
  },
  //记住,数据源在哪里,修改数据的方法就配置在哪里
  //最好不要在任何子组件中修改父组件的数据
  methods: {
    //1.添加一个todo
    addTodo(todoObj) {
      //借助这个函数,拿到Header中用户输入的东西
      this.todos.unshift(todoObj)
    },
    //2.勾选or取消勾选一个todo
    changeTodo(id) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.done = !todo.done
      })
    },
    //3.删除一个todo
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    //4.全选or取消全选,其他子框同步
    checkAllTodo(done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    //5.删除已完成任务
    clearAllTodo() {
      this.todos = this.todos.filter((todo) => {
        return !todo.done
      })
    },
  },
}
</script>

<style>
/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

2、TodolistHeader.vue

<template>
  <div class="todo-header">
    <input
      type="text"
      placeholder="请输入你的任务名称,按回车键确认"
      v-model="title"
      @keyup.enter="add"
    />
  </div>
</template>

<script>
import { nanoid } from 'nanoid'
export default {
  name: 'TodolistHeader',
  data() {
    return {
      title: '',
    }
  },
  props: ['addTodo'],
  methods: {
    // 输入内容按回车添加一个事件
    add() {
      //1.校验数据
      if (!this.title.trim()) return alert('输入不能为空')
      //2.将用户的输入包装成一个todo对象
      const todoObj = { id: nanoid(), title: this.title, done: false }
      //3.借助App里的函数,把用户的输入从Header传给App里面的todos 然后再通过App把todos传给List,用这种捷径(借助App)就巧妙地实现了Header和List的兄弟组件通信
      this.addTodo(todoObj)
      this.title = '' //4.输入完回车输入框变为空
    },
  },
}
</script>

<style scoped>
/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    0 0 8px rgba(82, 168, 236, 0.6);
}
</style>

3、TodolistList.vue

<template>
  <ul class="todo-main">
    <TodolistItem
      v-for="todoObj in todos"
      :key="todoObj.id"
      :todo="todoObj"
      :changeTodo="changeTodo"
      :deleteTodo="deleteTodo"
    />
  </ul>
</template>

<script>
import TodolistItem from './TodolistItem.vue'
export default {
  name: 'TodolistList',
  components: { TodolistItem },
  props: ['todos', 'changeTodo', 'deleteTodo'],
}
</script>

<style scoped>
/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>

4、TodolistItem.vue

<template>
  <li>
    <label>
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleChange(todo.id)"
      />
      <!-- 如下代码也能实现功能,但是不建议这么写,理由就是不能直接改props传来的东西 -->
      <!-- <input type="checkbox" v-model="todo.done" /> -->
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  name: 'TodolistItem',
  //声明接收todo对象
  props: ['todo', 'changeTodo', 'deleteTodo'],
  methods: {
    handleChange(id) {
      // 不能像下面这样直接修改props的数据,数据源在哪里,我们就去哪里改
      // 如下代码也能实现功能,但是不建议这么写,理由就是不能直接改props传来的东西
      // this.todo.done = !this.todo.done;
      //不报错是因为,改的不是整个对象(对象的地址),地址没变就不会报错

      //通知App要修改的数据的id是哪个
      this.changeTodo(id)
    },
    handleDelete(id) {
      //confirm点确定是true,取消是false
      if (confirm('确定删除这个事项吗?')) this.deleteTodo(id)
    },
  },
}
</script>

<style scoped>
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

li:hover {
  background-color: #ddd;
}

li:hover button {
  display: block;
}
</style>

5、TodolistFooter.vue

<template>
  <!-- 这里total如果显示是0(false)就隐藏(v-show)-->
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" :checked="isAll" @change="checkAll" />
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'TodolistFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    total() {
      return this.todos.length
    },
    doneTotal() {
      return this.todos.reduce(
        (pre, current) => pre + (current.done ? 1 : 0),
        0
      )
    },
    isAll() {
      return this.doneTotal === this.total && this.total > 0
    },
  },
  methods: {
    checkAll(e) {
      this.checkAllTodo(e.target.checked)
    },
    clearAll() {
      this.clearAllTodo() //点击调用App里边的方法,把所有为true的删掉
    },
  },
}
</script>

<style scoped>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}
</style>

三、总结

1.组件化编码流程:
(1)拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
(2)实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

  • 一个组件在用:放在组件自身即可。
  • 一些组件在用:放在他们共同的父组件上(状态提升)。

(3)实现交互:从绑定事件开始。

2.props适用于:
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)

3.使用v-model时要切记
v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

4.关于props
props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。


http://www.niftyadmin.cn/n/4944690.html

相关文章

Webpack和Parcel详解

构建工具和打包器是在开发过程中帮助组织、优化和打包项目的工具。它们可以处理依赖管理、资源优化、代码转换等任务&#xff0c;从而使开发流程更高效。以下是关于构建工具和打包器的一些指导&#xff1a; **Webpack&#xff1a;** Webpack 是一个功能强大的模块打包器&#…

如何使用Python编写小游戏?

大家好&#xff0c;我是沐尘而生&#xff0c;如果你是一个热爱编程的小伙伴&#xff0c;又想尝试游戏开发&#xff0c;那么这篇文章一定能满足你的好奇心。不废话&#xff0c;让我们马上进入Python游戏开发的精彩世界吧&#xff01; Python游戏开发的魅力 编写小游戏不仅仅是锻…

LLM - 大模型评估指标之 BLEU

目录 一.引言 二.BLEU 简介 1.Simple BLEU 2.Modified BLEU 3.Modified n-gram precision 4.Sentence brevity penalty 三.BLEU 计算 1.计算句子与单个 reference 2.计算句子与多个 reference 四.总结 一.引言 机器翻译的人工评价广泛而昂贵&#xff0c;且人工评估可…

Oracle外部表ORACLE_LOADER方式加载数据

当数据源为文本或其它csv文件时&#xff0c;oracle可通过使用外部表加载数据方式&#xff0c;不需要导入可直接查询文件内的数据。 1、如下有一个文件名为&#xff1a;test1.txt 的数据文件。数据文件内容为&#xff1a; 2、使用sys授权hr用户可读写 DATA_PUMP_DIR 目录权限&a…

vue2+Spring Boot2.7 大文件分片上传

之前我们文章 手把手带大家实现 vue2Spring Boot2.7 文件上传功能 将了上传文件 但如果文件很大 就不太好处理了 按正常情况甚至因为超量而报错 这里 我弄了个足够大的文件 我们先搭建 Spring Boot2.7 环境 首先 application.yml 代码编写如下 server:port: 80 upload:path:…

快速搭建图书商城小程序的简易流程与优势

很多人喜欢阅读电子书&#xff0c;又有很多人依旧喜欢实体书&#xff0c;而实体书店拥有一个图书商城小程序便成为了满足用户需求的理想选择。如果您也想进入这一充满潜力的领域&#xff0c;但担心开发难度和复杂流程&#xff0c;别担心&#xff01;您能做到快速搭建一个专业、…

Flink SQL TopN

Flink SQL 对于批处理&#xff08;Batch&#xff09;和流处理&#xff08;streaming&#xff09;模式的SQL&#xff0c;都支持 Top-N 查询。Top-N 查询可以根据指定列排序后获得前 N 个最小或最大值。并且该结果集还可用于进一步分析。Flink 使用 OVER 窗口子句和过滤条件的组合…

解决一直提示No module named “Crypto” 解决方案

今天跑脚本发现一直提示装Crypto包 已经装好了情况下还是这样&#xff1a; 解决方法&#xff1a; pip uninstall crypto pycryptodome pip install pycryptodome pycrypto和crypto是同一个库&#xff0c;crypto在 python 中又被称为pycrypto&#xff0c;它是一个第三方库&…