Tôi phải thú nhận một điều — tôi không thích hộp thoại modal (hay gọi tắt là "modal"). "Ghét" có lẽ là một từ quá mạnh để sử dụng, nhưng hãy nói rằng không có gì khiến người ta mất hứng hơn khi bắt đầu đọc một bài viết hơn là bị "tát vào mặt" bằng một cửa sổ modal trước khi tôi thậm chí bắt đầu hiểu những gì mình đang xem.
Hoặc, nếu tôi có thể trích dẫn Andy Budd:
Trong suốt sự nghiệp của mình, tôi đã viết khá nhiều hộp thoại. Tôi đã xây dựng các triển khai riêng bằng JavaScript thuần túy, jQuery và gần đây hơn là — React. Nếu bạn đã từng vật lộn để xây dựng một mô-đun, thì bạn sẽ hiểu ý tôi khi tôi nói: Rất dễ làm sai. Không chỉ từ quan điểm trực quan mà còn có rất nhiều tương tác phức tạp của người dùng cần được tính đến.
Tôi là kiểu người thích "đi sâu" vào các chủ đề khiến tôi khó chịu — đặc biệt là khi tôi thấy chủ đề đó xuất hiện trở lại — hy vọng là để tránh phải xem lại chúng một lần nữa. Khi tôi bắt đầu tìm hiểu sâu hơn về Web Components, tôi đã có khoảnh khắc "a-ha!". Bây giờ Web Components được hỗ trợ rộng rãi bởi mọi trình duyệt chính (RIP, IE11), điều này mở ra một cánh cửa cơ hội hoàn toàn mới. Tôi tự nhủ:
Lưu ý: Để hưởng lợi từ bài viết này và các ví dụ mã, bạn sẽ cần có một số hiểu biết cơ bản về HTML, CSS và JavaScript.


