介绍
如何编写一个 TODO ?
- 外观模式
- 数据分离
设计方案
传统写法
绑定事件处理函数 - 数据
1. 增加项 - 列表数据 -> 增加一项
{ id: timestamp, content: string, completed: false }
每一项的视图 -> 列表
2. 删除项 - 列表数据 -> id -> removeItem
将对应项的视图 -> 列表 -> 删除
3. 改变完成状态 - 列表数据 -> id -> change completed
将对应项的完成状态 -> 是否完成 toggle
面向对象、类的继承、横向切割程序
程序进行分类
外层:浏览器的事件 -> 调用方法 -> 事件处理函数的绑定
操作数据:addTodo、removeTodo、toggleComplete
操作DOM:addItem、removeItem、changeCompleted
管理模版:todoView -> 接受参数
项目初始化
初始化
$ mkdir todo
$ cd todo
$ npm init -y
$ npm i vite -D
然后修改 package.json
中的运行命令为 vite
// package.json
"scripts": {
"dev": "vite"
},
运行
现在可以直接运行项目了
$ npm run dev
当然,还需创建以下目录
todo
├── index.html
└── src
├── app.ts
└── js
├── TodoDom.ts // todo dom 操作
├── utils.ts // 工具类
├── TodoEvent.ts // todo 事件
├── TodoTemplate.ts // 渲染 todo-item 的 DOM 模板
└── typing.ts // 接口
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo</title>
</head>
<body>
<div class="app">
<div class="todo-input">
<input type="text" placeholder="请输入待办事项">
<button>增加</button>
</div>
<div class="todo-list"></div>
</div>
</body>
<script type="module" src="./src/app.ts"></script>
</html>
src/app.ts
import {ITodoData} from "./js/typing";
import {TodoEvent} from "./js/TodoEvent"
((doc) => {
const dInput: HTMLInputElement = document.querySelector('input') // input 输入框
const dAddBtn: HTMLElement = document.querySelector('button') // button 增加按钮
const dTodoList: HTMLElement = document.querySelector('.todo-list') // todo 列表
// 初始数据
const todoData: ITodoData[] = [
{
id: 1,
content: '123',
completed: true,
},
{
id: 2,
content: '456',
completed: false,
},
{
id: 3,
content: '789',
completed: true,
}
]
const todoEvent: TodoEvent = new TodoEvent(todoData, dTodoList)
const init = (): void => {
bindEvent()
}
function bindEvent(): void {
dAddBtn.addEventListener('click', handleAddBtnClick, false)
dTodoList.addEventListener('click', handleListClick, false)
}
// 添加操作
function handleAddBtnClick(): void {
const value: string = dInput.value.trim()
if (value.length) {
const result: undefined | number = todoEvent.addTodo(<ITodoData> {
id: new Date().getTime(),
content: value,
completed: false,
})
if (result && result === 1001) {
alert('列表项已存在')
dInput.select()
return
}
dInput.value = ''
dInput.focus()
}
}
// 每个 todo 子项的状态改变、删除事件
function handleListClick(e: MouseEvent): void {
const tar = e.target as HTMLElement
const tagName = tar.tagName.toLocaleLowerCase()
if (tagName === 'input' || tagName === 'button') {
const id = parseInt(tar.dataset.id)
switch (tagName) {
case "input":
todoEvent.toggleComplete(tar, id)
break
case "button":
todoEvent.removeTodo(tar, id)
break
default:
break
}
}
}
init()
})(document)
src/js/TodoEvent.ts
import {ITodoData} from "./typing";
import {TodoDom} from "./TodoDom";
class TodoEvent extends TodoDom{
private todoData: ITodoData[]
constructor(todoData: ITodoData[], todoWrapper: HTMLElement) {
super(todoWrapper)
this.todoData = todoData
this.init()
}
private init() {
this.initList(this.todoData)
}
public addTodo(todo: ITodoData): undefined | number {
const _todo: null | ITodoData = this.todoData.find((item: ITodoData) => item.content === todo.content) // 去重
if (!_todo) {
this.todoData.push(todo)
this.addItem(todo)
return
}
return 1001
}
public removeTodo(target: HTMLElement, id: number): void {
this.todoData = this.todoData.filter((todo: ITodoData) => todo.id !== id) // 删除
this.removeItem(target)
}
public toggleComplete(target: HTMLElement, id: number): void {
this.todoData = this.todoData.map((todo: ITodoData) => {
if (todo.id === id) {
todo.completed = !todo.completed
this.changeCompleted(target, todo.completed)
}
return todo
})
}
}
export {
TodoEvent
}
src/js/TodoDom.ts
import {ITodoData} from "./typing";
import {TodoTemplate} from "./TodoTemplate";
import {createItem, findParentNode} from "./utils";
class TodoDom extends TodoTemplate{
private todoWrapper: HTMLElement
constructor(todoWrapper: HTMLElement) {
super()
this.todoWrapper = todoWrapper
}
protected initList(todoData: ITodoData[]) {
todoData.map(todo => this.addItem(todo))
}
protected addItem(todo: ITodoData) {
const dItem: HTMLElement = createItem('div', 'todo-item', this.todoView(todo))
this.todoWrapper.appendChild(dItem)
}
protected removeItem(target: HTMLElement) {
const dParentNode: HTMLElement = findParentNode(target, 'todo-item')
dParentNode.remove()
}
protected changeCompleted(target: HTMLElement, completed: boolean) {
const dParentNode: HTMLElement = findParentNode(target, 'todo-item')
const dContent: HTMLElement = dParentNode.querySelector('span')
dContent.style.textDecoration = completed ? 'line-through' : 'none'
}
}
export {
TodoDom
}
src/js/TodoTemplate.ts
import {ITodoData} from "./typing";
class TodoTemplate {
protected todoView({id, content, completed}: ITodoData): string {
return `
<input type="checkbox" ${completed ? 'checked': ''} data-id="${id}"/>
<span style="text-decoration: ${completed ? 'line-through': 'none'}">${content}</span>
<button data-id="${id}">删除</button>
`
}
}
export {
TodoTemplate
}
src/js/typing.ts
interface ITodoData {
id: number
content: string
completed: boolean
}
export {
ITodoData
}
src/js/utils.ts
/**
* 寻找当前父节点
* @param target
* @param className
*/
function findParentNode(target: HTMLElement, className: string): HTMLElement {
while (target = target.parentNode as HTMLElement) {
if (target.className === className) {
return target
}
}
}
/**
* 创建节点
* @param tagName
* @param className
* @param todoItem
*/
function createItem(tagName: string, className: string, todoItem: string): HTMLElement {
const dItem: HTMLElement = document.createElement(tagName)
dItem.className = className
dItem.innerHTML = todoItem
return dItem
}
export {
findParentNode,
createItem,
}
地址
最后对应的 GitHub 地址:https://github.com/Pooc-J/todo
内容来源 B 站小野:https://www.bilibili.com/video/BV1Jt4y1k7dS