Wu_Candy

V1

2022/06/28阅读:30主题:默认主题

200行纯前端代码!教你写一个专属TodoList(零基础友好)

1.前言

身为互联网农民工的我们,提起 Todo List 大家肯定一点都不陌生,因为Todo List是一个圈内比较知名的案例,可以通过 Java, Python 等后端语言再以少量的前端 Html 语言辅助实现整个 Todo List 的增、删、改、查功能。

今天分享的 Todo List 案例与常见的实现方式不太一样,因为今天分享的案例是由纯前端代码 Vue 组件化来实现的,完全没有后端语言的支撑,也能实现Todo List功能的动态效果。

Vue.js 是什么

这是 Vue 官网对 Vue 的介绍,感兴趣的小伙伴可以去 Vue 官网(https://cn.vuejs.org/v2/guide/)了解更多。

2.动态效果图

【操作视频】

3.框架结构

下图是实现效果图:

从效果图分析进行组件结构拆分,如下图所示。

效果图与组件结构图之间的序号是一一对应的,可以对比查看便于更好地理解。

(1). App.vue组件: 所有组件的父组件

(2). ListHeader.vue组件: 管理 input 输入框模块

(3). AllList.vue组件: 负责展示所有待办事项,即每一条 item

(4). ListItem.vue组件: 管理每一条item的具体内容及 checkbox 的勾选状态

(5). ListFooter.vue组件: 负责展示已完成和所有待办事项总数及删除所有已完成状态的 item 功能

4.Vue 代码实现

(1). AllList.vue组件
<template>
    <ul class="todo-main">
        <!--给子组件ListItem绑定事件,例如:todo,checkTodo,在ListItem组件中需要使用props进行声明接收后即可使用-->
        <ListItem
            v-for="todoObj in todos"
            :key="todoObj.id"
            :todo="todoObj"
            :checkTodo="checkTodo"
            :deleteTodo="deleteTodo"
        />

    </ul>
</template>

<script>
    //引入ListItem组件
    import ListItem from "./ListItem"
    export default {
        //给组件命名为AllList
        name:'AllList',
        //在当前组件内注册子组件: ListItem
        components:{ListItem},
        //props用于接收App组件传递过来的参数
        props:['todos','checkTodo','deleteTodo']
    }
</script>

<style scoped>
    .todo-main {
        margin-left0px;
        border1px solid #dddddd;
        border-radius2px;
        padding0px;
    }

    .todo-empty {
        height40px;
        line-height40px;
        border1px solid #dddddd;
        border-radius2px;
        padding-left5px;
        margin-top10px;
    }
</style>
(2). ListFooter.vue组件
<template>
    <div class="todo-footer" v-show="total">
        <label>
            <!-- v-model表示双向绑定,页面操作控制底层数据,底层数据变更影响页面展示 -->
            <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
            <span>已完成:{{doneTotal}}</span> /全部:{{total}}
        </span>
        <!-- 绑定click事件调用clearAll方法用于删除已完成任务 -->
        <button class="btn btn-danger" @click="clearAll">删除已完成任务</button>
    </div>
</template>

<script>
    export default {
        //给当前组件命名为: ListFooter
        name:'ListFooter',
        //使用props进行声明接收父组件App里的数据与方法
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
            total(){
                //返回整个todo列表的长度
                return this.todos.length
            },
            doneTotal(){
                //统计list列表中当前状态下done:true的总数和
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1:0),0)
            },
            isAll: {
                //当所有item的done为true时,即所有事项已完成且当前列表至少有1条item,则默认自动勾选中底部的checkbox
                get(){
                    return this.doneTotal === this.total && this.total > 0
                },
                //获取底部的checkbox是选中还是未选中,然后给App里的每条item的done字段进行赋值为:true或false(以此控制每条item是选中还是未选中)
                set(value){
                    this.checkAllTodo(value)
                }
            }
        },
        methods:{
            //回调父组件App提供的删除所有已完成待办事项的方法,具体实现在App组件的clearAllTodo方法里
            clearAll(){
                this.clearAllTodo()
            }
        }
    }
</script>

<style scoped>
    .todo-footer {
        height40px;
        line-height40px;
        padding-left6px;
        margin-top5px;
    }

    .todo-footer label {
        display: inline-block;
        margin-right20px;
        cursor: pointer;
    }

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

    .todo-footer button {
        float: right;
        margin-top5px;
    }
</style>
(3). ListHeader.vue组件
<template>
    <div class="todo-header">
        <h2>今日待办事项</h2>
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model='title' @keyup.enter="add"/>
    </div>
</template>

