Skip to main content

มาแต่ง todo list กัน

มาลองแต่ง Todo List ด้วย DaisyUI กัน

├── src
   ├── App.vue --> root component
   ├── router
   │   └── index.js --> ตำแหน่งรวม router (ตำแหน่งเดิม)
   ├── stores
   │   └── todo.js --> store สำหรับเก็บ todo list ไว้
   └── views
   │ ├── EditView.vue --> หน้าสำหรับแก้ไข EditView.vue
   │ └── HomeView.vue --> หน้าสำหรับหน้าหลัก
└── main.js

เรามีหน้าจะต้องแต่ง 2 หน้าคือ

  • EditView.vue
  • HomeView.vue

โดยเราจะกำหนด layout ใหญ่สุดของหน้าจอก่อน ว่าจะไม่ให้เกิน 768px ไป (เพื่อให้ UI ไม่ใหญ่จนเกินไป)

ที่ App.vue เราจะเพิ่ม class มาห่อ RouterView เป็น

<script setup>
import { RouterView } from 'vue-router'
</script>

<template>
<div class="max-w-3xl mx-auto my-0 p-4">
<RouterView />
</div>
</template>

เริ่ม แต่งหน้า HomeView.vue

สิ่งที่เราจะทำ

  1. ทำการเรียง input ตีให้เต็มหน้าจอ คู่กับปุ่ม Add (เรียงบรรทัดแรกใหม่)
  2. เปลี่ยน icon ปุ่มให้เป็นปุ่มแก้ไข และปุ่มลบ (เอา svg มาจาก https://fontawesome.com/)
  3. แก้ไข loading ให้ตี overlay ทั้งหน้าจอแทน
  4. เรียงของใหม่โดยเอา status ออกและเอา checkbox เข้ามาแทน (เป็นการ mark status done แทนที่จะเปลี่ยนจาก dropdown)
  5. ทำการเพิ่ม tab เข้ามาเพื่อแยก status ระหว่าง Pending, Doing, Done ออกจากกัน
  6. เพิ่มขีดฆ่าเข้ามากับเคสที่ Done แล้ว (ถ้าติ๊กออกให้กลับมา Doing)

ผลลัพธ์จะออกมาเป็นแบบนี้ workshop-02 workshop-03

code ทั้งหมดของ HomeView.vue

<script setup>
import { onMounted, ref, computed } from 'vue'
import { RouterLink } from 'vue-router'

import { useTodoStore } from '../stores/todo'

import Loading from '../components/Loading.vue'

const todoStore = useTodoStore()
const todoText = ref('')
const isLoading = ref(false)

const selectedStatus = ref('Pending')

onMounted(async () => {
isLoading.value = true
await todoStore.loadTodos()
isLoading.value = false
// console.log(todoStore.list) มันเป็น proxy = เราก็แก้ผ่านตัวนี้ก็ได้
})

const todoFilteredList = computed(() => {
return todoStore.list.filter(todo => todo.status === selectedStatus.value)
})

const changeSelectedStatus = (newStatus) => {
selectedStatus.value = newStatus
}

const addTodo = async (todoText) => {
isLoading.value = true
try {
await todoStore.addTodo(todoText)
await todoStore.loadTodos()
} catch (error) {
console.log('error', error)
}
isLoading.value = false
}

const updateStatus = async (todoId, todoStatus) => {
isLoading.value = true
try {
await todoStore.editTodo({
status: todoStatus
}, todoId)
} catch (error) {
console.log('error', error)
}
isLoading.value = false
}

const changeDoneStatus = async (event, todoId) => {
try {
if (event.target.checked) {
await updateStatus(todoId, 'Done')
await todoStore.loadTodos()
}
} catch (error) {
console.log('error', error)
}
}

const removeTodo = async (id) => {
isLoading.value = true
try {
await todoStore.removeTodo(id)
await todoStore.loadTodos()
} catch (error) {
console.log('error', error)
}
isLoading.value = false
}

</script>

<template>
<div class="flex">
<input
class="input input-bordered input-info w-full"
type="text"
placeholder="Add your item"
v-model="todoText"
>
<button class="btn ml-4" @click="addTodo(todoText)">Add</button>
</div>
<div>
<Loading v-if="isLoading"></Loading>
<div>
<div class="tabs tabs-boxed my-2">
<a
v-for="status in todoStore.statuses" :key="status"
:class="status === selectedStatus ? 'tab tab-active' : 'tab'"
@click="changeSelectedStatus(status)">
{{ status }}
</a>
</div>
<div class="flex items-center justify-between my-2" v-for="todo in todoFilteredList" :key="todo.id">
<input type="checkbox" :checked="todo.isDone" class="checkbox" @change="changeDoneStatus($event, todo.id)" />
<div :class="todo.isDone ? 'line-through' : ''">
Item {{ todo.name }}
</div>
<div>
<RouterLink :to="{ name: 'edit-view', params: { id: todo.id } }">
<button class="btn btn-square btn-outline">
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z"/></svg>
</button>
</RouterLink>
<button class="btn btn-square btn-outline ml-2" @click="removeTodo(todo.id)">
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>
</button>
</div>
</div>
</div>
</div>
</template>

<style scoped>
svg {
fill: white;
}
</style>

แต่งหน้า EditView.vue

สิ่งที่เราจะทำ

  1. แก้ไขให้ขนาดหน้าจอเล็กลงครึ่งหนึ่ง และอยู่ตรงกลางหน้าจอ
  2. เรียง form input ใหม่ (input name, status)
  3. เปลี่ยนปุ่มให้เป็นสี primary และตีเต็มแทน
  4. โชว์ badge item id
  5. เพิิ่ม loading เข้ามา
  6. update เสร็จ ให้บอกผ่าน Toast ออกมาว่า update เรียบร้อย

code ทั้งหมดของ EditView.vue

<script setup>
import { onMounted, ref, reactive } from 'vue'
import { useTodoStore } from '../stores/todo'
import { useRoute, RouterLink } from 'vue-router'

import Loading from '../components/Loading.vue'

const todoStore = useTodoStore()
const route = useRoute()

const todoId = ref(-1)
/* Status */
const isLoading = ref(false)
const isUpdated = ref(false)
const isLoaded = ref(false)
const todoData = reactive({
name: '',
status: ''
})

const editTodo = async (todoData, todoId) => {
try {
isLoading.value = true
await todoStore.editTodo(todoData, todoId)
isUpdated.value = true
isLoading.value = false
// set update ออกไป
setTimeout(() => {
isUpdated.value = false
}, 2000)
} catch (error) {
console.log('error', error)
}
}

onMounted(async () => {
try {
isLoading.value = true
todoId.value = parseInt(route.params.id)
await todoStore.loadTodo(todoId.value)
todoData.name = todoStore.selectedTodo.name
todoData.status = todoStore.selectedTodo.status
isLoaded.value = true
isLoading.value = false
} catch (error) {
console.log('error', error)
}
})
</script>

<template>
<div class="w-1/2 mx-auto">
<div class="flex items-center">
<Loading v-if="isLoading"></Loading>
<RouterLink :to="{ name: 'home' }">
<button class="btn btn-square">
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
</button>
</RouterLink>
<div class="ml-2">
Edit
</div>
</div>

<div class="flex flex-col" v-if="isLoaded">
<div class="flex gap-4">
<div class="text-sm ml-1">Item id</div>
<span class="badge badge-primary">12</span>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Item name</span>
</label>
<input class="input input-bordered w-full" type="text" v-model="todoData.name">
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Status</span>
</label>
<select class="select select-bordered" v-model="todoData.status">
<option
v-for="status in todoStore.statuses"
:key="status"
:value="status"
>
{{ status }}
</option>
</select>
</div>
<div class="mt-4">
<button class="btn btn-primary w-full" @click="editTodo(todoData, todoId)">
Update
</button>
</div>
</div>
<div class="toast toast-top toast-start">
<div v-show="isUpdated" class="alert alert-success">
<span>Update successful.</span>
</div>
</div>
</div>
</template>

<style scoped>
svg {
fill: white;
}
</style>

ผลลัพธของ code นี้

workshop-04

และนี่คือผลลัพธ์ทั้งหมดของ Session นี้

เมื่อรวมกันทั้ง 2 หน้า action ทั้งหมดและ UI ทั้งหมดก็จะหน้าตาประมาณนี้ workshop-01

2 Session ต่อไป เราจะจัดหนักจัดเต็มกับการแต่ง Style CSS นะครับ หลังจากที่เราปูพื้นฐานทั้งหมดไป เราจะกลับมาสู่แผนภาพของเรา Easy Commerce กันจริงๆและ