VUE 表格元件進階-分頁

上一篇《VUE 表格元件進階-排序》有提到把表格做成公用的子元件,子元件本身有排序的功能,透過父元件的傳入的資料設定表格的欄位,表格的內容也透過父元件傳入子元件進行排列,也就是父元件負責資料,表格子元件負責排列及排序的功能。

在這一篇我要加上分頁的功能,把分頁元件當成表格元件的子元件,如此一個表格元件的功能才算完整。

以下說明一下相關元件要處理的任務。

父元件:

負責取得資料與傳資料給表格子元件。

表格元件:
  1. 新增一個 currnetPage 的 ref 變數,用來儲存 user 點擊的頁次,初始時預設為 1 。
  2. 新增一個 perPage 的 ref 變數,用來儲存每一頁要渲染在表格上的資料筆數。
  3. 把資料的總筆數傳遞給分頁元件,在上一篇中我用 tempData 這個 ref 儲存取得的資料。
  4. 透過 props 把 currnetPage 與 perPage 傳給分頁元件(pagination)。
  5. 當使用者點擊分頁元件上的頁次時,分頁元件 把 currentPage 的值 emits 傳給表格元件,表格元件透過 computed 觀察 currentPage 的變化,計算出那個分頁要呈現的資料,然後渲染在表格的 DOM 上。
分頁元件:
  1. 取得表格元件 props 進來的 currentPage、perPage 與總筆數。
  2. User 點擊分頁元件的頁次後,emits 給表格元件使用者點擊的是那個分頁。
  3. 點擊分頁元件後,被點擊的那一格應該呈現 active 的狀態。
  4. 往前一頁跟後一頁的功能。

Step1: 表格元件新增 currentPage、perPage 的 ref 變數

//DataTable 元件
const perPage = ref(10);
const currentPage = ref(1);

Step2: 表格元件傳遞 props 給分頁元件

表格元件把 currentPage、perPage 與資料總筆數傳遞給分頁元件,其中 currentPage 可以用 v-model 來雙向綁定。

 <pagination    
    :perPage="perPage"
    :totalRows="tempData.length"
    v-model="currentPage"
  ></pagination>

Step3: 分頁元件接收表格元件傳入的 props

分頁元件透過 props 取得表格元件傳入的 totalRows 、 perPage 與 currentPage,currentPage 因為是使用 v-model 的方式取得,所以分頁元件以 modelValue 承接,同時也要設定 emit = defineEmits(['update:modelValue']),當使用者點擊頁次時觸發 emit 傳遞事件跟值給表格元件。

// Props
const props = defineProps({
  align: {
    type: String,
    default: 'left',
  },
  totalRows: {
    type: Number,
    default: 0,
  },
  perPage: {
    type: Number,
    default: 10,
  },
  modelValue: {
    type: Number,
    default: 1,
  },
});

// Emits
const emit = defineEmits(['update:modelValue']);

// Reactive state
const currentPage = ref(props.modelValue);

Step3:分頁元件的DOM布局

以下為幾個確保數字為整數的 help function,用來處理計算分頁時不要出錯,說明詳見註解

// Constants
const DEFAULT_PER_PAGE = 10;
const DEFAULT_TOTAL_ROWS = 0;

// Helper functions
//若 toInteger(value) 轉換後為 0、NaN、null、undefined,則會回傳 DEFAULT_PER_PAGE(預設每頁顯示的數量)。
//如果轉換後有數值,則使用該數值。
const sanitizePerPage = (value) =>
  // 確保 perPage 至少是 1,避免無效的 0 或負數
  Math.max(toInteger(value) || DEFAULT_PER_PAGE, 1);

//若轉換後的數值為 0、NaN、null、undefined,則使用 DEFAULT_TOTAL_ROWS(預設總筆數)。
//若有正常數值,則使用該數值。
const sanitizeTotalRows = (value) =>
  // 確保 totalRows 至少是 0,因為總筆數不能是負數。
  Math.max(toInteger(value) || DEFAULT_TOTAL_ROWS, 0);

