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:
GET
: https://jsonplaceholder.typicode.com/todos?_limit=5POST
,PUT
,DELETE
: giả lập, không ghi thật nhưng phản hồi đúng JSON
📁 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 |
---|---|
Pinia | Quản lý todo toàn cục |
fetch API | Giao tiếp với backend |
CRUD | Thêm / sửa / xóa công việc |
Composition API | Code sạch, dễ mở rộng |
Router | Tách trang: danh sách / thêm mới |
Bài tập mở rộng:
- Thêm chức năng chỉnh sửa todo
- Thêm bộ lọc: Tất cả / Đã làm / Chưa làm
- Lưu localStorage khi không dùng API
Thảo luận