0%

【套件筆記 - JS】頁碼 Pagination.js

Pagintaion 套件 + Axios API 請求範例
Pagination_00

一、官網資源

pagination 官網:https://pagination.js.org/docs/index.html
可由此處下載 css 原始檔客製 https://pagination.js.org/docs/index.html#Theme

二、CDN 安裝

頁碼 pagination.scss(非必要引入,也可以從官網下載原始檔更改)

1
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.6.0/pagination.min.css">

jQuery Import(使用 slim.min.js 版本會出錯)

1
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>

頁碼 pagination.js

1
<script src="https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.6.0/pagination.min.js"></script>

Bootstrap 5 CSS(建立表格用,非必要)

1
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">

三、使用範例

https://codepen.io/annchou_illu/pen/oNmZxmd
以下以政府 API 做為資料來源,搭配 Bootstrap 5 切版

3-1. 目標功能

  • 頁碼與資料渲染
  • 簡單篩選功能
  • 點選至多 3 筆資料

3-2. 示範步驟

  • 規劃版位
  • 變數、原始資料宣告、按鈕綁定
  • 撰寫初始化渲染函式 renderData
    。原始資料請求
    。篩選資料生成
    。頁碼生成
  • 拆解元件
  • 加入點選物件功能

3-3. 實作開始

規劃版位 html

最主要是先建立好渲染資料和頁碼生成位置,完整 html 可見 codepen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 被點選物件顯示區 -->
<p class="bg-light">你選擇的專案 seq:<span class="showCheckedList"></span></p>

<!-- 篩選按鈕區 -->
<div class="d-flex justify-content-end gap-1 mb-3">
<input type="button" class="btn btn-secondary btnFilter" value="全部" />
<input type="button" class="btn btn-secondary btnFilter" value="前鎮區" />
<input type="button" class="btn btn-secondary btnFilter" value="三民區" />
</div>

<!-- 資料生成位置與內容模板 -->
<div id="pagination-Container">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between">
<div>
<p>專案名稱</p>
<p>專案區域</p>
</div>
<input class="form-check-input" type="checkbox" id="checkProject" />
</li>
</ul>
</div>
1
2
<!-- 頁碼 -->
<div id="pagination-Pages" class="d-flex justify-content-end mt-3"></div>

基本前置設定

於 JavaScript 準備 API 路徑來源宣告,以及將 DOM 元素綁定監聽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const _url =
'https://api.kcg.gov.tw:443/api/service/Get/1f2a6afe-f953-436f-981c-92f2739b3475'
// 顯示選取專案
const showCheckedList = document.querySelector('.showCheckedList')
const checkedProjectList = [] // 被選取清單

// 初始篩選分類
let selectedFilter = '全部'
// 按鈕篩選處理
const btnFilter = document.querySelectorAll('.btnFilter')
// 按鈕監聽
btnFilter.forEach((btn) => {
btn.addEventListener('click', function () {
selectedFilter = btn.value // 當按鈕按下,selectedFilter 的值會被改變
renderData() // 每次被按下都會一再觸發 renderData()
})
})

了解 Pagination.js 和 jQuery 文件。

Pagintaion.js docs/commenlyused 第三項提到 ajax 的請求格式如下。並且套件支援以 jQuery ajax 操作 API 來源資料,如果請求成功則使用 success 接收並繼續執行,請求失敗則跳到 error

1
2
3
4
5
6
7
8
9
dataSource: function(done){
$.ajax({
type: 'GET',
url: '/test.json',
success: function(response){
done(response);
}
});
}

建立初始化 function renderData 基本架構

這時就可以組合出大概的渲染函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function renderData() {
$(function () {
(function (name) {
let container = $("#pagination-" + name); // 這裡面包裝了套件所需設定,暫時不用理會
$.ajax({
url: _url, // 資料來源 API 網址
success: function (data) {
... // 請求成功後執行
},
error: function (error) {
console.error(error); // 請求失敗時執行
}
});
})("Pages");
});
}

renderData(); // 一啟動網頁就運行

儲存 API 請求回傳的原始資料

使用 console 測試,發現我們需要撈回的資料被放在 data 中的 data 屬性,所以 data.data 是我們所需要的,所以宣告一個變數儲存它。

1
2
3
success: function (data) {
const allData = data.data//宣告一個 allData 變數來存放 API 回傳的全部資料
},

撰寫篩選按鈕動作

因為我們在 html 中寫了三個按鈕,讓它可以做區域篩選,並且上面已經先宣告了變數 selectedFilter,預設值為「全部」。當 selectedFilter 變數的值已經不是「全部」,則將 filteredData 重新賦值成符合篩選條件的資料。

1
2
3
4
let filteredData = [] // 真正要被渲染的已篩選資料
selectedFilter !== '全部' // 我們在步驟 2 時宣告過全域,並設定初始值為"全部"
? (filteredData = allData.filter((item) => item.name === selectedFilter))
: (filteredData = allData)

將按鈕加入點擊監聽

測試上述的 selectedFilter 有沒有因點擊而更改。因為每次點擊按鈕都要重新利用 renderData 函式處理,所以我們在執行函式中再度呼叫 renderData

1
2
3
4
5
6
btnFilter.forEach((btn) => {
btn.addEventListener('click', function () {
selectedFilter = btn.value
renderData()
})
})

目前我們的 renderData 函式全部是以下狀態,並且加入 console 測試 selectedFilterfilteredData 值是否有跟著按鈕一起改變。確定成功就可以將 console 刪除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function renderData() {
$(function () {
(function (name) {
let container = $("#pagination-" + name); // 這裡面包裝了套件所需設定,暫時不用理會
$.ajax({
url: _url, // 資料來源 API 網址
success: function (data) {
const allData = data.data// 宣告一個 allData 變數來存放 API 回傳的全部資料

let filteredData = []; // 真正要被渲染的已篩選資料
selectedFilter !== "全部"
? (filteredData = allData.filter((item) => item.name === selectedFilter))
: (filteredData = allData);

console.log(selectedFilter)
console.log(filteredData)
},
error: function (error) {
console.error(error); // 請求失敗時執行
}
});
})("Pages");
});
}

計算頁碼與設定

接著用篩選後的資料計算可以生成幾頁頁碼,以下程式碼可以接在剛才 filteredData console 的下面。回到套件 docs/methods,發現我們可以透過 container.pagination({ ... }) 設定套件呈現細節。以下屬性詳細請參照官網文件。

1
2
3
4
5
6
7
8
9
container.pagination({
dataSource: filteredData, //我們剛才建立的實際渲染資料
//locator: "data", // 資料來源中的屬性,因為上面使用的 filterData 已經是處理過的資料,所以可忽略設定
totalNumber: filteredData.length, //實際資料總數
pageSize: 5, //每頁資料數
showPageNumbers: true,
showPrevious: true,
showNext: true
})

渲染頁碼與資料

回到官網文件 docs/dataSourcescontainer 中的 callback 屬性可用於插入自訂的 html,所以我們可以將上面 html 要渲染的資料 html 模板貼下來。input 中的各項屬性視原始資料內容加上獨特值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
container.pagination({
//...略

//頁碼渲染與監聽
callback: function (res, pagination) {
let dataHtml = '<ul class="list-group">'

$.each(res, function (index, item) {
dataHtml += `<li class="list-group-item d-flex justify-content-between">
<div>
<p>${item.projectName}</p>
<p>${item.name}</p>
</div>
</li>`
})

dataHtml += '</ul>'
$('#pagination-Container').html(dataHtml) // 設定 HTML 內容到 container​
}
})

最後補上執行 renderData,讓網頁一開啟就運行。就完成目標功能的第一二項,已經可以作頁碼切換與按鈕篩選資料了。

1
2
3
function renderData(){...略}​

renderData();

拆元件

因為目前 function renderData 內的程式碼冗長,所以先來做拆分整理。我們剛才在 function renderData 內的程式碼大致可以分為

  • 基礎架構
  • 資料依據按鈕篩選:需要輸入資料 allData,並生成 filteredData
  • 將篩選過的資料計算頁碼:需要輸入這兩筆資料 filteredDatacontainer
  • 渲染頁碼與資料:需要輸入資料 res
    依據以上分類可以拆成四個函式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 基礎架構