const toInteger = (value, defaultValue = NaN) => {
  //轉換為整數,忽略小數和非數字部分。
  //強制使用十進位,避免舊瀏覽器誤判 0 或 0x 為其他進位。
  //轉換失敗時回傳 NaN。
  const integer = parseInt(value, 10);
  return isNaN(integer) ? defaultValue : integer;
};

利用 computed 算出有幾個分頁

// Computed properties 計算總頁數
const numberOfPages = computed(() => {
  const result = Math.ceil(
    sanitizeTotalRows(props.totalRows) / sanitizePerPage(props.perPage)
  );
  console.log(result);
  return result < 1 ? 1 : result;
});

在DOM上渲染出分頁數與綁定事件

<nav aria-label="Page navigation example">
    <ul class="pagination pagination-sm" :class="justifyContent">
      <!-- 往前的箭號 如果currentPage為1 則呈現 disabled 樣式 -->
      <li class="page-item" :class="currentPage === 1 ? 'disabled' : ''">
        <a class="page-link" @click="Previous" aria-label="Previous">
          <span aria-hidden="true"></span>
        </a>
      </li>
      <!-- 用 v-for 選染出頁次 -->
      <li
        class="page-item"
        :class="currentPage === item ? 'active' : ''"
        v-for="item in numberOfPages"
        :key="item"
      >
        <!-- 如果頁次等於 1 或是最後頁 或頁次小於等於5 或目前頁次減掉item小於2 則顯示數字 -->
        <!-- 綁定一個 onClick 事件 -->
        <a
          v-if="
            Math.abs(currentPage - item) < 2 ||
            item === 1 ||
            item === numberOfPages ||
            numberOfPages <= 5
          "
          class="page-link"
          @click="onClick(item)"
          >{{ item }}</a
        >
        <a v-else-if="Math.abs(currentPage - item) === 2" class="page-link"
          >...</a
        >
      </li>
      <!-- 往前的箭號 如果currentPage為總頁數 則呈現 disabled 樣式 -->
      <li
        class="page-item"
        :class="currentPage === numberOfPages ? 'disabled' : ''"
      >
        <a class="page-link" @click="Next" aria-label="Next">
          <span aria-hidden="true"></span>
        </a>
      </li>
    </ul>
  </nav>

Step4: 分頁元件點擊事件處理

在這裡透過 emit('update:modelValue', currentPage.value),把雙向綁定的 currentPage 透過事件傳遞給表格元件。

// Methods 
// 往前一頁
const Previous = () => {
  if (currentPage.value !== 1) {
    currentPage.value -= 1;
    emit('update:modelValue', currentPage.value);
  }
};
// 往後一頁
const Next = () => {
  if (currentPage.value !== numberOfPages.value) {
    currentPage.value += 1;
    emit('update:modelValue', currentPage.value);
  }
};
// 點擊分頁
const onClick = (pageNumber) => {
  console.log(pageNumber);
  if (pageNumber === currentPage.value) return;
  currentPage.value = pageNumber;
  emit('update:modelValue', currentPage.value);
};

Step5: 表格元件觀察到 currentPage 的改變,渲染分頁的資料

// 計算當前頁面的資料
const paginatedData = computed(() => {
  const start = (currentPage.value - 1) * perPage.value;
  return tempData.value.slice(start, start + perPage.value);
});

備註:在 VUE 3.4 之後,使用元件 v-model 的時候,可以不用再 const emit = defineEmits(['update:modelValue']);

而是使用 defineModel(),讓後面的事件更為直覺。

  const currentPage = defineModel();

在點擊事件上也要把 emit('update:modelValue', currentPage.value); 拿掉

// Methods 
// 往前一頁
const Previous = () => {
  if (currentPage.value !== 1) {
    currentPage.value -= 1;
  }
};
// 往後一頁
const Next = () => {
  if (currentPage.value !== numberOfPages.value) {
    currentPage.value += 1;
  }
};
// 點擊分頁
const onClick = (pageNumber) => {
  console.log(pageNumber);
  if (pageNumber === currentPage.value) return;
  currentPage.value = pageNumber;
};

範例檔案