อันนี้คือตัวอย่างโค้ดที่เคนอยากทำ Pagination แล้วมันเกิดปัญหาที่ว่า ไม่สามารถ Custom ตัว Table ของข้อมูลได้ขนาดนั้นมันยุ่งยากเกิน
จึงเกิดไอเดียขึ้นมาว่า งั้นใน Child Component ที่เป็นส่วนข้อมูลใหญ่ๆ จะทำงานอัตโนมัติ เช่น การคำนวณว่ามีแถวเท่าไหร่ การค้นหาข้อมูล การทำ Pagination และส่วนตัว Table ที่แสดงข้อมูลจะมาทำใน Parent Component และส่งไปผ่าน <slot /> ของ Child Component
สิ่งที่ต้องรู้ถึงจะสามารถทำได้
- ref
- defineExpose
- @vue/complier-sfc (ถ้าไม่ติดปัญหา ไม่ต้องติดตั้ง)
รายการ Component
- index.vue
- components/PaginationMain.vue
- components/PaginationTotalAndSearch.vue
- components/PaginationListPage.vue
DB/API เลือกใช้ Supabase
ของเคนเลือกใช้ Supabase นะ ถ้าใครเลือกใช้ตัวอื่นก็ให้ปรับส่วนการดึงข้อมูลกันเอา Supabase จะคล้ายๆ Firebase แหละ มีส่วนฐานข้อมูล, Authentication, เก็บภาพ
เริ่มเลย หน้า index
ในหน้าหลักจะมีการเรียกข้อมูลจาก Supabase และส่งไปให้ PaginationMain ในการจัดการข้อมูล และส่งกลับมาเป็นตัวแปร paginationData เพื่อ render ข้อมูล
// index.vue <script setup> const supabase = useSupabaseClient(); /** GET DATA */ const { data } = await supabase .from('aff_products') .select() .eq('is_delete', false) .order('name', { ascending: true }); /** กำหนดว่าสามารถค้นหาใน Field ไหนได้บ้าง */ const keysData = ['id', 'name', 'description']; /** ข้อมูลที่ผ่านการค้นหา และแบ่งหน้ามาแล้ว จาก PaginationMain Component */ const paginationData = ref(null); /** UPDATE: ปุ่มลบ */ const deleteItem = async (id) => { if (confirm('ยืนยันการลบ')) { paginationComponent.value.deleteItem(id); } }; </script> <template> <section class="mt-10" v-if="data" > <PaginationMain ref="paginationComponent" :data="data" pagination_page_url="/admin/products" > <div class="overflow-x-auto"> <table class="table"> <thead> <tr> <th>รูป</th> </tr> </thead> <tbody v-if="paginationComponent"> <tr v-for="(item, index) in paginationComponent.paginationData" :key="item.id" > <td> <div class="flex items-center space-x-3"> <div class="avatar"> <div class="mask mask-squircle w-12 h-12"> <img :src="item.image" :alt="item.name" class="w-[80px] mx-auto" /> </div> </div> <div> <div class="font-bold">{{ item.name }}</div> <div class="text-sm opacity-50">{{ item.description }}</div> </div> </div> </td> <!-- UPDATE: ปุ่มลบ --> <td> <div class="join"> <button class="btn btn-sm join-item btn-error" @click="deleteItem(item.id)" > ลบ </button> </div> </td> </tr> </tbody> <tfoot> <tr> <th>รูป</th> </tr> </tfoot> </table> </div> </PaginationMain> </section> </template>
Component – PaginationMain
มีการรับ Props ต่างๆ ที่ต้องการเอามาปรับแต่งใช้งาน และมีการคำนวณว่า ค้นหาอย่างไร แบ่งหน้าอย่างไร
และมีการเรียก PaginationTotalAndSearch โดยมีการผูก v-model ให้ เพื่อให้เวลาค้นหาแล้วกลับมาใช้ฟังก์ชั่น filterData() เพื่อกรองข้อมูล
รวมถึงมีการสร้าง watch() เพื่อสังเกตการเปลี่ยนแปลงของ query ?page เพื่อดูว่ามีการกดเปลี่ยนหน้าไหม ถ้าเปลี่ยนหน้าจะทำการแบ่งข้อมูลมาแสดง
// components/PaginationMain.vue <script setup> const props = defineProps({ data: Array, keys_data_auto: Boolean, keys_data: Array, pagination_page_url: String, }); const route = useRoute(); const PAGINATION_PER_PAGE = 30; const originalData = ref([]); const filteredData = ref([]); const paginationData = ref([]); const nowPage = ref(1); const totalPage = ref(1); const slicePage = ref(PAGINATION_PER_PAGE); const searchKeyword = ref(); let keysData = []; if (props.data) { originalData.value = props.data; filteredData.value = props.data; // เอาไว้ค้นหาสินค้า paginationData.value = props.data; //เอาไว้ทำเพจจิ้งหลังจากค้นหาสินค้าแล้ว if (props.keys_data_auto || !props.keys_data) { /** Key เพื่อค้นหา แบบ DYNAMIC ตามข้อมูลที่รับมา */ keysData = Object.keys(filteredData.value[0]); // key ของ object ของสินค้า } else { keysData = props.keys_data; } } const filterData = async () => { if (searchKeyword.value) { filteredData.value = await originalData.value.filter((product) => { let haveProduct = false; keysData.every((key) => { const typeOfValue = typeof product[key]; if (typeOfValue !== 'object') { if ( product[key] .toString() .toLowerCase() .includes(searchKeyword.value) ) { haveProduct = true; return false; } } return true; }); return haveProduct; }); } else { filteredData.value = originalData.value; } setPagination(); }; const setPagination = async (toPage) => { totalPage.value = Math.ceil( filteredData.value.length / PAGINATION_PER_PAGE ); nowPage.value = toPage || route.query.page || 1; if (totalPage.value < nowPage.value) { nowPage.value = 1; } const offset = nowPage.value - 1; const sliceStart = offset * PAGINATION_PER_PAGE; const sliceEnd = nowPage.value * PAGINATION_PER_PAGE; slicePage.value = sliceEnd > filteredData.value.length ? filteredData.value.length : sliceEnd; paginationData.value = await filteredData.value.slice(sliceStart, sliceEnd); }; /** UPDATE: ปุ่มลบ */ const deleteItem = async (id) => { originalData.value = originalData.value.filter((item) => item.id !== id); filterData(); }; /** คอยตรวจดูว่ามีการเปลี่ยนค่าของ ?page ไหม ถ้ามีให้เปลี่ยนค่าที่ใช้แสดง */ watch( () => route.query, async () => { await setPagination(); } ); /** เหตุผลที่ defineExpose อยู่ก่อน await setPagination เพราะ await มันไปขัดการทำงานอ่ะ ทำให้ไม่โหลดข้อมูล */ defineExpose({ paginationData, deleteItem }); await setPagination(); </script> <template> <PaginationTotalAndSearch :total_data="filteredData.length" :current_page="slicePage" :modelValue="searchKeyword" @update:modelValue=" (newValue) => { searchKeyword = newValue; filterData(); } " /> <slot /> <PaginationTotalAndSearch :total_data="filteredData.length" :current_page="slicePage" :modelValue="searchKeyword" @update:modelValue=" (newValue) => { searchKeyword = newValue; filterData(); } " /> <section class="mt-10"> <PaginationListPage :total_page="totalPage" :current_page="parseInt(nowPage)" :href="pagination_page_url" /> </section> </template>
Component – PaginationTotalAndSearch
ส่วนนี้จะคือที่แสดงว่ากำลังแสดงกี่รายการในหน้านี้ และมีทั้งหมดเท่าไหร่ รวมถึงช่องค้นหา
// components/PaginationTotalAndSearch.vue <script setup> const props = defineProps({ total_data: Number, current_page: Number, href: String, modelValue: String, }); const emit = defineEmits(['update:modelValue']); </script> <template> <div class="flex justify-between"> <div class="text-sm opacity-50 mt-2"> ทั้งหมด {{ current_page }} จาก {{ total_data }} รายการ </div> <input type="search" class="input input-bordered input-sm text-center" placeholder="ค้นหา" :value="props.modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </div> </template>
Component – PaginationListPage
อันนี้จะแสดงหน้าให้เราสามารถกดได้
// components/PaginationListPage.vue <script setup> const props = defineProps({ total_page: Number, current_page: Number, href: String, }); </script> <template> <div class="mt-2 flex justify-center"> <div class="join"> <span v-for="(item, index) in Array.from( { length: props.total_page }, (_, i) => i + 1 )" :key="index" > <NuxtLink :to="{ path: props.href, query: { page: item } }" class="btn btn-sm join-item" :class="{ 'btn-active': item == props.current_page }" >{{ item }}</NuxtLink > </span> </div> </div> </template>