Đầu tiên, chúng ta sẽ tìm hiểu nhanh về Thành phần Web. Chúng là các đoạn mã HTML, CSS và JavaScript được đóng gói lại để bao hàm phạm vi. Nghĩa là không có kiểu nào từ bên ngoài thành phần sẽ ảnh hưởng đến bên trong hoặc ngược lại. Hãy nghĩ về nó như một “phòng sạch” được bịt kín hoàn toàn của thiết kế UI.
Thoạt nhìn, điều này có vẻ vô lý. Tại sao chúng ta lại muốn một phần UI mà chúng ta không thể kiểm soát bên ngoài thông qua CSS? Hãy giữ nguyên suy nghĩ đó, vì chúng ta sẽ sớm quay lại với nó.
Giải thích tốt nhất là khả năng tái sử dụng. Xây dựng một thành phần theo cách này có nghĩa là chúng ta không bị ràng buộc với bất kỳ khuôn khổ JS nào du jour. Một cụm từ phổ biến được sử dụng trong các cuộc trò chuyện xung quanh các tiêu chuẩn web là "sử dụng nền tảng." Bây giờ, hơn bao giờ hết, bản thân nền tảng này có hỗ trợ đa trình duyệt tuyệt vời.
Lưu ý: Tôi đang sử dụng TypeScript ở đây, nhưng bạn hoàn toàn không cần bất kỳ công cụ bổ sung nào để tạo Thành phần Web. Trên thực tế, tôi đã viết bằng chứng khái niệm ban đầu của mình bằng JS thuần túy. Sau đó, tôi đã thêm TypeScript để tăng cường sự tự tin cho những người khác sử dụng nó như một gói NPM.
Tệp
Có hai lý do cho việc này. Chúng tôi muốn đảm bảo rằng có trình duyệt hỗ trợ cho
Hãy nghĩ đơn giản rằng: "Một nhóm các hàm được đóng gói lại với nhau."
Phần này của tệp chứa các giá trị nguyên thủy mà tôi dự định sử dụng lại trong toàn bộ khai báo lớp JS của mình. Tôi sẽ chỉ ra một số trong số chúng là đặc biệt thú vị.
Ghê quá, đúng không!? Bạn có thể thấy lý do tại sao tôi muốn chia thành nhiều dòng.
Là một độc giả tinh ý, bạn có thể nhận thấy
Ngoài ra, chúng tôi cung cấp các thuộc tính này cho div mà chúng tôi muốn dùng làm thành phần hộp thoại hộp thoại của mình. Điều này cho trình duyệt biết rằng
Bạn có thể tự hỏi: "Tại sao không sử dụng thẻ
Trong
Tiếp theo, chúng ta gọi
Sau đó, chúng ta lấy tất cả các phần tử — từ bên trong phần component markup đã đề cập ở trên — để sử dụng trong các lệnh gọi hàm sau. Cuối cùng, chúng ta gọi một số phương thức trợ giúp đọc các thuộc tính từ thẻ
Binding
Đây là một chút thủ thuật JS giúp chúng ta không phải gõ mã tẻ nhạt không cần thiết ở nơi khác. Khi làm việc với sự kiện DOM, ngữ cảnh của
Một cách để đảm bảo rằng
Thay vì nhập đoạn mã trên, mỗi khi chúng ta thêm một hàm mới, một lệnh gọi
Tôi sẽ không đi sâu vào chi tiết về điều đó, nhưng bạn có thể đọc thêm tại đây:
Boolean trả về là sự kết hợp của kiểm tra đó, cộng với cờ
Các biến được sử dụng trong CSS trước đó của chúng tôi cũng được sử dụng ở đây:
Điều này bao gồm việc lắng nghe các sự kiện khác nhau được coi là kích hoạt một nút:
Sau khi biên dịch từ TypeScript sang JavaScript, tôi chạy Terser so với đầu ra JS. Tất cả các hàm đã đề cập ở trên bắt đầu bằng dấu gạch dưới (
Bước đó làm giảm đáng kể kích thước tệp. Sau đó, tôi chạy đầu ra đã thu nhỏ thông qua quy trình minifyWebComponent.js mà tôi đã tạo, quy trình này nén
Ví dụ, tên lớp và các thuộc tính khác (và bộ chọn) được thu nhỏ. Điều này xảy ra trong CSS và HTML.
Tôi biết chúng ta đã đề cập khá nhiều, nhưng tin tốt là: Đó là tất cả những gì cần làm. Không có khuôn khổ nào để học trừ khi bạn muốn. Thực tế là bạn có thể bắt đầu viết Web Components của riêng mình bằng JS thuần túy mà không cần quy trình xây dựng.
Thực sự chưa bao giờ có thời điểm nào tốt hơn để
Mặc dù tôi thiên vị và cảm thấy cách tiếp cận của mình mang lại điều gì đó độc đáo — nếu không, tôi đã không cố gắng "phát minh lại bánh xe" — bạn có thể thấy rằng một trong những cách này sẽ phù hợp hơn với nhu cầu của bạn.
Các ví dụ sau đây khác với CTA Modal ở chỗ tất cả chúng đều yêu cầu ít nhất một số JavaScript bổ sung do nhà phát triển người dùng cuối viết. Trong khi với CTA Modal, tất cả những gì bạn phải viết là mã HTML.
HTML phẳng & JS:
Thành phần web:
jQuery:
React:
Vue:
Hoặc, nếu tôi có thể trích dẫn Andy Budd:
Tuy nhiên, hộp thoại có mặt ở khắp mọi nơi trong chúng ta. Chúng là một mô hình giao diện người dùng mà chúng ta không thể dễ dàng thay đổi. Khi được sử dụng một cách tinh tế và khôn ngoan, tôi dám nói rằng chúng thậm chí có thể giúp thêm ngữ cảnh vào tài liệu hoặc ứng dụng.Một lượt truy cập trang web thông thường vào năm 2022
1. Tìm cách từ chối tất cả các cookie ngoại trừ cookie cần thiết
2. Đóng tiện ích hỗ trợ hỏi xem tôi có cần trợ giúp không
3. Dừng video tự động phát
4. Đóng cửa sổ bật lên “đăng ký nhận bản tin của chúng tôi”
5. Hãy thử nhớ lại lý do tại sao tôi đến đây ngay từ đầu
— Andy Budd (@andybudd) Ngày 2 tháng 1 năm 2022
Trong suốt sự nghiệp của mình, tôi đã viết khá nhiều hộp thoại. Tôi đã xây dựng các triển khai riêng bằng JavaScript thuần túy, jQuery và gần đây hơn là — React. Nếu bạn đã từng vật lộn để xây dựng một mô-đun, thì bạn sẽ hiểu ý tôi khi tôi nói: Rất dễ làm sai. Không chỉ từ quan điểm trực quan mà còn có rất nhiều tương tác phức tạp của người dùng cần được tính đến.
Tôi là kiểu người thích "đi sâu" vào các chủ đề khiến tôi khó chịu — đặc biệt là khi tôi thấy chủ đề đó xuất hiện trở lại — hy vọng là để tránh phải xem lại chúng một lần nữa. Khi tôi bắt đầu tìm hiểu sâu hơn về Web Components, tôi đã có khoảnh khắc "a-ha!". Bây giờ Web Components được hỗ trợ rộng rãi bởi mọi trình duyệt chính (RIP, IE11), điều này mở ra một cánh cửa cơ hội hoàn toàn mới. Tôi tự nhủ:
Viết một lần và chạy ở mọi nơi, có thể nói như vậy, hoặc ít nhất đó là nguyện vọng cao cả của tôi. Tin tốt đây. Thật vậy, có thể xây dựng một modal có tương tác phong phú mà chỉ cần sử dụng HTML.“Sẽ thế nào nếu có thể xây dựng một modal mà với tư cách là một nhà phát triển tạo ra một trang hoặc ứng dụng, tôi sẽ không phải loay hoay với bất kỳ cấu hình JavaScript bổ sung nào?”
Lưu ý: Để hưởng lợi từ bài viết này và các ví dụ mã, bạn sẽ cần có một số hiểu biết cơ bản về HTML, CSS và JavaScript.


Trước khi chúng ta bắt đầu
Nếu bạn không có nhiều thời gian và chỉ muốn xem sản phẩm hoàn thiện, hãy xem tại đây:- CTA Modal Trang demo
- CTA Modal Kho Git
Sử dụng Nền tảng
Bây giờ chúng ta đã đề cập đến "lý do" để giải quyết vấn đề cụ thể này, trong phần còn lại của bài viết này, tôi sẽ giải thích "cách" xây dựng nền tảng.Đầu tiên, chúng ta sẽ tìm hiểu nhanh về Thành phần Web. Chúng là các đoạn mã HTML, CSS và JavaScript được đóng gói lại để bao hàm phạm vi. Nghĩa là không có kiểu nào từ bên ngoài thành phần sẽ ảnh hưởng đến bên trong hoặc ngược lại. Hãy nghĩ về nó như một “phòng sạch” được bịt kín hoàn toàn của thiết kế UI.
Thoạt nhìn, điều này có vẻ vô lý. Tại sao chúng ta lại muốn một phần UI mà chúng ta không thể kiểm soát bên ngoài thông qua CSS? Hãy giữ nguyên suy nghĩ đó, vì chúng ta sẽ sớm quay lại với nó.
Giải thích tốt nhất là khả năng tái sử dụng. Xây dựng một thành phần theo cách này có nghĩa là chúng ta không bị ràng buộc với bất kỳ khuôn khổ JS nào du jour. Một cụm từ phổ biến được sử dụng trong các cuộc trò chuyện xung quanh các tiêu chuẩn web là "sử dụng nền tảng." Bây giờ, hơn bao giờ hết, bản thân nền tảng này có hỗ trợ đa trình duyệt tuyệt vời.
Deep Dive
Để tham khảo, tôi sẽ tham khảo ví dụ mã này —cta-modal.ts
.Lưu ý: Tôi đang sử dụng TypeScript ở đây, nhưng bạn hoàn toàn không cần bất kỳ công cụ bổ sung nào để tạo Thành phần Web. Trên thực tế, tôi đã viết bằng chứng khái niệm ban đầu của mình bằng JS thuần túy. Sau đó, tôi đã thêm TypeScript để tăng cường sự tự tin cho những người khác sử dụng nó như một gói NPM.
Tệp
cta-modal.ts
được chia thành nhiều phần:- Gói có điều kiện;
- Hằng số:
- Biến có thể sử dụng lại,
- Kiểu thành phần,
- Đánh dấu thành phần;
-
Lớp CtaModal
:- Trình xây dựng,
- Liên kết
this
context, - Vòng đời phương pháp,
- Thêm và xóa sự kiện,
- Phát hiện các thay đổi thuộc tính,
- Tập trung vào các phần tử cụ thể,
- Phát hiện chế độ modal "bên ngoài",
- Phát hiện tùy chọn chuyển động,
- Chuyển đổi chế độ modal hiển thị/ẩn,
- Xử lý sự kiện: nhấp vào lớp phủ,
- Xử lý sự kiện: nhấp vào chuyển đổi,
- Xử lý sự kiện: phần tử tiêu điểm,
- Xử lý sự kiện: bàn phím;
- Lệnh gọi lại đã tải DOM:
- Chờ trang sẵn sàng,
- Đăng ký thẻ
.
Bộ bao bọc có điều kiện
Có mộtif
cấp cao nhất, bao bọc toàn bộ mã của tệp:
Mã:
// ==========================// BẮT ĐẦU: if "customElements".// ===========================if ('customElements' in window) { /* LƯU Ý: ĐÃ XÓA CÁC DÒNG, ĐỂ NGẮN GỌN. */}// ========================// KẾT THÚC: if "customElements".// =========================
window.customElements
. Nếu có, điều này sẽ cung cấp cho chúng tôi một cách tiện dụng để duy trì phạm vi biến. Nghĩa là khi khai báo biến thông qua const
hoặc let
, chúng sẽ không "rò rỉ" ra bên ngoài khối if {…}
. Trong khi sử dụng var
cũ sẽ có vấn đề, vô tình tạo ra nhiều biến toàn cục.Biến có thể tái sử dụng
Lưu ý: LớpFoo {…}
của JavaScript khác với class="foo"
của HTML hoặc CSS.Hãy nghĩ đơn giản rằng: "Một nhóm các hàm được đóng gói lại với nhau."
Phần này của tệp chứa các giá trị nguyên thủy mà tôi dự định sử dụng lại trong toàn bộ khai báo lớp JS của mình. Tôi sẽ chỉ ra một số trong số chúng là đặc biệt thú vị.
Mã:
// ==========// Hằng số.// ==========/* LƯU Ý: ĐÃ XÓA CÁC DÒNG, ĐỂ NGẮN GỌN. */const ANIMATION_DURATION = 250;const DATA_HIDE = 'data-cta-modal-hide';const DATA_SHOW = 'data-cta-modal-show';const PREFERS_REDUCED_MOTION = '(prefers-reduced-motion: reduce)';const FOCUSABLE_SELECTORS = [ '[contenteditable]', '[tabindex="0"]:not([disabled])', 'a[href]', 'audio[controls]', 'button:not([disabled])', 'iframe', "input:not([disabled]):not([type='hidden'])", 'select:not([disabled])', 'summary', 'textarea:not([disabled])', 'video[controls]',].join(',');
-
ANIMATION_DURATION
Chỉ định thời gian hoạt ảnh CSS của tôi sẽ mất. Tôi cũng sử dụng lại điều này sau trongsetTimeout
để giữ cho CSS và JS của tôi đồng bộ. Nó được đặt thành250
mili giây, tức là một phần tư giây.
Trong khi CSS cho phép chúng ta chỉ địnhanimation-duration
theo toàn bộ giây (hoặc mili giây), JS sử dụng các gia số của mili giây. Sử dụng giá trị này cho phép tôi sử dụng nó cho cả hai. -
DATA_SHOW
vàDATA_HIDE
Đây là các chuỗi cho các thuộc tính dữ liệu HTML'data-cta-modal-show'
và'data-cta-modal-hide'
được sử dụng để kiểm soát việc hiển thị/ẩn của modal, cũng như điều chỉnh thời gian hoạt ảnh trong CSS. Chúng được sử dụng sau đó kết hợp vớiANIMATION_DURATION
. -
PREFERS_REDUCED_MOTION
Một truy vấn phương tiện xác định xem người dùng có đặt tùy chọn hệ điều hành của họ thànhreduce
choprefers-reduced-motion
hay không. Tôi xem giá trị này trong cả CSS và JS để xác định xem có nên tắt hoạt ảnh hay không. -
FOCUSABLE_SELECTORS
Chứa các bộ chọn CSS cho tất cả các phần tử có thể được coi là có thể lấy nét trong một hộp thoại. Nó được sử dụng sau đó nhiều hơn một lần, thông quaquerySelectorAll
. Tôi đã khai báo nó ở đây để giúp tăng khả năng đọc, thay vì thêm sự lộn xộn vào thân hàm.
Mã:
[contenteditable], [tabindex="0"]:not([disabled]), a[href], audio[controls], button:not([disabled]), iframe, input:not([disabled]):not([type='hidden']), select:not([disabled]), summary, textarea:not([disabled]), video[controls]
Là một độc giả tinh ý, bạn có thể nhận thấy
type='hidden'
và tabindex="0"
đang sử dụng các dấu ngoặc kép khác nhau. Điều đó có chủ đích, và chúng ta sẽ xem lại lý do sau.Kiểu thành phần
Phần này chứa một chuỗi nhiều dòng với thẻ
. Như đã đề cập trước đó, các kiểu có trong Thành phần Web không ảnh hưởng đến phần còn lại của trang. Điều đáng chú ý là cách tôi sử dụng các biến nhúng ${etc}
thông qua nội suy chuỗi.- Chúng tôi tham chiếu biến
PREFERS_REDUCED_MOTION
của mình để buộc đặt hoạt ảnh thànhnone
cho những người dùng thích chuyển động giảm. - Chúng tôi tham chiếu
DATA_SHOW
vàDATA_HIDE
cùng vớiANIMATION_DURATION
để cho phép chia sẻ quyền kiểm soát đối với hoạt ảnh CSS. Lưu ý việc sử dụng hậu tốms
cho mili giây, vì đó là ngôn ngữ chung của CSS và JS.
Mã:
// ======// Kiểu.// ======const STYLE = ` /* LƯU Ý: ĐÃ XÓA CÁC DÒNG, ĐỂ NGẮN GỌN. */ @media ${PREFERS_REDUCED_MOTION} { *, *:after, *:before { hoạt ảnh: không có !important; chuyển tiếp: không có !important; } } [${DATA_SHOW}='true'] .cta-modal__overlay { thời lượng hoạt ảnh: ${ANIMATION_DURATION}ms; tên hoạt ảnh: SHOW-OVERLAY; } [${DATA_SHOW}='true'] .cta-modal__dialog { thời lượng hoạt ảnh: ${ANIMATION_DURATION}ms; animation-name: SHOW-DIALOG; } [${DATA_HIDE}='true'] .cta-modal__overlay { animation-duration: ${ANIMATION_DURATION}ms; animation-name: HIDE-OVERLAY; opacity: 0; } [${DATA_HIDE}='true'] .cta-modal__dialog { animation-duration: ${ANIMATION_DURATION}ms; animation-name: HIDE-DIALOG; transform: scale(0.95); } `;
Đánh dấu thành phần
Đánh dấu cho modal là phần đơn giản nhất. Đây là những khía cạnh thiết yếu tạo nên modal:- các khe,
- khu vực có thể cuộn,
- bẫy tiêu điểm,
- lớp phủ bán trong suốt,
- cửa sổ hộp thoại,
- nút đóng.
trong trang của một người, có hai điểm chèn cho nội dung. Đặt các thành phần bên trong các khu vực này khiến chúng xuất hiện như một phần của hộp thoại:-
ánh xạ tới
,
-
ánh xạ tới
.
Ngoài ra, chúng tôi cung cấp các thuộc tính này cho div mà chúng tôi muốn dùng làm thành phần hộp thoại hộp thoại của mình. Điều này cho trình duyệt biết rằng
có ý nghĩa về mặt ngữ nghĩa. Nó cũng cho phép chúng ta tập trung vào phần tử thông qua JS:-
aria-modal='true'
, -
role='dialog'
, -
tabindex'-1'
.
Mã:
// =========// Mẫu.// =========const FOCUS_TRAP = ` `;const MODAL = ` ${FOCUS_TRAP} × ${FOCUS_TRAP} `;// Lấy đánh dấu.const markup = [STYLE, MODAL].join(EMPTY_STRING).trim().replace(SPACE_REGEX, SPACE);// Lấy mẫu.const template = document.createElement(TEMPLATE);template.innerHTML = markup;
dialog
?" Câu hỏi hay. Tại thời điểm viết bài này, nó vẫn có một số điểm kỳ quặc trên nhiều trình duyệt. Để biết thêm thông tin về điều đó, hãy đọc bài viết này của Scott O’hara. Ngoài ra, theo tài liệu của Mozilla, dialog
không được phép có thuộc tính tabindex
, thuộc tính này chúng ta cần tập trung vào modal của mình.Constructor
Bất cứ khi nào một lớp JS được khởi tạo, hàmconstructor
của nó sẽ được gọi. Đó chỉ là một thuật ngữ hoa mỹ có nghĩa là một thể hiện của lớp CtaModal
đang được tạo. Trong trường hợp Thành phần Web của chúng ta, quá trình khởi tạo này diễn ra tự động bất cứ khi nào gặp
trong HTML của trang.Trong
constructor
, chúng ta gọi super
để yêu cầu lớp HTMLElement
(mà chúng ta đang extend
-ing) gọi constructor
của riêng nó. Hãy nghĩ về nó như mã keo, để đảm bảo chúng ta khai thác một số phương thức vòng đời mặc định.Tiếp theo, chúng ta gọi
this._bind()
mà chúng ta sẽ đề cập thêm một chút sau. Sau đó, chúng ta đính kèm "shadow DOM" vào phiên bản lớp của mình và thêm đánh dấu mà chúng ta đã tạo dưới dạng chuỗi nhiều dòng trước đó.Sau đó, chúng ta lấy tất cả các phần tử — từ bên trong phần component markup đã đề cập ở trên — để sử dụng trong các lệnh gọi hàm sau. Cuối cùng, chúng ta gọi một số phương thức trợ giúp đọc các thuộc tính từ thẻ
tương ứng.
Mã:
// ======================// Vòng đời: constructor.// =======================constructor() { // Constructor cha. super(); // Liên kết ngữ cảnh. this._bind(); // Shadow DOM. this._shadow = this.attachShadow({ mode: 'closed' }); // Thêm template. this._shadow.appendChild( // Sao chép node. template.content.cloneNode(true) ); // Lấy các khe cắm. this._slotForButton = this.querySelector("[slot='button']"); this._slotForModal = this.querySelector("[slot='modal']"); // Lấy các phần tử. this._heading = this.querySelector('h1, h2, h3, h4, h5, h6'); // Lấy các phần tử bóng đổ. this._buttonClose = this._shadow.querySelector('.cta-modal__close') dưới dạng HTMLElement; this._focusTrapList = this._shadow.querySelectorAll('.cta-modal__focus-trap'); this._modal = this._shadow.querySelector('.cta-modal__dialog') dưới dạng HTMLElement; this._modalOverlay = this._shadow.querySelector('.cta-modal__overlay') dưới dạng HTMLElement; this._modalScroll = this._shadow.querySelector('.cta-modal__scroll') dưới dạng HTMLElement; // Thiếu khe cắm? if (!this._slotForModal) { window.console.error('Required [slot="modal"] not found inside cta-modal.'); } // Đặt cờ hoạt ảnh. this._setAnimationFlag(); // Đặt tiêu đề đóng. this._setCloseTitle(); // Đặt nhãn modal. this._setModalLabel(); // Đặt cờ tĩnh. this._setStaticFlag(); /* ===== LƯU Ý: ===== Chúng tôi đặt cờ này cuối cùng vì hình ảnh UI bên trong phụ thuộc vào một số cờ khác đang được đặt. */ // Đặt cờ hoạt động. this._setActiveFlag();}
Binding this
Context
Đây là một chút thủ thuật JS giúp chúng ta không phải gõ mã tẻ nhạt không cần thiết ở nơi khác. Khi làm việc với sự kiện DOM, ngữ cảnh của this
có thể thay đổi, tùy thuộc vào phần tử nào đang được tương tác trong trang.Một cách để đảm bảo rằng
this
luôn có nghĩa là thể hiện của lớp của chúng ta là gọi cụ thể bind
. Về cơ bản, hàm this tạo ra nó, để nó được xử lý tự động. Điều đó có nghĩa là chúng ta không phải nhập những thứ như thế này ở mọi nơi.
Mã:
/* LƯU Ý: Chỉ là một ví dụ, chúng ta không cần this. */this.someFunctionName1 = this.someFunctionName1.bind(this);this.someFunctionName2 = this.someFunctionName2.bind(this);
this._bind()
tiện dụng trong constructor
sẽ xử lý bất kỳ/tất cả các hàm mà chúng ta có thể có. Vòng lặp này sẽ lấy mọi thuộc tính lớp là hàm
và tự động liên kết chúng.
Mã:
// ===========================// Trợ giúp: liên kết ngữ cảnh `this`.// =============================_bind() { // Lấy tên thuộc tính. const propertyNames = Object.getOwnPropertyNames( // Lấy nguyên mẫu. Object.getPrototypeOf(this) ) as (keyof CtaModal)[]; // Lặp lại. propertyNames.forEach((name) => { // Liên kết các hàm. if (typeof this[name] === FUNCTION) { /* ===== LƯU Ý: ===== Tại sao lại sử dụng "@ts-expect-error" ở đây? Gọi `*.bind(this)` là một thông lệ chuẩn khi sử dụng các lớp JavaScript. Điều này là cần thiết đối với các hàm có thể thay đổi ngữ cảnh vì chúng tương tác trực tiếp với các phần tử DOM. Về cơ bản, tôi đang nói với TypeScript: "Hãy để tôi sống cuộc sống của mình!" 😎 */ // @ts-expect-error bind this[name] = this[name].bind(this); } });}
Phương pháp vòng đời
Theo bản chất của dòng này, khi chúng tamở rộng
từ HTMLElement
, chúng ta nhận được một vài lệnh gọi hàm tích hợp "miễn phí". Miễn là chúng ta đặt tên cho các hàm của mình theo những tên này, chúng sẽ được gọi vào thời điểm thích hợp trong vòng đời của thành phần
của chúng ta.
Mã:
// =========// Thành phần.// ==========class CtaModal extends HTMLElement { /* LƯU Ý: ĐÃ XÓA CÁC DÒNG, ĐỂ NGẮN GỌN. */}
-
observedAttributes
Điều này cho trình duyệt biết chúng ta đang theo dõi những thuộc tính nào để thay đổi. -
attributeChangedCallback
Nếu bất kỳ thuộc tính nào trong số đó thay đổi, lệnh gọi lại này sẽ được gọi. Tùy thuộc vào thuộc tính nào đã thay đổi, chúng ta sẽ gọi một hàm để đọc thuộc tính đó. -
connectedCallback
Điều này được gọi khi thẻđược đăng ký với trang. Chúng tôi sử dụng cơ hội này để thêm tất cả các trình xử lý sự kiện của mình.
Nếu bạn quen thuộc với React, thì điều này tương tự như sự kiện vòng đờicomponentDidMount
. -
disconnectedCallback
Điều này được gọi khi thẻbị xóa khỏi trang. Tương tự như vậy, chúng tôi xóa tất cả các trình xử lý sự kiện lỗi thời khi/nếu điều này xảy ra.
Điều này tương tự như sự kiện vòng đờicomponentWillUnmount
trong React.
_
). Mặc dù không thực sự cần thiết, nhưng lý do cho điều này là hai mặt. Một là, nó làm rõ những hàm nào chúng ta đã tạo cho
mới của mình và những hàm nào là sự kiện vòng đời gốc của lớp HTMLElement
. Hai là, khi chúng ta thu nhỏ mã sau này, tiền tố biểu thị rằng chúng có thể bị làm sai lệch. Trong khi đó, các phương thức vòng đời gốc cần giữ nguyên tên của chúng.
Mã:
// ===========================// Vòng đời: theo dõi các thuộc tính.// ============================static get observedAttributes() { return [ACTIVE, ANIMATED, CLOSE, STATIC];}// =============================// Vòng đời: các thuộc tính đã thay đổi.// =============================attributeChangedCallback(name: string, oldValue: string, newValue: string) { // Giá trị cũ/mới khác nhau? if (oldValue !== newValue) { // Đã thay đổi giá trị [active="…"]? if (name === ACTIVE) { this._setActiveFlag(); } // Đã thay đổi giá trị [animated="…"]? if (name === ANIMATED) { this._setAnimationFlag(); } // Đã thay đổi giá trị [close="…"]? if (name === CLOSE) { this._setCloseTitle(); } // Đã thay đổi giá trị [static="…"]? if (name === STATIC) { this._setStaticFlag(); } }}// ==========================// Vòng đời: gắn kết thành phần.// ===========================connectedCallback() { this._addEvents();}// ============================// Vòng đời: hủy gắn kết thành phần.// ===========================disconnectedCallback() { this._removeEvents();}
Thêm và xóa sự kiện
Các hàm này đăng ký (và xóa) các lệnh gọi lại cho nhiều sự kiện ở cấp độ phần tử và trang:- nhấp vào nút,
- lấy tiêu điểm vào phần tử,
- nhấn bàn phím,
- nhấp vào lớp phủ.
Mã:
// ===================// Trợ giúp: thêm sự kiện.// ====================_addEvents() { // Ngăn chặn sự trùng lặp. this._removeEvents(); document.addEventListener(FOCUSIN, this._handleFocusIn); document.addEventListener(KEYDOWN, this._handleKeyDown); this._buttonClose.addEventListener(CLICK, this._handleClickToggle); this._modalOverlay.addEventListener(CLICK, this._handleClickOverlay); if (this._slotForButton) { this._slotForButton.addEventListener(CLICK, this._handleClickToggle); this._slotForButton.addEventListener(KEYDOWN, this._handleClickToggle); } if (this._slotForModal) { this._slotForModal.addEventListener(CLICK, this._handleClickToggle); this._slotForModal.addEventListener(KEYDOWN, this._handleClickToggle); }}// =====================// Trợ giúp: xóa sự kiện.// ======================_removeEvents() { document.removeEventListener(FOCUSIN, this._handleFocusIn); document.removeEventListener(KEYDOWN, this._handleKeyDown); nút_nút_đóng.removeEventListener(NHẤP, nút_xử lý_ClickToggle); nút_modalOverlay.removeEventListener(NHẤP, nút_xử lý_ClickOverlay); nếu (nút_xử lý_slotForButton) { nút_xử lý_EventListener(NHẤP, nút_xử lý_ClickToggle); nút_xử lý_EventListener(KEYDOWN, nút_xử lý_ClickToggle); } nếu (nút_xử lý_Modal) { nút_xử lý_EventListener(NHẤP, nút_xử lý_ClickToggle); nút_xử lý_EventListener(KEYDOWN, nút_xử lý_ClickToggle); }}
Phát hiện các thay đổi thuộc tính
Các hàm này xử lý việc đọc các thuộc tính từ thẻ
và thiết lập nhiều cờ khác nhau như sau:- Thiết lập boolean
_isAnimated
trên phiên bản lớp của chúng ta. - Thiết lập các thuộc tính
title
vàaria-label
trên nút đóng của chúng ta. - Thiết lập
aria-label
cho hộp thoại modal của chúng ta, dựa trên văn bản tiêu đề. - Thiết lập boolean
_isActive
trên phiên bản lớp của chúng ta. - Thiết lập boolean
_isStatic
trên phiên bản lớp của chúng ta.
aria-label
để liên kết modal với văn bản tiêu đề của nó (nếu có). Tại thời điểm viết bài này, trình duyệt hiện không thể liên kết thuộc tính aria-labelledby="…"
— trong DOM shadow — với id="…"
nằm trong DOM chuẩn (hay còn gọi là "light").Tôi sẽ không đi sâu vào chi tiết về điều đó, nhưng bạn có thể đọc thêm tại đây:
Mã:
// ==========================// Trợ giúp: đặt cờ hoạt ảnh.// ============================_setAnimationFlag() { this._isAnimated = this.getAttribute(ANIMATED) !== FALSE;}// =======================// Trợ giúp: thêm văn bản đóng.// ======================_setCloseTitle() { // Lấy tiêu đề. const title = this.getAttribute(CLOSE) || CLOSE_TITLE; // Đặt tiêu đề. this._buttonClose.title = title; this._buttonClose.setAttribute(ARIA_LABEL, title);}// ======================// Trợ giúp: thêm nhãn modal.// ========================_setModalLabel() { // Đặt sau. let label = MODAL_LABEL_FALLBACK; // Tiêu đề có tồn tại không? if (this._heading) { // Lấy văn bản. label = this._heading.textContent || label; label = label.trim().replace(SPACE_REGEX, SPACE); } // Đặt nhãn. this._modal.setAttribute(ARIA_LABEL, label);}// ======================// Trợ giúp: đặt cờ đang hoạt động.// =======================_setActiveFlag() { // Lấy cờ. const isActive = this.getAttribute(ACTIVE) === TRUE; // Đặt cờ. this._isActive = isActive; // Đặt hiển thị. this._toggleModalDisplay(() => { // Focus modal? if (this._isActive) { this._focusModal(); } });}// ======================// Trợ giúp: đặt cờ tĩnh.// =======================_setStaticFlag() { this._isStatic = this.getAttribute(STATIC) === TRUE;}
Tập trung vào các phần tử cụ thể
Hàm_focusElement
cho phép chúng ta tập trung vào một phần tử có thể đã hoạt động trước khi một modal trở nên hoạt động. Trong khi đó, hàm _focusModal
sẽ đặt tiêu điểm vào chính hộp thoại modal và sẽ đảm bảo rằng phông nền modal được cuộn lên trên cùng.
Mã:
// ======================// Trình trợ giúp: phần tử focus.// =====================_focusElement(element: HTMLElement) { window.requestAnimationFrame(() => { if (typeof element.focus === FUNCTION) { element.focus(); } });}// ===================// Trình trợ giúp: phần tử focus.// ====================_focusModal() { window.requestAnimationFrame(() => { this._modal.focus(); this._modalScroll.scrollTo(0, 0); });}
Phát hiện Modal "Ngoài"
Hàm này rất hữu ích để biết liệu một phần tử có nằm ngoài thẻ
cha hay không. Hàm này trả về giá trị boolean, chúng ta có thể sử dụng giá trị này để thực hiện hành động thích hợp. Cụ thể là, điều hướng bẫy tab bên trong modal khi modal đang hoạt động.
Mã:
// ============================// Trợ giúp: phát hiện modal ngoài.// ==============================_isOutsideModal(element?: HTMLElement) { // Thoát sớm. if (!this._isActive || !element) { return false; } // Có phần tử? const hasElement = this.contains(element) || this._modal.contains(element); // Lấy boolean. const bool = !hasElement; // Hiển thị boolean. return bool;}
Phát hiện sở thích chuyển động
Tại đây, chúng tôi sử dụng lại biến từ trước (cũng được sử dụng trong CSS của chúng tôi) để phát hiện xem người dùng có đồng ý với chuyển động hay không. Tức là họ không thiết lập rõ ràngprefers-reduced-motion
thành reduce
thông qua tùy chọn hệ điều hành của họ.Boolean trả về là sự kết hợp của kiểm tra đó, cộng với cờ
animated="false"
không được đặt trên
.
Mã:
// ==========================// Trợ giúp: phát hiện chuyển động pref.// ===========================_isMotionOkay() { // Nhận pref. const { matches } = window.matchMedia(PREFERS_REDUCED_MOTION); // Hiển thị boolean. return this._isAnimated && !matches;}
Chuyển đổi Ẩn/Hiện modal
Có khá nhiều thứ diễn ra trong hàm này, nhưng về bản chất, nó khá đơn giản.- Nếu modal không hoạt động, hãy hiển thị nó. Nếu được phép hoạt ảnh, hãy làm cho nó hoạt động tại chỗ.
- Nếu modal đang hoạt động, hãy ẩn nó. Nếu được phép hoạt ảnh, hãy làm cho nó biến mất.
Các biến được sử dụng trong CSS trước đó của chúng tôi cũng được sử dụng ở đây:
-
ANIMATION_DURATION
, -
DATA_SHOW
, -
DATA_HIDE
.
Mã:
// ====================// Trợ lý: chuyển đổi chế độ xem modal.// =====================_toggleModalDisplay(callback: () => void) { // @ts-expect-error boolean this.setAttribute(ACTIVE, this._isActive); // Lấy giá trị boolean. const isModalVisible = this._modalScroll.style.display === BLOCK; const isMotionOkay = this._isMotionOkay(); // Lấy độ trễ. const delay = isMotionOkay ? ANIMATION_DURATION : 0; // Lấy chiều rộng thanh cuộn. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; // Lấy phần tử đang hoạt động. const activeElement = document.activeElement dưới dạng HTMLElement; // Bộ nhớ đệm phần tử đang hoạt động? if (this._isActive && activeElement) { this._activeElement = activeElement; } // ============= // Modal active? // ============= if (this._isActive) { // Hiển thị modal. this._modalScroll.style.display = BLOCK; // Ẩn thanh cuộn. document.documentElement.style.overflow = HIDDEN; // Thêm chỗ giữ chỗ? if (scrollbarWidth) { document.documentElement.style.paddingRight = `${scrollbarWidth}px`; } // Đặt cờ. if (isMotionOkay) { this._isHideShow = true; this._modalScroll.setAttribute(DATA_SHOW, TRUE); } // Kích hoạt lệnh gọi lại.callback(); // Chờ hoạt ảnh CSS. this._timerForShow = window.setTimeout(() => { // Clear.clearTimeout(this._timerForShow); // Xóa cờ. this._isHideShow = false; this._modalScroll.removeAttribute(DATA_SHOW); // Delay. }, delay); /* ===== LƯU Ý: ===== Chúng ta muốn đảm bảo rằng modal hiện đang hiển thị vì chúng ta không muốn đưa scroll trở lại phần tử `` một cách không cần thiết. Lý do là một `` khác trong trang có thể đã được kết xuất trước với thuộc tính [active="true"] . Nếu vậy, chúng ta muốn giữ nguyên giá trị tràn của trang. */ } else if (isModalVisible) { // Đặt cờ. if (isMotionOkay) { this._isHideShow = true; this._modalScroll.setAttribute(DATA_HIDE, TRUE); } // Kích hoạt lệnh gọi lại? callback(); // Chờ hoạt ảnh CSS. this._timerForHide = window.setTimeout(() => { // Xóa. clearTimeout(this._timerForHide); // Xóa cờ. this._isHideShow = false; this._modalScroll.removeAttribute(DATA_HIDE); // Ẩn modal. this._modalScroll.style.display = NONE; // Hiển thị thanh cuộn. document.documentElement.style.overflow = EMPTY_STRING; // Xóa chỗ giữ chỗ. document.documentElement.style.paddingRight = EMPTY_STRING; // Trì hoãn. }, trì hoãn); }}
Xử lý sự kiện: Click Overlay
Khi nhấp vào lớp phủ bán trong suốt, giả sửstatic="true"
không được đặt trên thẻ
, chúng tôi sẽ đóng hộp thoại.
Mã:
// =====================// Sự kiện: nhấp vào lớp phủ.// =====================_handleClickOverlay(event: MouseEvent) { // Thoát sớm. if (this._isHideShow || this._isStatic) { return; } // Lấy lớp. const target = event.target dưới dạng HTMLElement; // Bên ngoài modal? if (target.classList.contains('cta-modal__overlay')) { this._handleClickToggle(); }}
Xử lý sự kiện: Nhấp vào Chuyển đổi
Hàm này sử dụng ủy quyền sự kiện trên các phần tử
và
. Bất cứ khi nào một phần tử con có lớp cta-modal-toggle
được kích hoạt, nó sẽ khiến trạng thái hoạt động của modal thay đổi.Điều này bao gồm việc lắng nghe các sự kiện khác nhau được coi là kích hoạt một nút:
- nhấp chuột,
- nhấn phím
enter
, - nhấn phím
phím cách
.
Mã:
// ===================// Sự kiện: chuyển đổi modal.// ====================_handleClickToggle(event?: MouseEvent | KeyboardEvent) { // Đặt sau. let key = EMPTY_STRING; let target = null; // Sự kiện có tồn tại không? if (event) { if (event.target) { target = event.target as HTMLElement; } // Lấy key. if ((event as KeyboardEvent).key) { key = (event as KeyboardEvent).key; key = key.toLowerCase(); } } // Đặt sau. let button; // Target có tồn tại không? if (target) { // Nhấp trực tiếp. if (target.classList.contains('cta-modal__close')) { button = target as HTMLButtonElement; // Nhấp được ủy quyền. } else if (typeof target.closest === FUNCTION) { button = target.closest('.cta-modal-toggle') as HTMLButtonElement; } } // Lấy boolean. const isValidEvent = event && typeof event.preventDefault === FUNCTION; const isValidClick = button && isValidEvent && !key; const isValidKey = button && isValidEvent && [ENTER, SPACE].includes(key); const isButtonDisabled = button && button.disabled; const isButtonMissing = isValidEvent && !button; const isWrongKeyEvent = key && !isValidKey; // Thoát sớm. if (isButtonDisabled || isButtonMissing || isWrongKeyEvent) { return; } // Ngăn chặn mặc định? if (isValidKey || isValidClick) { event.preventDefault(); } // Đặt cờ. this._isActive = !this._isActive; // Đặt hiển thị. this._toggleModalDisplay(() => { // Focus modal? if (this._isActive) { this._focusModal(); // Trả về focus? } else if (this._activeElement) { this._focusElement(this._activeElement); } });}
Xử lý sự kiện: Focus Element
Hàm này được kích hoạt bất cứ khi nào một phần tử nhận đượcfocus
trên trang. Tùy thuộc vào trạng thái của modal và phần tử nào được focus, chúng ta có thể bẫy điều hướng tab trong hộp thoại modal. Đây là nơi FOCUSABLE_SELECTORS
của chúng ta từ đầu phát huy tác dụng.
Mã:
// =========================// Sự kiện: tiêu điểm trong tài liệu.// ========================_handleFocusIn() { // Thoát sớm. if (!this._isActive) { return; } // prettier-ignore const activeElement = ( // Lấy phần tử đang hoạt động. this._shadow.activeElement || document.activeElement ) dưới dạng HTMLElement; // Lấy giá trị boolean. const isFocusTrap1 = activeElement === this._focusTrapList[0]; const isFocusTrap2 = activeElement === this._focusTrapList[1]; // Đặt sau. let focusListReal: HTMLElement[] = []; // Slot có tồn tại không? if (this._slotForModal) { // Lấy các phần tử "thực". focusListReal = Array.from( this._slotForModal.querySelectorAll(FOCUSABLE_SELECTORS) ) as HTMLElement[]; } // Lấy các phần tử "bóng". const focusListShadow = Array.from( this._modal.querySelectorAll(FOCUSABLE_SELECTORS) ) as HTMLElement[]; // Lấy các phần tử "tổng số". const focusListTotal = focusListShadow.concat(focusListReal); // Lấy các mục đầu tiên và cuối cùng. const focusItemFirst = focusListTotal[0]; const focusItemLast = focusListTotal[focusListTotal.length - 1]; // Bẫy tiêu điểm: ở trên? if (isFocusTrap1 && focusItemLast) { this._focusElement(focusItemLast); // Bẫy tiêu điểm: ở dưới? } else if (isFocusTrap2 && focusItemFirst) { this._focusElement(focusItemFirst); // Outside modal? } else if (this._isOutsideModal(activeElement)) { this._focusModal(); }}
Xử lý sự kiện: Bàn phím
Nếu modal đang hoạt động khi nhấn phímescape
, modal đó sẽ đóng lại. Nếu nhấn phím tab
, chúng tôi sẽ đánh giá xem có cần điều chỉnh phần tử nào đang được lấy nét hay không.
Mã:
// =================// Sự kiện: nhấn phím.// =================_handleKeyDown({ key }: KeyboardEvent) { // Thoát sớm. if (!this._isActive) { return; } // Lấy key. key = key.toLowerCase(); // Phím thoát? if (key === ESCAPE && !this._isHideShow && !this._isStatic) { this._handleClickToggle(); } // Phím Tab? if (key === TAB) { this._handleFocusIn(); }}
DOM Loaded Callback
Trình lắng nghe sự kiện này yêu cầu cửa sổ đợi DOM (trang HTML) được tải, sau đó phân tích cú pháp cho bất kỳ trường hợp nào của
và đính kèm tính tương tác JS của chúng tôi vào đó. Về cơ bản, chúng tôi đã tạo một thẻ HTML mới và bây giờ trình duyệt biết cách sử dụng thẻ đó.
Mã:
// ==============// Xác định phần tử.// ==============window.addEventListener('DOMContentLoaded', () => { window.customElements.define('cta-modal', CtaModal);});
Tối ưu hóa thời gian xây dựng
Tôi sẽ không đi sâu vào khía cạnh này, nhưng tôi nghĩ rằng nó đáng để nêu ra.Sau khi biên dịch từ TypeScript sang JavaScript, tôi chạy Terser so với đầu ra JS. Tất cả các hàm đã đề cập ở trên bắt đầu bằng dấu gạch dưới (
_
) đều được đánh dấu là an toàn để làm biến dạng. Nghĩa là, chúng chuyển từ tên _bind
và _addEvents
thành các chữ cái đơn lẻ.Bước đó làm giảm đáng kể kích thước tệp. Sau đó, tôi chạy đầu ra đã thu nhỏ thông qua quy trình minifyWebComponent.js mà tôi đã tạo, quy trình này nén
nhúng và đánh dấu thậm chí còn hơn nữa.Ví dụ, tên lớp và các thuộc tính khác (và bộ chọn) được thu nhỏ. Điều này xảy ra trong CSS và HTML.
-
class='cta-modal__overlay'
trở thànhclass=o
. Các dấu ngoặc kép cũng bị xóa vì về mặt kỹ thuật, trình duyệt không cần chúng để hiểu ý định. - Bộ chọn CSS duy nhất không bị thay đổi là
[tabindex="0"]
, vì việc xóa các dấu ngoặc kép quanh0
dường như khiến nó không hợp lệ khi được phân tích cú pháp bởiquerySelectorAll
. Tuy nhiên, an toàn khi thu nhỏ trong HTML từtabindex='0'
thànhtabindex=0
.
- chưa thu nhỏ: 16.849,
- thu nhỏ terser: 10.230,
- và tập lệnh của tôi: 7.689.
favicon.ico
trên Smashing Magazine là 4.286 byte. Vì vậy, chúng ta thực sự không thêm nhiều chi phí, cho nhiều chức năng chỉ yêu cầu viết HTML để sử dụng.Kết luận
Nếu bạn đã đọc đến đây, cảm ơn vì đã theo dõi tôi. Tôi hy vọng rằng tôi đã ít nhất khơi dậy sự quan tâm của bạn đối với Web Components!Tôi biết chúng ta đã đề cập khá nhiều, nhưng tin tốt là: Đó là tất cả những gì cần làm. Không có khuôn khổ nào để học trừ khi bạn muốn. Thực tế là bạn có thể bắt đầu viết Web Components của riêng mình bằng JS thuần túy mà không cần quy trình xây dựng.
Thực sự chưa bao giờ có thời điểm nào tốt hơn để
#UseThePlatform
. Tôi mong muốn được thấy những gì bạn tưởng tượng.Đọc thêm
Tôi sẽ là người thiếu sót nếu không đề cập rằng có vô số các tùy chọn modal khác ngoài kia.Mặc dù tôi thiên vị và cảm thấy cách tiếp cận của mình mang lại điều gì đó độc đáo — nếu không, tôi đã không cố gắng "phát minh lại bánh xe" — bạn có thể thấy rằng một trong những cách này sẽ phù hợp hơn với nhu cầu của bạn.
Các ví dụ sau đây khác với CTA Modal ở chỗ tất cả chúng đều yêu cầu ít nhất một số JavaScript bổ sung do nhà phát triển người dùng cuối viết. Trong khi với CTA Modal, tất cả những gì bạn phải viết là mã HTML.
HTML phẳng & JS:
Thành phần web:
jQuery:
React:
Vue: