Vue implementation Todo-list case

Case effect display (analysis):

Case implementation ideas:

When we get a case, we must think about how to realize it. We mainly use Vue to achieve the effect here. Then with vue component programming, we have to consider how to split a large HTML structure into multiple components.

 According to the effect split diagram shown above, we divide this case into four component parts, namely MyHeader (used to add tasks) , MyList (used to display the list of tasks) , MyFooter (used to display the status of task completion) , and clear the completed task function) , and then MyList is divided into MyItem (used to display the details of each task in the list, and realize editing and deleting tasks) .

After completing the static page components, consider whether our data is placed in that component, whether it is used jointly, or a certain component is used alone. After considering the functions in each component, start to bind data and write corresponding function functions.

Case function realization:

1. MyHeader.vue component

The function implemented by this component is that the user enters the name of the task and clicks the Enter key to confirm. Then pass the input data to the App component (because our data is used by each component, it is placed in the App component. The App component is the boss that manages all components). It lays the foundation for subsequent task list display, task editing and modification and other functions. A keyboard event is bound here, @keyup.enter="add" (press Enter to confirm). Because here the child component transmits data to the parent component. All methods in the parent component must prepare an addTodo() method in advance, and then the child component can use props:['addtodo'] to call the method of the parent component. What I use here is to use emit trigger to notify the App component to add a todo object.

Code display:

<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:'MyHeader',
		data() {
			return {
				//收集用户输入的title
				title:''
			}
		},
		methods: {
			add(){
				//校验数据
				if(!this.title.trim()) return alert('输入不能为空')
				//将用户的输入包装成一个todo对象
				const todoObj = {id:nanoid(),title:this.title,done:false}
				//通知App组件去添加一个todo对象
				this.$emit('addTodo',todoObj,1,2,3)
				//清空输入
				this.title = '' 
			}
		},
	}
</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>

2. MyList.vue component

The function implemented by this component is to display a list of the user's tasks. Use props:['todos'] to accept the data passed by the App component. Then traverse the data and pass the data to its subcomponents .

Code display:

<template>
	<ul class="todo-main">
		<transition-group name="todo" appear>
		<MyItem 
			v-for="todoObj in todos"
			:key="todoObj.id" 
			:todo="todoObj" 
		/>
		</transition-group>
	</ul>
</template>

<script>
	import MyItem from './MyItem'
	export default {
		name:'MyList',
		components:{MyItem},
		//声明接收App传递过来的数据
		props:['todos']
	}
</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;
	}
	.todo-enter-active{
        animation: atguigu 0.5s linear;
    }
    .todo-leave-active{
        animation: atguigu 0.5s linear reverse;
    }
    @keyframes atguigu{
        from{
            transform: translateX(-100%);
        }
        to{
            transform: translateX(0px);
        }
    }
</style>

3. MyItem.vue component

The function of this component is to display a specific sub-item in the user's task list. Then realize the page rendering of the sub-item, the editing and deleting functions of the input content.

Code display:

<template>
		<li>
			<label>
				<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
				<span v-show="!todo.isEdit">{
   
   {todo.title}}</span>
				<input
				type="text" 
				v-show="todo.isEdit" 
				:value="todo.title"
				@blur="handleBlur(todo,$event)"
				ref="inputTitle"
				>
			</label>
			<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
			<button class="btn btn-edit" v-show="!todo.isEdit"  @click="handleEdit(todo)">编辑</button>
		</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			},
			//编辑
			handleEdit(todo){
				// if(todo.hasOwnProperty('isEdit')){
				if(todo.isEdit){
					todo.isEdit=true
				}else{
					this.$set(todo,'isEdit',true)
				}
				this.$nextTick(function () {
				    this.$refs.inputTitle.focus();
                });
			},
			//失去焦点的时候(真正执行修改逻辑)
			handleBlur(todo,e){
				todo.isEdit=false
				if(!e.target.value.trim()) return alert('输入不能为空')
				this.$bus.$emit('updateTodo',todo.id,e.target.value)
			}
		},
	}
</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>

4.MyFooter.vue pairs

The function realized by this component is to display the completed quantity and total quantity. When the check box is ticked, all tasks will be completed. If canceled, all tasks will be cancelled, and the completed tasks can be cleared.

Code display:

<template>
	<div class="todo-footer" v-show="total">
		<label>
			<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
			<input type="checkbox" v-model="isAll"/>
		</label>
		<span>
			<span>已完成{
   
   {doneTotal}}</span> / 全部{
   
   {total}}
		</span>
		<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
	</div>
</template>

<script>
	export default {
		name:'MyFooter',
		props:['todos'],
		computed: {
			//总数
			total(){
				return this.todos.length
			},
			//已完成数
			doneTotal(){
				//此处使用reduce方法做条件统计
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
			},
			//控制全选框
			isAll:{
				//全选框是否勾选
				get(){
					return this.doneTotal === this.total && this.total > 0
				},
				//isAll被修改时set被调用
				set(value){
					// this.checkAllTodo(value)
					this.$emit('checkAllTodo',value)
				}
			}
		},
		methods: {
			//清空所有已完成
			clearAll(){
				// this.clearAllTodo()
				this.$emit('clearAllTodo')
			}
		},
	}
</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>

5. App.vue component

This component contains common data, creates methods, and passes the methods to subcomponents. The global event bus is used, and this.$bus.$emit('xxxx', data) method is used to provide data.

Code display:

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader @addTodo="addTodo"/>
				<MyList :todos="todos"/>
				<MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
			</div>
		</div>
	</div>
</template>

<script>
    import pubsub from 'pubsub-js'

	import MyHeader from './components/MyHeader'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				todos:JSON.parse(localStorage.getItem('todos')) || []
			}
		},
		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
			clearAllTodo(){
				this.todos = this.todos.filter((todo)=>{
					return !todo.done
				})
			},
			//更新一个todo
			updateTodo(id,title){
				this.todos.forEach((todo)=>{
					if(todo.id === id) todo.title = title
				})
			},
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
		mounted() {
			this.$bus.$on('checkTodo',this.checkTodo)
			this.$bus.$on('updateTodo',this.updateTodo)
			this.pubId=pubsub.subscribe('deleteTodo',this.deleteTodo)
		},
		beforeDestroy() {
			this.$bus.$off('checkTodo')
			this.$bus.$off('updateTodo')
			pubsub.unsubscribe(this.pubId)
		},
	}
</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-edit{
		color: #fff;
		background-color: skyblue;
		border: 1px solid rgb(103, 159, 180);
		margin-right: 5px;
	}
	.btn-edit:hover {
		color: #fff;
		background-color: rgb(103, 159, 180);
	}
	.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>

Guess you like

Origin blog.csdn.net/m0_60263299/article/details/123261812