利用 Vue3 实现一个简单的 todo-list

334 阅读3分钟

利用 Vue 实现一个简单的待办列表

我们将使用 Vue.js 3.0 实现一个简单的待办列表( Todo List )应用。具体用到的知识点如下:

  • Vue 组件基础。包含组件的全局注册、传递 props 和监听事件。
  • Vue 列表渲染。
  • Vue 事件处理。
  • Vue 表单输入绑定。
  • Vue 样式绑定。
  • JS 数组方法。包含 mapfilter

通过 CDN 使用 Vue

由于我们只是实现一个简单的应用,我们采用 CDN 的方式引入 Vue。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script><div id="app"></div><script>
  const { createApp } = Vue
  
  // 创建应用实例
  const app = createApp({})
  
  // 挂载应用
  app.mount('#app')
</script>

组件树

Todo List 应用的组件树如下:

TodoList (根组件)
├─ TodoForm (待办项表单组件)
└─ TodoItem-1 (具体待办项)
└─ TodoItem-2 (具体待办项)
└─ TodoItem-3 (具体待办项)
...
└─ TodoItem-n (具体待办项)

定义根组件

「根组件」是一个对象,是传入 createApp 参数。每个应用都需要一个「根组件」,其他组件将作为其子组件。在这里,我们的根组件是 TodoList 对象。

...

// 定义根组件
const TodoList = {}

// 创建应用实例
const app = createApp(TodoList)

...

待办项结构

我们的根组件需要包含每个待办项所需要的各种信息。一个待办项需要包含如下信息:

  • 待办项的 ID。
  • 待办项的具体内容。
  • 该待办项是否已完成。

根组件数据

因此,我们需要在根组件中定义一个待办项的列表。我们可以先添加 3 个待办项。

// 定义根组件
const TodoList = {
  data() {
    return {
      items: [
        {
          id: 1,
          content: 'Do homework',
          completed: false
        },
        {
          id: 2,
          content: 'Do chores',
          completed: false
        },
        {
          id: 3,
          content: 'Cook dinner'
          completed: false
        }
      ]
    }
  }
}

根组件方法

一个组件除了要包含数据之外,还需要包含方法。因此需要定义添加和删除待办项的方法。

// 定义根组件
const TodoList = {
  data() {
    return {
      items: [...]
    }
  },
  methods: {
    // 添加待办项
    addTodo(value) {
      this.items.push({
        id: new Date().getTime(), // 以时间戳为ID
        content: value,
        completed: false
      })
    },
    // 删除待办项
    removeTodo(id) {
      this.items = this.items.filter(item => item.id !== id)
    }
  }
}

与此同时,我们还需要增加一个修改待办项「完成状态」的方法。一个待办项可以被用户标记为「已完成」或者「未完成」。

// 定义根组件
const TodoList = {
  data() {
    return {
      items: [...]
    }
  },
  methods: {
    // 添加待办项
    addTodo(value) {
      ...
    },
    // 删除待办项
    removeTodo(id) {
      ...
    },
    // 修改待办项的完成状态
    changeCompleted(id) {
      this.items = this.items.map(item => {
        if (item.id === id) {
          item.completed = !item.completed
        }
        return item
      })
    }
  }
}

至此,我们的根组件已经定义完成。

定义表单组件

我们采用全局注册的方式来定义组件,因此需要使用「应用实例」。

...

// 定义根组件
const TodoList = {...}

// 创建应用实例
const app = createApp(TodoList)

// 定义表单组件
app.component('todo-form', {})

// 挂载应用
app.mount('#app')

表单的模板、数据和方法

表单是用来添加待办项的,所以一个表单组件包含一个文本输入框和一个提交按钮。

app.component('todo-form', {
  template: `
    <input type="text" placeholder="Add new todo..." />
    <button>Complete</button>
  `,
  data() {
    return {
      inputValue: '' // 表单输入框的内容
    }
  }
})

