BÀI 13: CRUD TODO APP – PINIA + API

jk5587725

By jk5587725

Đăng ngày Tháng 7 7, 2025

Todo App hoàn chỉnh dùng Pinia + API là bài tổng hợp cực kỳ thực chiến, kết hợp tất cả những gì bạn đã học:

  • Vue 3 Composition API
  • Component
  • Pinia (State management)
  • Fetch API
  • Router
  • CRUD (Create, Read, Update, Delete)

Mục tiêu:

  • Tạo ứng dụng Todo App hoàn chỉnh
  • Kết nối API giả lập (jsonplaceholder)
  • Lưu dữ liệu todo vào Pinia (global state)
  • CRUD: Thêm, sửa, xóa công việc

Chuẩn bị

API sử dụng:

📁 Cấu trúc:

index.html
├── Vue 3 CDN
├── Vue Router CDN
├── Pinia CDN
├── App (TodoApp)
│   ├── TodoList
│   ├── AddTodo
│   └── store/todoStore

Demo: Full App Trong Một File (Chạy trực tiếp)

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>Todo App với Pinia + API</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script src="https://unpkg.com/vue-router@4"></script>
  <script src="https://unpkg.com/pinia@2/dist/pinia.iife.js"></script>
  <style>
    body { font-family: Arial; padding: 20px; max-width: 800px; margin: auto; }
    nav a { margin-right: 15px; text-decoration: none; color: blue; }
    nav a.router-link-exact-active { font-weight: bold; color: darkgreen; }
    .todo { padding: 10px; border: 1px solid #ccc; margin-bottom: 8px; border-radius: 5px; }
    .done { text-decoration: line-through; color: gray; }
    button { margin-left: 10px; }
    input[type="text"] { width: 300px; padding: 5px; }
  </style>
</head>
<body>

<div id="app">
  <h2>✅ TODO APP + PINIA + API</h2>
  <nav>
    <router-link to="/">📋 Danh sách</router-link>
    <router-link to="/add">➕ Thêm</router-link>
  </nav>
  <router-view></router-view>
</div>

<script>
// Import từ CDN
const { createApp, ref, computed, onMounted } = Vue
const { createRouter, createWebHashHistory } = VueRouter
const { createPinia, defineStore } = Pinia

// 🔧 Store quản lý todo
const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchTodos() {
      this.loading = true
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
        this.todos = await res.json()
      } catch (e) {
        this.error = 'Không tải được dữ liệu'
      } finally {
        this.loading = false
      }
    },
    async addTodo(text) {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
        method: 'POST',
        body: JSON.stringify({ title: text, completed: false }),
        headers: { 'Content-Type': 'application/json' }
      })
      const newTodo = await res.json()
      this.todos.unshift(newTodo)
    },
    async deleteTodo(id) {
      await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        method: 'DELETE'
      })
      this.todos = this.todos.filter(t => t.id !== id)
    },
    async toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id)
      const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        method: 'PUT',
        body: JSON.stringify({ ...todo, completed: !todo.completed }),
        headers: { 'Content-Type': 'application/json' }
      })
      const updated = await res.json()
      todo.completed = updated.completed
    }
  }
})

// 🧩 Component: Danh sách
const TodoList = {
  setup() {
    const todoStore = useTodoStore()
    onMounted(() => todoStore.fetchTodos())

    return { 
      todos: computed(() => todoStore.todos),
      loading: computed(() => todoStore.loading),
      error: computed(() => todoStore.error),
      toggle: todoStore.toggleTodo,
      del: todoStore.deleteTodo
    }
  },
  template: `
    <div>
      <p v-if="loading">⏳ Đang tải...</p>
      <p v-if="error" style="color:red;">❌ {{ error }}</p>

      <div v-for="todo in todos" :key="todo.id" class="todo" :class="{done: todo.completed}">
        <input type="checkbox" :checked="todo.completed" @change="toggle(todo.id)">
        {{ todo.title }}
        <button @click="del(todo.id)">❌</button>
      </div>
    </div>
  `
}

// 🧩 Component: Thêm mới
const AddTodo = {
  setup() {
    const todoStore = useTodoStore()
    const newText = ref('')

    const add = async () => {
      if (newText.value.trim()) {
        await todoStore.addTodo(newText.value.trim())
        newText.value = ''
      }
    }

    return { newText, add }
  },
  template: `
    <div>
      <h3>➕ Thêm công việc</h3>
      <input type="text" v-model="newText" placeholder="Nhập nội dung...">
      <button @click="add">Thêm</button>
    </div>
  `
}

// 🚦 Router config
const routes = [
  { path: '/', component: TodoList },
  { path: '/add', component: AddTodo }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 🧠 App chính
const app = createApp({})
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
</script>

</body>
</html>

Bạn đã học được:

Kỹ năngÁp dụng
PiniaQuản lý todo toàn cục
fetch APIGiao tiếp với backend
CRUDThêm / sửa / xóa công việc
Composition APICode sạch, dễ mở rộng
RouterTách trang: danh sách / thêm mới

Bài tập mở rộng:

  1. Thêm chức năng chỉnh sửa todo
  2. Thêm bộ lọc: Tất cả / Đã làm / Chưa làm
  3. Lưu localStorage khi không dùng API

📂 Chuyên mục:

Thảo luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận tin mới

Nhận bài học, tài nguyên và cơ hội việc làm qua email hàng tuần.

[global_subscribe_form]

Chúng tôi cam kết không spam. Bạn có thể hủy bất cứ lúc nào.