Vue2(八):TodoList案例(初级版)

一、整体思路

在这里插入图片描述

1.分析结构

确定组件名称(Header,List,Item,Footer)和个数,还有嵌套关系(List里面包含Iterm,List和Hello\Footer为兄弟组件),然后引入相应的组件

2.拆html和css

分别放到对应的组件里面,style标签中加上scoped属性(保证当前样式只在当前组件中使用,防止样式名字不能重复使用)

3.初始化列表

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

<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"></MyItem>

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

4.实现添加列表功能

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

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

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

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

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

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

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

在这里插入图片描述

5.实现勾选功能

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

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

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

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

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

this.todo.done = !this.todo.done;   

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

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

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

这里只是没有特别复杂的需求,看不出问题,如果有一天,我当前组件就想用最新的值,但是其他组件就想保存原始值,然后你某个组件改了props,动了原数据,这不就乱套了吗tmlgb

6.实现删除功能

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

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去操作

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

7.实现底部统计功能

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

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

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

写法1:forEach遍历

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

写法2:for of遍历

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

写法3:filter过滤

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

写法4:ES6的奇葩写法reduce
第一个参数相当于个计数器?,第二个参数是当前数组元素
有几个元素,这个函数就执行几次,每次返回值都会作为下一次的pre

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" " />
isAll(){
  return this.total === this.doneTotal && this.total !== 0;
  }

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

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

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

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

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

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

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

handleCheckAll(e) {
            // this.checkAllTodos(!this.isAll);  这么写容易晕
            this.checkAllTodos(e.target.checked);  //直接拿dom的值比较合适
        },

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

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

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)两种情况

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

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

这个比较简单,在App中定义一个方法并传给Footer

//5.删除已完成任务,不用传参,直接让那边调用就行了
deleteDone() {
    if (confirm('确定要清除所有已完成任务吗?'))
        this.todos = this.todos.filter((todo) => {
            return todo.done !== true;
        })
}

Footer中调用这个方法,就欧了

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

二、代码文件总览

1.App.vue

<template>
    <div class="todo-container">
        <div class="todo-wrap">
            <MyHeader :addTodo="addTodo"></MyHeader>
            <MyList :todos="todos" :changeTodo="changeTodo" :deleteTodo="deleteTodo"></MyList>
            <!-- 通过App把todos传给List -->
            <MyFooter :todos="todos" :checkAllTodos="checkAllTodos" :deleteTodo="deleteTodo" :deleteDone="deleteDone">
            </MyFooter>
        </div>
    </div>
</template>

<script>
import MyHeader from './compoments/MyHeader.vue';
import MyList from './compoments/MyList.vue';
import MyFooter from './compoments/MyFooter.vue';

export default {
    name: 'App',
    data() {
        return {
            todos: [
                //通过App把todos传给List
                //id不用数值是因为数值是有尽头的,而字符串没有尽头
                { id: '001', title: '抽烟', done: true },
                { id: '002', title: '喝酒', done: false },
                { id: '003', title: '烫头', done: true }
            ]
        };
    },
    components: {
        MyHeader,
        MyList,
        MyFooter
    },
    //记住,数据源在哪里,修改数据的方法就配置在哪里
    // 不要在任何子组件中修改父组件的数据
    methods: {
        //1.添加一个todo
        addTodo(x) {
            //借助这个函数,拿到Header中用户输入的东西
            this.todos.unshift(x);
        },
        //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;
            })
            // this.todos = this.todos.filter(todo => todo.id !== id);
        },
        //4.全选or取消全选,其他子框同步
        checkAllTodos(isDone) {
            this.todos.forEach(todo => todo.done = isDone);
        },
        //5.删除已完成任务,不用传参,直接让那边调用就行了
        deleteDone() {
            if (confirm('确定要清除所有已完成任务吗?'))
                this.todos = this.todos.filter((todo) => {
                    return todo.done !== true;
                })
        }
    },
}
</script>

2.MyHeader.vue

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

<script>
//借助一个包生成唯一的id
import { nanoid } from 'nanoid'
export default {
    name: 'MyHeader',
    data() {
        return {
            title: ''
        }
    },
    methods: {
        add(e) {
            // console.log(e.target.value);  //利用事件对象获取用户输入的内容
            // console.log(this.title);  //利用双向数据绑定获取用户输入的内容

            //1.校验数据
            if (!this.title.trim()) return alert('不能输入空值!');   //输入空值跳出函数,否则继续往下执行
            //2.将用户的输入包装成一个todoObj对象
            const todoObj = { id: nanoid(), title: this.title, done: false };
            this.addTodo(todoObj);//3.借助App里的函数,把用户的输入从Header传给App里面的todos
            //然后再通过App把todos传给List,用这种捷径(借助App)就巧妙地实现了Header和List的兄弟组件通信
            this.title = '';  //4.输入完回车输入框变为空
        }
    },
    props: ['addTodo']
}
</script>

