[Nuxt 3] ตัวอย่างทำ Pagination + define Expose ดึงค่าตัวแปรจาก Child Component มาที่ Parent Component แล้วใช้งานต่อ


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