Làm thế nào để xây dựng một danh sách được cải tiến dần dần, có thể truy cập, có thể lọc và phân trang

theanh

Administrator
Nhân viên
Hầu hết các trang web tôi xây dựng đều là các trang web tĩnh với các tệp HTML được tạo bởi trình tạo trang web tĩnh hoặc các trang được phục vụ trên máy chủ bởi CMS như Wordpress hoặc CraftCMS. Tôi chỉ sử dụng JavaScript để nâng cao trải nghiệm của người dùng. Tôi sử dụng JavaScript cho những thứ như tiện ích tiết lộ, accordion, điều hướng bay ra hoặc hộp thoại.

Yêu cầu đối với hầu hết các tính năng này đều đơn giản, vì vậy việc sử dụng thư viện hoặc khung sẽ là quá mức cần thiết. Tuy nhiên, gần đây, tôi thấy mình rơi vào tình huống mà việc viết một thành phần từ đầu trong Vanilla JS mà không có sự trợ giúp của một khung sẽ quá phức tạp và lộn xộn.

Khung nhẹ​

Nhiệm vụ của tôi là thêm nhiều bộ lọc, sắp xếp và phân trang vào danh sách các mục hiện có. Tôi không muốn sử dụng một JavaScript Framework như Vue hay React, chỉ vì tôi cần trợ giúp ở một số nơi trên trang web của mình và tôi không muốn thay đổi ngăn xếp của mình. Tôi đã tham khảo Twitter và mọi người đề xuất các framework tối giản như lit, petite-vue, hyperscript, htmx hoặc Alpine.js. Tôi đã chọn Alpine vì nghe có vẻ như đây chính xác là thứ tôi đang tìm kiếm:
“Alpine là một công cụ mạnh mẽ, tối giản để soạn thảo hành vi trực tiếp trong mã đánh dấu của bạn. Hãy nghĩ về nó như jQuery cho web hiện đại. Thêm một thẻ script và bắt đầu.”

Alpine.js​

Alpine là một bộ sưu tập nhẹ (~7KB) gồm 15 thuộc tính, 6 thuộc tính và 2 phương thức. Tôi sẽ không đi sâu vào những điều cơ bản của nó (hãy xem bài viết này về Alpine của Hugo Di Francesco hoặc đọc tài liệu Alpine), nhưng hãy để tôi giới thiệu nhanh cho bạn về Alpine:

Lưu ý: Bạn có thể bỏ qua phần giới thiệu này và đi thẳng đến nội dung chính của bài viết nếu bạn đã quen thuộc với Alpine.js.

Giả sử chúng ta muốn biến một danh sách đơn giản có nhiều mục thành tiện ích tiết lộ. Bạn có thể sử dụng các thành phần HTML gốc: chi tiết và tóm tắt cho mục đích đó, nhưng đối với bài tập này, tôi sẽ sử dụng Alpine.

Theo mặc định, khi JavaScript bị tắt, chúng tôi sẽ hiển thị danh sách, nhưng chúng tôi muốn ẩn nó và cho phép người dùng mở và đóng nó bằng cách nhấn nút nếu JavaScript được bật:
Mã:
[HEADING=2]Beastie Boys Anthology[/HEADING]
The Sounds of Science là album tuyển tập đầu tiên của nhóm nhạc rap rock Mỹ Beastie Boys gồm các bản hit hay nhất, B-side và các bản nhạc chưa phát hành trước đó.
[LIST=1] 
[*] Beastie Boys 
[*] Slow And Low 
[*] Shake Your Rump 
[*] Gratitude 
[*] Skills To Pay The Bills 
[*] Root Down 
[*] Believe Me …[/LIST]
Đầu tiên, chúng tôi bao gồm Alpine bằng cách sử dụng thẻ script. Sau đó, chúng tôi gói danh sách trong div và sử dụng chỉ thị x-data để truyền dữ liệu vào thành phần. Thuộc tính open bên trong đối tượng mà chúng ta đã truyền có sẵn cho tất cả các phần tử con của div:
Mã:
 [LIST=1] 
[*] Beastie Boys 
[*] Slow And Low … [/LIST]
Chúng ta có thể sử dụng thuộc tính open cho chỉ thị x-show, chỉ thị này xác định xem một phần tử có hiển thị hay không:
Mã:
 [LIST=1] 
[*] Beastie Boys 
[*] Slow And Low … [/LIST]
Vì chúng ta đặt open thành false, nên danh sách hiện đã ẩn.

Tiếp theo, chúng ta cần một nút để chuyển đổi giá trị của thuộc tính open. Chúng ta có thể thêm sự kiện bằng cách sử dụng lệnh x-on:click hoặc @-Syntax ngắn hơn @click:
Mã:
 Tracklist [LIST=1] 
[*] Beastie Boys 
[*] Slow And Low … [/LIST]
Khi nhấn nút, open giờ sẽ chuyển đổi giữa falsetruex-show sẽ theo dõi những thay đổi này một cách phản ứng, hiển thị và ẩn danh sách cho phù hợp.

Mặc dù điều này có hiệu quả với người dùng bàn phím và chuột, nhưng lại vô dụng với người dùng trình đọc màn hình vì chúng ta cần truyền đạt trạng thái của tiện ích. Chúng ta có thể thực hiện điều đó bằng cách chuyển đổi giá trị của thuộc tính aria-expanded:
Mã:
 Tracklist
Chúng ta cũng có thể tạo kết nối ngữ nghĩa giữa nút và danh sách bằng cách sử dụng aria-controls cho trình đọc màn hình hỗ trợ thuộc tính:
Mã:
 Tracklist[LIST=1] …[/LIST]
Đây là kết quả cuối cùng:

Xem Bút [Tiện ích tiết lộ đơn giản với Alpine.js](https://codepen.io/smashingmag/pen/xxpdzNz) của Manuel Matuzovic.
Xem Bút Tiện ích tiết lộ đơn giản với Alpine.js của Manuel Matuzovic.
Thật tuyệt! Bạn có thể cải thiện nội dung tĩnh hiện có bằng JavaScript mà không cần phải viết một dòng JS nào. Tất nhiên, bạn có thể cần phải viết một số JavaScript, đặc biệt là nếu bạn đang làm việc trên các thành phần phức tạp hơn.

Danh sách phân trang tĩnh​

Được rồi, bây giờ chúng ta đã biết những điều cơ bản về Alpine.js, tôi cho rằng đã đến lúc xây dựng một thành phần phức tạp hơn.
Lưu ý: Bạn có thể xem kết quả cuối cùng trước khi chúng ta bắt đầu.
Tôi muốn xây dựng một danh sách phân trang các đĩa than của mình hoạt động mà không cần JavaScript. Chúng tôi sẽ sử dụng trình tạo trang web tĩnh eleventy (hay viết tắt là “11ty”) cho mục đích đó và Alpine.js để cải thiện nó bằng cách làm cho danh sách có thể lọc được.


Thiết lập​

Trước khi bắt đầu, chúng ta hãy thiết lập trang web của mình. Chúng ta cần:
  • một thư mục dự án cho trang web của chúng ta,
  • 11ty để tạo các tệp HTML,
  • một tệp đầu vào cho HTML của chúng ta,
  • một tệp dữ liệu chứa danh sách các bản ghi.
Trên dòng lệnh của bạn, điều hướng đến thư mục mà bạn muốn lưu dự án, tạo một thư mục và cd vào đó:
Mã:
cd Sites # hoặc bất cứ nơi nào bạn muốn lưu dự ánmkdir myrecordcollection # chọn bất kỳ tên nàocd myrecordcollection
Sau đó, tạo một tệp package.jsoncài đặt eleventy:
Mã:
npm init -ynpm install @11ty/eleventy
Tiếp theo, tạo một Tệp index.njk (.njk có nghĩa là đây là tệp Nunjucks; thông tin chi tiết bên dưới) và thư mục _datarecords.json:
Mã:
touch index.njkmkdir _datatouch _data/records.json
Bạn không cần phải thực hiện tất cả các bước này trên dòng lệnh. Bạn cũng có thể tạo thư mục và tệp trong bất kỳ giao diện người dùng nào. Cấu trúc tệp và thư mục cuối cùng trông như thế này:


Thêm nội dung​

11ty cho phép bạn viết nội dung trực tiếp vào tệp HTML (hoặc Markdown, Nunjucks và các ngôn ngữ mẫu khác). Bạn thậm chí có thể lưu trữ dữ liệu trong phần nội dung hoặc trong tệp JSON. Tôi không muốn quản lý hàng trăm mục nhập theo cách thủ công, vì vậy tôi sẽ lưu trữ chúng trong tệp JSON mà chúng ta vừa tạo. Hãy thêm một số dữ liệu vào tệp:
Mã:
[ { "artist": "Akne Kid Joe", "title": "Die große Palmöllüge", "year": 2020 }, { "artist": "Bring me the Horizon", "title": "Post Human: Survial Horror", "year": 2020 }, { "artist": "Idles", "title": "Joy as an Act of Resistance", "year": 2018 }, { "artist": "Beastie Boys", "title": "Được cấp phép cho Ill", "year": 1986 }, { "artist": "Beastie Boys", "title": "Paul's Boutique", "year": 1989 }, { "artist": "Beastie Boys", "title": "Check Your Head", "year": 1992 }, { "artist": "Beastie Boys", "title": "Ill Communication", "year": 1994 }]
Cuối cùng, hãy thêm một cấu trúc HTML cơ bản vào tệp index.njk và bắt đầu eleventy:
Mã:
   My Record Collection [HEADING=1]My Record Collection[/HEADING]
Bằng cách chạy lệnh sau, bạn sẽ có thể truy cập trang web tại http://localhost:8080:
Mã:
eleventy --serve


Hiển thị nội dung​

Bây giờ chúng ta hãy lấy dữ liệu từ tệp JSON của mình và chuyển nó thành HTML. Chúng ta có thể truy cập nó bằng cách lặp qua đối tượng records trong nunjucks:
Mã:
 [LIST=1] {% for record in records %} 
[*]  [B]{{ record.title }}[/b]
 Phát hành vào {{ record.year }} bởi {{ record.artist }}.  {% endfor %} [/LIST]


Phân trang​

Eleventy hỗ trợ phân trang ngay khi cài đặt. Tất cả những gì chúng ta phải làm là thêm một khối frontmatter vào trang của mình, cho 11ty biết tập dữ liệu nào sẽ được sử dụng để phân trang và cuối cùng, chúng ta phải điều chỉnh vòng lặp for của mình để sử dụng danh sách được phân trang thay vì tất cả các bản ghi:
Mã:
---pagination: data: records size: 5---    My Record Collection   [HEADING=1]My Record Collection[/HEADING]  
Hiển thị {{ records.length }} bản ghi
  [LIST=1] {% for record in pagination.items %} 
[*]  [B]{{ record.title }}[/b]
 Đã phát hành vào {{ record.year }} bởi {{ record.artist }}.  {% endfor %} [/LIST]
Nếu bạn truy cập lại trang, danh sách chỉ chứa 5 mục. Bạn cũng có thể thấy rằng tôi đã thêm một thông báo trạng thái (bỏ qua phần tử output lúc này), gói danh sách trong div với role "region", và tôi đã gắn nhãn cho nó bằng cách tạo tham chiếu đến #message bằng aria-labelledby. Tôi đã làm như vậy để biến nó thành landmark và cho phép người dùng trình đọc màn hình truy cập trực tiếp vào danh sách kết quả bằng phím tắt.

Tiếp theo, chúng ta sẽ thêm một điều hướng có liên kết đến tất cả các trang được tạo bởi trình tạo trang tĩnh. Đối tượng pagination giữ một mảng chứa tất cả các trang. Chúng ta sử dụng aria-current="page" để làm nổi bật trang hiện tại:
Mã:
 [LIST=1] {% for page_entry in pagination.pages %} {%- set page_url = pagination.hrefs[loop.index0] -%} 
[*]  [URL={{ page_url }}] Trang {{ loop.index }} [/URL]  {% endfor %} [/LIST]
Cuối cùng, hãy thêm một số mã CSS cơ bản để cải thiện kiểu dáng:
Mã:
body { font-family: sans-serif; line-height: 1.5;}ol { list-style: none; margin: 0; padding: 0;}.records > * + * { margin-top: 2rem;}h2 { margin-bottom: 0;}nav { margin-top: 1.5rem;}.pages { display: flex; flex-wrap: wrap; gap: 0.5rem;}.pages a { border: 1px solid #000000; padding: 0.5rem; border-radius: 5px; display: flex; text-decoration: none;}.pages a:where([aria-current]) { background-color: #000000; color: #ffffff;}.pages a:where(:focus, :hover) { background-color: #6c6c6c; color: #ffffff;}



Bạn có thể xem nó hoạt động trong bản demo trực tiếp và bạn có thể xem mã trên GitHub.

Điều này hoạt động khá tốt với 7 bản ghi. Nó thậm chí có thể hoạt động với 10, 20 hoặc 50, nhưng tôi có hơn 400 bản ghi. Chúng ta có thể duyệt danh sách dễ dàng hơn bằng cách thêm bộ lọc.

Danh sách phân trang và lọc động​

Tôi thích JavaScript, nhưng tôi cũng tin rằng nội dung cốt lõi và chức năng của một trang web phải có thể truy cập được mà không cần JavaScript. Điều này không có nghĩa là bạn không thể sử dụng JavaScript, nó chỉ có nghĩa là bạn bắt đầu với nền tảng kết xuất máy chủ cơ bản của thành phần hoặc trang web của mình và bạn thêm chức năng từng lớp một. Đây được gọi là cải tiến tiến bộ.

Nền tảng của chúng tôi trong ví dụ này là danh sách tĩnh được tạo bằng 11ty và bây giờ chúng tôi thêm một lớp chức năng bằng Alpine.

Đầu tiên, ngay trước thẻ đóng body, chúng tôi tham chiếu đến phiên bản mới nhất (tính đến thời điểm viết 3.9.1) của Alpine.js:
Mã:
Lưu ý: Hãy cẩn thận khi sử dụng CDN của bên thứ ba, điều này có thể gây ra nhiều tác động tiêu cực (hiệu suất, quyền riêng tư, bảo mật). Hãy cân nhắc tham chiếu tệp cục bộ hoặc nhập tệp dưới dạng mô-đun.
Trong trường hợp bạn đang tự hỏi tại sao bạn không thấy hàm băm Subresource Integrity trong tài liệu chính thức, thì đó là vì tôi đã tạo và thêm hàm băm này theo cách thủ công.

Vì chúng ta đang chuyển sang thế giới JavaScript, chúng ta cần cung cấp các bản ghi của mình cho Alpine.js. Có lẽ không phải là giải pháp tốt nhất, nhưng giải pháp nhanh nhất là tạo tệp .eleventy.js trong thư mục gốc của bạn và thêm các dòng sau:
Mã:
module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("_data");};
Điều này đảm bảo rằng eleventy không chỉ tạo tệp HTML mà còn sao chép nội dung của thư mục _data vào thư mục đích của chúng ta, giúp các tập lệnh của chúng ta có thể truy cập vào thư mục đó.

Đang lấy dữ liệu​

Giống như trong ví dụ trước, chúng ta sẽ thêm lệnh x-data vào thành phần của mình để truyền dữ liệu:
Mã:
Chúng ta không có dữ liệu nào, vì vậy chúng ta cần lấy dữ liệu khi thành phần khởi tạo. Chỉ thị x-init cho phép chúng ta móc vào giai đoạn khởi tạo của bất kỳ phần tử nào và thực hiện các tác vụ:
Mã:
  […]
Nếu chúng ta xuất kết quả trực tiếp, chúng ta sẽ thấy danh sách [object Object], vì chúng ta đang lấy và nhận một mảng. Thay vào đó, chúng ta nên lặp lại danh sách bằng cách sử dụng lệnh x-for trên thẻ template và xuất dữ liệu bằng cách sử dụng x-text:
Mã:
[*]  [B][/b]
 Phát hành vào  bởi .
"Phần tử HTML là một cơ chế giữ HTML không được hiển thị ngay lập tức khi trang được tải nhưng có thể được khởi tạo sau đó trong thời gian chạy bằng JavaScript."

MDN: : Phần tử mẫu nội dung
Dưới đây là toàn bộ danh sách trông như thế nào bây giờ:
Mã:
Hiển thị {{ records.length }} bản ghi
  [LIST=1]  
[*]  [B][/b]
 Phát hành vào  bởi .   {%- for record in pagination.items %} 
[*]  [B]{{ record.title }}[/b]
 Đã phát hành vào {{ record.year }} bởi {{ record.artist }}.  {%- endfor %} [/LIST]  […]
Thật đáng kinh ngạc khi chúng tôi có thể truy xuất và xuất dữ liệu nhanh như vậy phải không? Hãy xem bản demo bên dưới để xem Alpine điền kết quả vào danh sách như thế nào.

Gợi ý: Bạn không thấy bất kỳ mã Nunjucks nào trong CodePen này, vì 11ty không chạy trên trình duyệt. Tôi vừa sao chép và dán mã HTML đã kết xuất của trang đầu tiên.

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 1](https://codepen.io/smashingmag/pen/abEWRMY) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 1 của Manuel Matuzovic.
Bạn có thể đạt được nhiều thành tựu bằng cách sử dụng Alpine chỉ thị, nhưng tại một số điểm chỉ dựa vào các thuộc tính có thể trở nên lộn xộn. Đó là lý do tại sao tôi quyết định di chuyển dữ liệu và một số logic vào một đối tượng thành phần Alpine riêng biệt.

Đây là cách thức hoạt động: Thay vì truyền dữ liệu trực tiếp, giờ đây chúng ta tham chiếu đến một thành phần bằng cách sử dụng x-data. Phần còn lại khá giống nhau: Xác định một biến để chứa dữ liệu của chúng ta, sau đó lấy tệp JSON của chúng ta trong giai đoạn khởi tạo. Tuy nhiên, chúng ta không thực hiện điều đó bên trong một thuộc tính, mà thay vào đó là bên trong thẻ hoặc tệp script:
Mã:
 […][…] document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); }, init() { this.getRecords(); } })) })
Nhìn vào CodePen trước, bạn có thể nhận thấy rằng bây giờ chúng ta có một tập dữ liệu trùng lặp. Đó là vì danh sách tĩnh 11ty của chúng ta vẫn còn đó. Alpine có một lệnh yêu cầu nó bỏ qua một số phần tử DOM nhất định. Tôi không biết điều này có thực sự cần thiết ở đây không, nhưng đây là một cách hay để đánh dấu các phần tử không mong muốn này. Vì vậy, chúng ta thêm lệnh x-ignore vào 11 mục danh sách của mình và thêm một lớp vào phần tử html khi dữ liệu đã được tải và sau đó sử dụng lớp và thuộc tính để ẩn các mục danh sách đó trong CSS:
Mã:
 .alpine [x-ignore] { display: none; }[…]{%- for record in pagination.items %} 
[*]  [B]{{ record.title }}[/b]
 Đã phát hành vào {{ record.year }} bởi {{ record.artist }}. {%- endfor %}[…] document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } })) })
11ty dữ liệu bị ẩn, kết quả đến từ Alpine, nhưng phân trang không hoạt động tại thời điểm này:

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 2](https://codepen.io/smashingmag/pen/eYyWQOe) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 2 của Manuel Matuzovic.

Phân trang​

Trước khi thêm bộ lọc, hãy phân trang dữ liệu của chúng ta. 11ty đã giúp chúng ta xử lý tất cả logic, nhưng bây giờ chúng ta phải tự mình thực hiện. Để chia dữ liệu của chúng ta thành nhiều trang, chúng ta cần những thông tin sau:
  • số mục trên mỗi trang (itemsPerPage),
  • trang hiện tại (currentPage),
  • tổng số trang (numOfPages),
  • một tập hợp con động, được phân trang của toàn bộ dữ liệu (page).
Mã:
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], itemsPerPage: 5, currentPage: 0, numOfPages: // tổng số trang, trang: // các mục được phân trang async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))})
Số lượng mục trên mỗi trang là một giá trị cố định (5) và trang hiện tại bắt đầu bằng 0. Chúng ta có được số trang bằng cách chia tổng số mục cho số mục trên mỗi trang:
Mã:
numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) // 7 / 5 = 1.4 // Math.ceil(7 / 5) = 2},
Cách dễ nhất để tôi có được các mục trên mỗi trang là sử dụng phương thức slice() trong JavaScript và lấy ra phần của tập dữ liệu mà tôi cần cho trang hiện tại:
Mã:
page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) // this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage // Trang 1: 0 * 5, (0 + 1) * 5 (=> slice(0, 5);) // Trang 2: 1 * 5, (1 + 1) * 5 (=> slice(5, 10);) // Trang 3: 2 * 5, (2 + 1) * 5 (=> slice(10, 15);)}
Để chỉ hiển thị các mục cho trang hiện tại, chúng ta phải điều chỉnh vòng lặp for để lặp qua page thay vì records:
Mã:
[LIST=1]  
[*]  [B][/b]
 Đã phát hành vào  bởi .  [/LIST]
Bây giờ chúng ta có một trang, nhưng không có liên kết nào cho phép chúng ta nhảy từ trang này sang trang khác. Giống như trước đó, chúng ta sử dụng phần tử template và chỉ thị x-for để hiển thị các liên kết trang của chúng ta:
Mã:
[LIST=1]  
[*]     {% cho page_entry trong pagination.pages %} 
[*]  […]  {% endfor %}[/LIST]
Vì chúng tôi không muốn tải lại toàn bộ trang nữa, nên chúng tôi đặt sự kiện nhấp vào mỗi liên kết, ngăn chặn hành vi nhấp mặc định và thay đổi số trang hiện tại khi nhấp:
Mã:
[URL=/][/URL]
Đây là giao diện trong trình duyệt. (Tôi đã thêm nhiều mục nhập hơn vào tệp JSON. Bạn có thể tải xuống trên GitHub.)

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 3](https://codepen.io/smashingmag/pen/GRymwjg) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 3 của Manuel Matuzovic.

Lọc​

Tôi muốn có thể lọc danh sách theo nghệ sĩ và theo thập kỷ.

Chúng tôi thêm hai phần tử select được bọc trong fieldset vào thành phần của mình và đặt lệnh x-model trên mỗi phần tử. x-model cho phép chúng tôi liên kết giá trị của phần tử đầu vào với dữ liệu Alpine:
Mã:
 Lọc theo Artist  All  Decade  Tất cả
Tất nhiên, chúng ta cũng phải tạo các trường dữ liệu này trong thành phần Alpine của mình:
Mã:
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ filters: { year: '', artist: '', }, records: [], itemsPerPage: 5, currentPage: 0, numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) }, page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) }, async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))})
Nếu chúng ta thay đổi giá trị đã chọn trong mỗi select, filters.artistfilters.year sẽ tự động cập nhật. Bạn có thể thử ở đây với một số dữ liệu giả mà tôi đã thêm thủ công:

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 4](https://codepen.io/smashingmag/pen/GRymwEp) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 4 của Manuel Matuzovic.
Bây giờ chúng ta có select phần tử và chúng tôi đã liên kết dữ liệu với thành phần của mình. Bước tiếp theo là điền động từng select với nghệ sĩ và thập kỷ tương ứng. Đối với điều đó, chúng ta lấy mảng records và thao tác dữ liệu một chút:
Mã:
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ artists: [], decade: [], // […] async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); this.artists = [...new Set(this.records.map(record => record.artist))].sort(); this.decades = [...new Set(this.records.map(record => record.year.toString().slice(0, -1)))].sort(); document.documentElement.classList.add('alpine'); }, // […] }))})
Điều này có vẻ hoang đường, và tôi chắc chắn là tôi sẽ sớm quên những gì đang diễn ra ở đây thôi, nhưng đoạn mã này sẽ lấy mảng các đối tượng và biến nó thành một mảng các chuỗi (map()), đảm bảo rằng mỗi mục là duy nhất (đó là những gì [...new Set()] thực hiện ở đây) và sắp xếp mảng theo thứ tự bảng chữ cái (sort()). Đối với mảng thập kỷ, tôi cũng sẽ cắt bỏ chữ số cuối cùng của năm vì tôi không muốn bộ lọc này quá chi tiết. Lọc theo thập kỷ là đủ tốt.

Tiếp theo, chúng ta điền thông tin cho các phần tử select của nghệ sĩ và thập kỷ, một lần nữa sử dụng phần tử template và chỉ thị x-for:
Mã:
Artist All   Decade All
Hãy tự mình thử nghiệm trong bản demo 5 trên Codepen.

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 5](https://codepen.io/smashingmag/pen/OJzmaZb) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 5 của Manuel Matuzovic.
Chúng tôi đã điền thành công các phần tử được chọn bằng dữ liệu từ tệp JSON của mình. Để lọc dữ liệu cuối cùng, chúng ta duyệt qua tất cả các bản ghi, chúng ta kiểm tra xem bộ lọc có được đặt hay không. Nếu đúng như vậy, chúng ta kiểm tra xem trường tương ứng của bản ghi có tương ứng với giá trị đã chọn của bộ lọc hay không. Nếu không, chúng ta lọc bản ghi này ra. Chúng ta còn lại một mảng đã lọc khớp với tiêu chí:
Mã:
get filteredRecords() { const filtered = this.records.filter((item) => { for (var key in this.filters) { if (this.filters[key] === '') { continue } if(!String(item[key]).includes(this.filters[key])) { return false } } return true }); trả về đã lọc}
Để điều này có hiệu lực, chúng ta phải điều chỉnh các hàm numOfPages()page() của mình để chỉ sử dụng các bản ghi đã lọc:
Mã:
numOfPages() { return Math.ceil(this.filteredRecords.length / this.itemsPerPage)},page() { return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)},
Xem Bút [Phân trang + Lọc với Bước Alpine.js 6](https://codepen.io/smashingmag/pen/GRymwQZ) của Manuel Matuzovic.
Xem Bút Phân trang + Lọc bằng Alpine.js Bước 6 của Manuel Matuzovic.
Còn ba việc phải làm:
  1. sửa lỗi;
  2. ẩn biểu mẫu;
  3. cập nhật thông báo trạng thái.

Sửa lỗi: Xem Thuộc tính Thành phần​

Khi bạn mở trang đầu tiên, hãy nhấp vào trang 6, sau đó chọn “1990” — bạn không thấy bất kỳ kết quả nào. Đó là vì bộ lọc của chúng ta nghĩ rằng chúng ta vẫn đang ở trang 6, nhưng 1) chúng ta thực sự đang ở trang 1 và 2) không có trang 6 nào có "1990" đang hoạt động. Chúng ta có thể khắc phục điều đó bằng cách đặt lại currentPage khi người dùng thay đổi một trong các bộ lọc. Để theo dõi các thay đổi trong đối tượng filter, chúng ta có thể sử dụng cái gọi là phương thức ma thuật:
Mã:
init() { this.getRecords(); this.$watch('filters', filter => this.currentPage = 0);}
Mỗi khi thuộc tính filter thay đổi, currentPage sẽ được đặt thành 0.