<script>
    //引入nanoid库用于每条item添加时的id值
    import {nanoid} from 'nanoid'
    export default {
        //给当前组件命名为: ListHeader
        name:'ListHeader',
        //使用props声明接收父组件App里的addTodo方法
        props:['addTodo'],
        data(){
            return {
                title''
            }
        },
        methods: {
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(), title:this.title, done:false}
                //通知App组件去添加一个todo对象
                this.addTodo(todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</script>

<style scoped>
    .todo-header input {
        width560px;
        height28px;
        font-size14px;
        border1px solid #cccccc;
        border-radius4px;
        padding4px 7px;
    }

    .todo-header input:focus {
        outline: none;
        border-colorrgba(821682360.8);
        box-shadow: inset 0 1px 1px rgba(0000.075), 0 0 8px rgba(821682360.8);
    }
</style>
(4). ListItem.vue组件
<template>
    <li>
        <label>
            <!--
            1.初始化时: 绑定checked事件,如果为true时则勾选中checkbox,反之不勾选中checkbox
            2.数据变更时: 绑定change事件,调用handleCheck方法并传入当前item的所属id值来操作是取消还是选中checkbox的勾选
            -->

            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
        </label>
        <!-- 给每条item的删除按钮绑定click点击事件,通知App组件中将对应的todo进行删除操作 -->
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default{
        //给当前组件命名为: ListItem
        name:'ListItem',
        //使用props声明接收父组件AllList里的todo对象和App组件里的checkTodo,deleteTodo方法
        props:['todo','checkTodo','deleteTodo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id) {
                //通知App组件将对应的todo对象的done值取反
                this.checkTodo(id)
            },
            //删除
            handleDelete(id){
                if(confirm('确认删除吗?')){
                    //通知App组件将对应的todo对象删除掉
                    this.deleteTodo(id)
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height36px;
        line-height36px;
        padding0 5px;
        border-bottom1px solid #dddddd;
    }

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

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

    li button {
        float: right;
        display: none;
        margin-top3px;
    }

    li:before {
        content: initial;
    }

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

    li:hover {
        background-color: lightskyblue;
    }

    li:hover button {
        display: block;
    }
</style>
(5). App.vue组件
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <!--
                1.给ListHeader,AllList,ListFooter子组件绑定属性
                2.在子组件中使用props进行声明接收就可以调用本组件定义的方法和数据了
                -->

                <ListHeader :addTodo="addTodo"/>
                <AllList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
                <ListFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    //引入子组件
    import ListHeader from "./components/ListHeader"
    import AllList from "./components/AllList"
    import ListFooter from "./components/ListFooter"

    export default {
        //给当前组件命名为:App
        name:'App',
        //注册引入的子组件
        components:{ListHeader, AllList, ListFooter},
        //初始化的todo list数据
        data(){
            return {
                todos:[
                    {id:'001'title:'读文学书'done:true},
                    {id:'002'title:'解函数'done:false},
                    {id:'003'title:'上瑜珈课'done:false},
                    {id:'004'title:'研究三角函数'done:false},

                ]
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                //精简写法
                this.todos = this.todos.filter( todo => todo.id !== id )
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已经完成的todo,保留done为false的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            }
        }
    }
</script>

<style>
    body {
        background#ffffff;
    }

    .btn {
        display: inline-block;
        padding4px 12px;
        margin-bottom0;
        font-size14px;
        line-height20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(2552552550.2), 0 1px 2px rgba(0000);
        border-radius4px;
    }

    .btn-danger {
        color#ffffff;
        background-color#da4f49;
        border1px solid #bd362f;
    }

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

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width600px;
        margin0 auto;
    }

    .todo-container .todo-wrap {
        padding10px;
        border1px solid #dddddd;
        border-radius5px;
    }
</style>
(6). main.js入口文件
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
    el:'#app',
    renderh => h(App)
})
(7). index.html网页展示文件
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 开启移动端的理想视口 -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- 配置页签图标 -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- 配置网页标题 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!-- 容器 -->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>

</html>

这个文件中的核心代码只有一行(倒数第3行):

 <div id="app"></div>

5.总结

(1).以上7个文件的源码都有对代码实现功能进行详细的解释说明,请注意查看。

(2).使用 Vue 组件化编码时,应注意以下三点:

1). 静态组件要按照功能点进行拆分。

例如: ListHeader.vue 组件负责管理input输入功能, 可以提取为一个单独的组件, 组件命名时最好是见名知意。

2). 实现动态组件时要考虑好数据的存放位置, 如果数据是一个组件在使用, 则可直接放在当前使用的组件里即可, 如果是多个组件都在使用共同的数据, 则应该把数据放在多个组件所属的父组件里, 这样有利于组件间数据的调用。

例如:

这里的 Todo 列表数据, 因为 ListHeader.vue 组件会通过输入新增待办事项来改变 Todo 列表数据, 而 AllList.vue 组件需要展示所有待办事项, 也用到了 Todo 列表数据, 因此这份数据应该放在这两个组件所属的父组件 App.vue 里。

3). 组件间交互使用绑定事件来实现。

例如:

App.vue 组件里的这段代码, 使用 :addTodo 的方式给子组件 ListHeader.vue 绑定: 添加todo的事件

(3). props适用于:

1). 父组件传递数据给子组件: 父组件中绑定事件, 子组件使用 props 声明接收后使用。

2). 子组件传递数据给父组件: 父组件中绑定事件, 子组件使用 props 声明接收后再调用父组件中定义好的函数进行回调,将数据传递给父组件。

例如:

ListItem.vue 组件中的这段代码

鼓励大家亲自动手实现一下 Todo List 案例,无论开发还是测试开发,vVue.js作为优秀的前端框架都值得一学。

代码注释较为详细,在实践过程中,可以仔细阅读以便于加深理解。

公众号后台回复:todo,即可获得项目完整源码包。

分类:

前端

标签:

Vue.js

作者介绍

Wu_Candy
V1