我们可以使用 v-model 指令将表单输入框的内容同步给 todo-form 组件中的 inputValue

app.component('todo-form', {
  template: `
    <input type="text" placeholder="Add new todo..." v-model="inputValue" />
    <button>Complete</button>
  `,
  data() {
    return {
      inputValue: '' // 表单输入框的内容
    }
  }
})

使用组件

现在我们可以将 todo-form 子组件放在根组件内。注意,在 DOM 模板中,我们必须显式地关闭标签

<div id="app">
  <todo-form></todo-form>
</div>

与此同时,我们也需要在点击按钮之后,触发添加待办项的事件。我们需要和父组件进行通信,将「添加待办项」这一事件通知到父组件 TodoList

app.component('todo-form', {
  template: `
    <input type="text" placeholder="Add new todo..." v-model="inputValue" />
    <button @click="addTodo">Complete</button>
  `,
  data() {
    return {
      inputValue: '' // 表单输入框的内容
    }
  },
  methods: {
    addTodo() {
      this.$emit('add-todo', this.inputValue)
    }
  }
})

在父组件中监听子组件中定义的事件,并绑定该事件。

<div id="app">
  <todo-form @add-todo="addTodo"></todo-form>
</div>

定义待办项组件

用户在添加完待办项之后,需要在表单下面显示待办项列表,因此我们可以定义一个待办项组件。

...

// 创建应用实例
const app = createApp(TodoList)

// 定义表单组件
app.component('todo-form', {})

// 定义待办项组件
app.component('todo-item', {})

// 挂载应用
app.mount('#app')

待办项组件基本结构

待办项组件内部包含如下三部分,每一部分包含一个功能。

  • 一个行内文本。用于显示当前待办项的名称。
  • 一个选择框。用于改变当前待办项的完成状态。
  • 一个按钮。用于删除当前待办项。
// 定义待办项组件
app.component('todo-item', {
  template: `
    <li>
      <input type="checkbox" />
      <span></span>
      <button>Delete</button>
    </li>
  `
})

向组件中传数据

在父组件中引用 todo-item 组件。我们使用「列表渲染」来实现显示待办项组件列表,并传入待办项信息。

<div id="app">
  <todo-form @add-todo="addTodo"></todo-form>
  <ul>
    <todo-item 
      v-for="item of itmes" :key="item.id"
      :todo="item"
    >
    </todo-item>
  </ul>
</div>

我们还需要在组件中接收父组件传过来的待办项具体信息作为 props。接收到 props 之后,我们才能实现待办项的上述三个功能。

// 定义待办项组件
app.component('todo-item', {
  props: ['todo'],
  template: `
    <input type="checkbox" @click="changeCompleted(todo.id)" />
    <span>{{ todo.content }}</span>
    <button @click="removeTodo(todo.id)">Delete</button>
  `,
  methods: {
    changeCompleted(id) {
      this.$emit('change-completed', id)
    },
    removeTodo(id) {
      this.$emit('remove-todo', id)
    }
  }
})

监听组件中的事件

在父组件中监听子组件的事件。

<div id="app">
  <todo-form @add-todo="addTodo"></todo-form>
  <ul>
    <todo-item 
      v-for="item of itmes" :key="item.id"
      :todo="item"
      @change-completed="changeCompleted"
      @remove-todo="removeTodo"
    >
    </todo-item>
  </ul>
</div>

至此,我们的基本功能就完成了。

样式绑定

绑定内联样式。

// 定义待办项组件
app.component('todo-item', {
  props: ['todo'],
  template: `
    <input type="checkbox" @click="changeCompleted(todo.id)" />
    <span :style="{ textDecoration: todo.completed ? 'line-through' : 'none' }">
      {{ todo.content }}
    </span>
    <button @click="removeTodo(todo.id)">Delete</button>
  `,
  methods: {
    ...
  }
})

至此,我们的待办列表应用已经全部完成。