อันนี้คือตัวอย่างโค้ดที่เคนอยากทำ 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>
เมื่อเสร็จเรียบร้อยแล้วจะได้หน้าตาแบบนี้

เมื่อกดเปลี่ยนหน้าก็จะมีการจัดข้อมูลใหม่

หรือค้นหาก็จะกรองข้อมูลใหม่