Ẩn biểu mẫu​

Vì các bộ lọc chỉ hoạt động khi JavaScript được bật và hoạt động, chúng ta nên ẩn toàn bộ biểu mẫu khi không phải trường hợp đó. Chúng ta có thể sử dụng lớp .alpine mà chúng ta đã tạo trước đó cho mục đích đó:
Mã:
 […]
Mã:
.filters { display: block;}html:not(.alpine) .filters { visibility: hidden;}
Tôi đang sử dụng visibility: hidden thay vì hidden chỉ để tránh nội dung bị dịch chuyển trong khi Alpine vẫn đang tải.

Thông báo thay đổi​

Thông báo trạng thái ở đầu danh sách của chúng ta vẫn ghi là "Hiển thị 7 bản ghi", nhưng thông báo này không thay đổi khi người dùng thay đổi trang hoặc lọc danh sách. Có hai điều chúng ta phải làm để làm cho đoạn văn trở nên động: liên kết dữ liệu với nó và truyền đạt những thay đổi cho công nghệ hỗ trợ (ví dụ: trình đọc màn hình).

Đầu tiên, chúng ta liên kết dữ liệu với phần tử output trong đoạn văn thay đổi dựa trên trang hiện tại và bộ lọc:
Mã:
Hiển thị {{ records.length }} records
Mã:
Alpine.data('collection', () => ({ message() { return `${this.filteredRecords.length} records`; },// […]
Tiếp theo, chúng ta muốn truyền đạt cho trình đọc màn hình rằng nội dung trên trang đã thay đổi. Có ít nhất hai cách để thực hiện điều đó:
  1. Chúng ta có thể biến một phần tử thành cái gọi là vùng trực tiếp bằng cách sử dụng thuộc tính aria-live. Vùng trực tiếp là phần tử thông báo nội dung của nó cho trình đọc màn hình mỗi khi nó thay đổi.
    Mã:
    Những thay đổi động sẽ được thông báo
    Trong trường hợp của chúng ta, chúng ta không cần phải làm gì cả, vì chúng ta đã sử dụng phần tử output (bạn còn nhớ không?) là vùng trực tiếp ngầm định theo mặc định.
    Mã:
    Hiển thị {{ records.length }} records
    ```
    “Phần tử HTML là phần tử chứa mà trang web hoặc ứng dụng có thể đưa kết quả tính toán hoặc kết quả của người dùng vào hành động.”

    Nguồn: : Phần tử đầu ra, MDN Web Docs
    ```
  2. Chúng ta có thể làm cho vùng có thể lấy nét được và di chuyển tiêu điểm đến vùng đó khi nội dung của vùng đó thay đổi. Vì vùng được gắn nhãn, tên và vai trò của vùng đó sẽ được thông báo khi điều đó xảy ra.
    Mã:
    Chúng ta có thể tham chiếu đến vùng bằng cách sử dụng chỉ thị x-ref.
    Mã:
Tôi đã quyết định thực hiện cả hai:
  1. Khi người dùng lọc trang, chúng tôi cập nhật vùng trực tiếp, nhưng chúng tôi không di chuyển tiêu điểm.
  2. Khi họ thay đổi trang, chúng tôi di chuyển tiêu điểm đến danh sách.
Vậy là xong. Sau đây là kết quả cuối cùng:

Xem Bút [Phân trang + Bộ lọc với Alpine.js Bước 7](https://codepen.io/smashingmag/pen/zYpwMXX) của Manuel Matuzovic.
Xem Bút Phân trang + Bộ lọc với Alpine.js Bước 7 của Manuel Matuzovic.
Lưu ý: Khi bạn lọc theo nghệ sĩ và thông báo trạng thái hiển thị "1 bản ghi" và bạn lọc lại theo một nghệ sĩ khác, cũng chỉ có một bản ghi, nội dung của phần tử đầu ra không thay đổi và không có thông báo nào được báo cáo cho trình đọc màn hình. Điều này có thể được coi là lỗi hoặc là tính năng để giảm thông báo trùng lặp. Bạn sẽ phải thử nghiệm điều này với người dùng.

Tiếp theo là gì?​

Những gì tôi đã làm ở đây có vẻ trùng lặp, nhưng nếu bạn giống tôi và không đủ tin tưởng vào JavaScript, thì nó đáng để nỗ lực. Và nếu bạn xem CodePen cuối cùng hoặc mã hoàn chỉnh trên GitHub, thực ra không cần quá nhiều công sức. Các khuôn khổ tối giản như Alpine.js giúp dễ dàng cải thiện dần dần các thành phần tĩnh và khiến chúng phản ứng.

Tôi khá hài lòng với kết quả, nhưng vẫn còn một vài điều nữa có thể cải thiện:
  1. Phân trang có thể thông minh hơn (số trang tối đa, liên kết trước và sau, v.v.).
  2. Cho phép người dùng chọn số mục trên mỗi trang.
  3. Sắp xếp sẽ là một tính năng hay.
  4. Làm việc với API lịch sử sẽ rất tuyệt.
  5. Có thể cải thiện việc chuyển đổi nội dung.
  6. Giải pháp cần được người dùng thử nghiệm và trình duyệt/trình đọc màn hình thử nghiệm.
P.S. Vâng, tôi biết, Alpine tạo ra HTML không hợp lệ với cú pháp thuộc tính x- tùy chỉnh của nó. Điều đó làm tôi đau lòng như nó làm bạn đau lòng, nhưng miễn là nó không ảnh hưởng đến người dùng, tôi có thể chấp nhận được. :)

P.S.S. Xin gửi lời cảm ơn đặc biệt đến Scott, Søren, Thain, David, Saptak và Christian vì phản hồi của họ.

Các nguồn tài nguyên khác​

Đọc thêm​

  • Chuyển đổi các mục nhập lớp trên cùng và thuộc tính hiển thị trong CSS
  • Cách OWASP giúp bạn bảo mật các ứng dụng web toàn bộ ngăn xếp của mình
  • Tạo biểu mẫu nhiều bước hiệu quả để có trải nghiệm người dùng tốt hơn
  • Tầm quan trọng của sự suy giảm duyên dáng trong thiết kế giao diện có thể truy cập
 
Back
Bên trên