อันนี้คือตัวอย่างโค้ดที่เคนอยากทำ 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>


เมื่อเสร็จเรียบร้อยแล้วจะได้หน้าตาแบบนี้

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

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

0 0 votes
Article Rating
0
Would love your thoughts, please comment.x
()
x