function renderData() {
$(function () {
;(function (name) {
let container = $('#pagination-' + name)
$.ajax({
url: _url,
success: function (data) {
// 將完整資料交給 handleFilterData 函式篩選
const allData = data.data
const filteredData = filterData(allData)
// 將返回的 filteredData 計算頁碼
handlePagination(filteredData, container)
},
error: function (error) {
console.error(error)
}
})
})('Pages')
})
}
// 資料依據按鈕篩選:需要輸入資料 allData,並生成 filteredData。​
function filterData(data) {
let filteredData = []
selectedFilter !== '全部'
? (filteredData = data.filter((item) => item.name === selectedFilter))
: (filteredData = data)
return filteredData
}
//使用篩選資料,計算頁碼
function handlePagination(filteredData, container) {
container.pagination({
dataSource: filteredData, // 實際資料來源
//locator: "data", // 資料來源中的屬性,因為上面使用的 filterData 已經是處理過的資料,所以可忽略設定
totalNumber: filteredData.length, // 實際資料總數
pageSize: 5, // 每頁資料數
showPageNumbers: true,
showPrevious: true,
showNext: true,

// 頁碼渲染與監聽
callback: function (res, pagination) {
//渲染頁碼與資料
renderPagination(res)
}
})
}
// 渲染頁碼與資料
function renderPagination(res) {
let dataHtml = '<ul class="list-group">'
$.each(res, function (index, item) {
dataHtml += `<li class="list-group-item d-flex justify-content-between">
<div>
<p>${item.projectName}</p>
<p>${item.name}</p>
</div>
</li>`
})

dataHtml += '</ul>'
$('#pagination-Container').html(dataHtml) // 設定 HTML 內容到 container
}

加入點選功能的 checkbox

先從渲染 checkbox 開始,修改剛才建立好的 function renderPagination 中的 dataHtml

1
2
3
4
5
6
7
8
9
$.each(res, function (index, item) {
dataHtml += `<li class="list-group-item d-flex justify-content-between">
<div>
<p>${item.projectName}</p>
<p>${item.name}</p>
</div>
<input class="form-check-input" type="checkbox">
</li>`
})

checkbox 加入辨識值

每一個 checkbox 都需要被辨識,觀察 $.each(res, function (index, item) 中的 item 可以發現 item.projectSEQ 這個唯一值可以被使用,所以我們可藉由 data-*(MDN) 的方式加入此值。

1
2
3
4
5
6
7
8
9
10
11
$.each(res, function (index, item) {
dataHtml += `<li class="list-group-item d-flex justify-content-between">
<div>
<p>${item.projectName}</p>
<p>${item.name}</p>
</div>
<input class="form-check-input" type="checkbox"
id="checkProject${item.projectSEQ}"
data-seq=${item.projectSEQ}> //這裡加入 data-seq
</li>`
})

儲存被點選的 checkbox

完成以上步驟後,我們已經可以點選 checkbox,也可以換頁。但如果回到上一頁,會發現剛才點選的 checkbox 狀態因為換頁資料重新渲染,導致失去被點選狀態。因此我們要紀錄有哪些 checkbox 被點選過,之後才能在渲染時判定每個 checkbox 該呈現什麼狀態。
步驟 2 中,我們有宣告一個 checkedProjectList 變數用來儲存被點選的 checkbox 清單。現在我們建立一個函式來將資料寫入變數中。判讀被點選的 checkbox data-seq,將 seq 存入。讀取 data-* 的方法使用 checkbox.getAttribute("data-seq"),或 checkbox.dataset.seq 都可以。
另外特別注意的是,我們從 API 取得回來的 projectSEQ 參數是 number 型別,所以我們在這裡存入 checkedProjectList 變數的值也必須轉成 number 型別才不會之後影響判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handleCheckProject() {
// 將所有 checkbox 綁定
const checkBoxes = document.querySelectorAll("input[type='checkbox']");

// 因為 checkBoxes 有多個 checkbox,所以必須先使用 forEach 才能 addEventListener 監聽
checkBoxes.forEach(checkbox => {
checkbox.addEventListener("change", () => {
const projectSeq = checkbox.getAttribute("data-seq");
checkedProjectList.push(+projectSeq) // 使用「+」轉 number

//因為步驟 2 中已經有做 DOM 綁定,所以在這裡將儲存的資料渲染
showCheckedList.textContent = checkedProjectList.join("、");
})
})
}​

回到 function handlePagination 函式,在 callback 處加入 handleCheckProject(),讓它能夠執行。

1
2
3
4
5
6
7
8
9
10
11
function handlePagination(filteredData, container){
...略

callback: function (res, pagination) {
...略

//處理 checkbox 選擇專案
handleCheckProject();
}
});
}

