Vue3.0实现todolist案例
文章内容输出来源:大前端高薪训练营
代码地址:
https://gitee.com/jiailing/lagou-fed/tree/master/fed-e-task-03-05/code/04-todolist
1. ToDoList功能列表
- 添加待办事项
- 删除待办事项
- 编辑待办事项
- 切换待办事项
- 存储待办事项
2. 项目结构
使用vue脚手架创建Vue项目,先升级vue-cli,4.5.6版本的vue-cli创建项目时可以选择vue版本。
Vue CLI 的包名称由 vue-cli
改成了 @vue/cli
。 如果已经全局安装了旧版本的 vue-cli
(1.x 或 2.x),你需要先通过 npm uninstall vue-cli -g
或 yarn global remove vue-cli
卸载它
可以使用下列任一命令安装这个新的包:
npm install -g @vue/cli
# OR
yarn global add @vue/cli
先使用vue create创建项目,创建的时候选择3.0。
vue create 04-todolist
然后选择Default (Vue 3 Preview) ([Vue 3] babel, eslint)
就会自动创建项目了。
3. 添加待办事项
在输入框输入文本按下enter键提交待办事项
// 1. 添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value?.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
4. 删除待办事项
点击待办事项右侧的叉号可以删除待办事项
// 2. 删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo)
todos.value.splice(index, 1)
}
return {
remove
}
}
5. 编辑待办事项
双击进入编辑状态,按esc退出编辑,按enter提交编辑,如果删光了文本,则为删除这一项。
- 双击待办事项,展示编辑文本框
- 按回车或者编辑文本框失去焦点,修改数据
- 按esc取消编辑
- 把编辑文本框清空按回车,删除这一项
- 显示编辑文本框的时候获取焦点
// 3. 编辑待办事项
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()
if (todo.text === '') remove(todo)
editingTodo.value = null
}
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
模板中:
<ul class="todo-list">
<li v-for="todo in todos" :key="todo" :class="{editing: todo === editingTodo}">
<div class="view">
<input type="checkbox" class="toggle">
<label @dblclick="editTodo(todo)">{
{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input type="text" class="edit" v-model="todo.text" @keyup.enter="doneEdit(todo)" @blur="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
</li>
</ul>
6. 编辑文本框获取焦点 - vue3.0自定义指令
传对象形式:
-
Vue 2.x
Vue.directive('editingFocus', { bind(el, binding, vnode, prevVnode) { }, inserted() { }, update() { } // remove, componentUpdated() { }, unbind() { } })
-
Vue 3.0
app.directive('editingFocus', { beforeMount(el, binding, vnode, prevVnode) { }, mounted() { }, breforeUpdate() { }, // new updated() { }, beforeUnmount() { }, // new unmounted() { } })
传函数形式:
-
Vue 2.x
Vue.directive('editingFocus', (el, binding) => { binding.value && el.focus() })
-
Vue 3.0
app.directive('editingFocus', (el, binding) => { binding.value && el.focus() })
代码实现自定义事件获取正在编辑的文本框焦点:
export default {
name: 'App',
// 省略了setup() {},
directives: {
editingFocus: (el, binding) => {
// binding可以获取到一些参数
binding.value && el.focus()
}
}
}
使用:
<input v-editing-focus="todo === editingTodo" type="text" class="edit" v-model="todo.text" @keyup.enter="doneEdit(todo)" @blur="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
7. 切换待办事项
- 点击CheckBox可以改变所有代办项状态
- All/Active/Completed
- 其他
- 显示未完成代办项个数
- 移除所有完成的项目
- 如果没有待办项,隐藏main和footer
// 4. 切换待办项完成状态
const useFilter = todos => {
const allDone = computed({
get: () => {
return !todos.value.filter(item => !item.completed).length
},
set: (value) => {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed)
}
const type = ref('all')
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 = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
filteredTodos,
remainingCount,
count
}
}
8. 本地存储
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
}
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
}
}
9. 完整代码
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input type="text" class="new-todo" placeholder="What needs to be done?" autocomplete="off" autofocus v-model="input" @keyup.enter="addTodo">
</header>
<section class="main" v-show="count">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li v-for="todo in filteredTodos" :key="todo" :class="{editing: todo === editingTodo, completed: todo.completed}">
<div class="view">
<input type="checkbox" class="toggle" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{
{
todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input v-editing-focus="todo === editingTodo" type="text" class="edit" v-model="todo.text" @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">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="count > remainingCount">Clear completed</button>
</footer>
</section>
</template>
<script>
import './assets/index.css'
import useLocalStorage from './utils/useLocalStorage'
import {
ref,
computed,
onMounted,
onUnmounted,
watchEffect
} from 'vue'
const storage = useLocalStorage()
// 1. 添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value?.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2. 删除待办事项
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
}
}
// 3. 编辑待办事项
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()
if (todo.text === '') remove(todo)
editingTodo.value = null
}
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4. 切换待办项完成状态
const useFilter = todos => {
const allDone = computed({
get: () => {
return !todos.value.filter(item => !item.completed).length
},
set: (value) => {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed)
}
const type = ref('all')
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 = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
filteredTodos,
remainingCount,
count
}
}
// 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可以获取到一些参数
binding.value && el.focus()
}
},
}
</script>
<style>
</style>