使用 TypeScript + Vite 编写原生 TODO

介绍

如何编写一个 TODO ?

  1. 外观模式
  2. 数据分离

设计方案

传统写法

绑定事件处理函数 - 数据
    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

上一篇:vite 开发 Cesium 程序最佳配置实践


下一篇:vite mock 数据插件:vite-plugin-easy-mock