Pagination_01

加入判斷當前 checkbox 是否已被點選,生成樣板

回到 function renderPagination 函式,在進入 each 之後,執行當前 item.projectSEQ 值是否存在於 checkedProjectList 變數中。includes 會返回 true/falseMDN)。
checkboxMDN)文件中提到如果是被勾選狀態,則會被加入 checked 在 HTML 中標示,所以也在 dataHTML 中使用三元運算子根據剛才 isChecked 的結果判斷是否加入 checked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function renderPagination(res) {
let dataHtml = '<ul class="list-group">'
$.each(res, function (index, item) {
// 是否已經被選取
const isChecked = checkedProjectList.includes(item.projectSEQ)

dataHtml += `<li class="list-group-item d-flex justify-content-between"><div><p>${
item.projectName
}</p><p>${
item.name
}</p></div><input class="form-check-input" type="checkbox" id="checkProject${
item.projectSEQ
}" data-seq=${item.projectSEQ}
${isChecked ? 'checked' : ''}> // 三元運算子判斷當前 checkbox 狀態
</li>`
})

dataHtml += '</ul>'
$('#pagination-Container').html(dataHtml) // 設定 HTML 內容到 container
}

完成後的資料表即使換頁再回上一頁,也能繼續顯示相同 checkbox 勾選狀態了。但還有一些小錯誤,即使 checkbox 取消,或是再次點選都會不斷累積相同 seq 編號在 checkedProjectList 變數中。

限制選取資料數量

我們要解決上述的 bug 以及完成最後一個任務:限制最多只能選取 3 筆資料。回到 function handleCheckProject,我們要在這裡根據 checkedProjectList 變數內的資料數量判斷是否還能再繼續點選動作。

■ 我們可能有幾種情境及對應執行:
(1)已達最多選取數 → 禁止用戶點選,但因為 checkbox 沒有 disable 屬性,所以我們只能讓 checked 強制取消
(2)未達最多選取數 → 加入清單
(3)清單中已有目前點選的 seq 編號 → 判斷為取消點選,從清單中移除
(4)清單中沒有目前的 seq 編號 → 加入清單
■ 綜合以上情境狀況,我們把要情境總結成
(1)已達最多選取數,並且清單中不存在此 seq → 取消當前 checkbox 勾選狀態
(2)未達最多選取數 → 根據清單是否存在 seq,執行加入清單或刪除該值的動作 ​,
以上不管是判斷清單中是否存在當前 seq,或是執行刪除,都會需要知道該 seq 在清單中的位置,所以可以使用 indexOf 尋找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function handleCheckProject() {
const checkBoxes = document.querySelectorAll("input[type='checkbox']")
const maxChecked = 3 // 最多選取數

checkBoxes.forEach((checkbox) => {
checkbox.addEventListener('change', () => {
const projectSeq = checkbox.getAttribute('data-seq')

// 判斷目前的 seq 在 checkedProjectList 中的 index,若無則返回 -1
const findCheckBoxIndex = checkedProjectList.indexOf(+projectSeq)

// 如果已達最多選取數,並且清單中不存在此 seq
if (
checkedProjectList.length === maxChecked &&
findCheckBoxIndex === -1
) {
checkbox.checked = false
} else {
// 未達最多選取數​
findCheckBoxIndex === -1
? checkedProjectList.push(+projectSeq)
: checkedProjectList.splice(findCheckBoxIndex, 1)
}

//渲染選取專案
showCheckedList.textContent = checkedProjectList.join('、')
})
})
}

完成!

四、結尾

這是我原本發布在六角學院 2023 JS 直播班討論區分享的心得,現移植到部落格存放,謝謝 Paul 提供了改寫優化。

五、資料參考

Paul 版本 codepen:https://codepen.io/paul-1997/pen/xxMqowP

本文原發表於 Vocus,此篇為個人 blog 備份。