3.MyList.vue

<template>
    <ul class="todo-main">
        <!-- 传值的时候注意,如果传的是表达式,一定要加上v-bind,简写冒号 -->
        <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :changeTodo="changeTodo"
            :deleteTodo="deleteTodo"></MyItem>
    </ul>
</template>

<script>
import MyItem from './MyItem.vue';
export default {
    name: "MyList",
    components: { MyItem },
    props: ['todos', 'changeTodo', 'deleteTodo']
}
</script>

4.MyItem.vue

<template>
    <li>
        <label>
            <!-- 卧槽,这块儿写的东西有点妙 -->
            <input type="checkbox" :checked="todo.done" @click="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: 'MyItem',
    //把父组件List中的对象传到vc身上来(vc.todo)
    props: ['todo', 'changeTodo', 'deleteTodo'],
    methods: {
        handleChange(id) {
            // 不能像下面这样直接修改props的数据,数据源在哪里,我们就去哪里改
            // 如下代码也能实现功能,但是不建议这么写,理由就是不能直接改props传来的东西
            // this.todo.done = !this.todo.done;   
            //不报错是因为,改的不是整个对象(对象的地址),地址没变就不会报错

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

5.MyFooter.vue

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

<script>
export default {
    name: 'MyFooter',
    props: ['todos', 'checkAllTodos', 'deleteTodo', 'deleteDone'],
    computed: {
        total() {
            return this.todos.length;
        },
        doneTotal() {
            //写法1
            // let count = 0;
            // //for in 拿到的是数组的索引,for of才能拿到元素
            // for (let todo of this.todos) {
            //     if (todo.done === true)
            //         count++;
            // }
            // return count;

            //写法2
            // let count = 0;
            // this.todos.forEach(todo => {
            //     if (todo.done) count++;
            // });
            // return count;

            //写法3
            // let count = this.todos.filter(todo => todo.done)
            // return count.length;

            //写法4,ES6的奇葩写法
            //第一个参数相当于个计数器?,第二个参数是当前数组元素
            //有几个元素,这个函数就执行几次,每次返回值都会作为下一次的pre
            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);
        },
        //用前两个计算属性套娃
        isAll: {
            get() {
                return this.total === this.doneTotal && this.total !== 0;
            },
            set(val) {
                this.checkAllTodos(val);
            }
        },
    },
    methods: {
        handleCheckAll(e) {
            // this.checkAllTodos(!this.isAll);  这么写容易晕
            this.checkAllTodos(e.target.checked);  //直接拿dom的值比较合适
        },
        clearDone() {
            this.deleteDone(); //点击调用App里边的方法,把所有true删掉
        }
    },
}
</script>

三、总结

1.组件化编码流程:

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

一个组件在用:放在组件自身即可。
一些组件在用:放在他们共同的父组件上(状态提升)。
(3)实现交互:从绑定事件开始。

2.props适用于:

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

3.使用v-model时要切记

v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

4.关于props

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

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以参考以下代码实现一个简单的 Vue TodoList: ``` <template> <div class="todo-list"> <h1>Vue TodoList</h1> <form @submit.prevent="addItem"> <input type="text" v-model="newItem" placeholder="Add item..."> <button type="submit">Add</button> </form> <ul> <li v-for="(item, index) in items" :key="index"> <span>{{ item }}</span> <button @click="deleteItem(index)">Delete</button> </li> </ul> </div> </template> <script> export default { data() { return { newItem: '', items: [] } }, methods: { addItem() { if (this.newItem !== '') { this.items.push(this.newItem); this.newItem = ''; } }, deleteItem(index) { this.items.splice(index, 1); } } } </script> <style> .todo-list { margin: 0 auto; max-width: 600px; } ul { list-style: none; margin: 0; padding: 0; } li { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 5px; } button { background-color: #f44336; color: #fff; border: none; border-radius: 5px; padding: 5px 10px; cursor: pointer; } button:hover { background-color: #d32f2f; } input[type="text"] { padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-right: 10px; font-size: 16px; } </style> ``` 在这个 TodoList 中,我们使用了 `v-model` 指令绑定 `newItem` 变量,通过 `addItem` 方法向 `items` 数组中添加新的项目,并使用 `v-for` 指令渲染出每个项目。同时,在每个项目中,我们添加了一个 `Delete` 按钮,用于删除对应的项目。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值