Xây dựng một thành phần web có thể kéo theo phong cách cổ điển với Lit

theanh

Administrator
Nhân viên
Quay trở lại những năm 90, hệ điều hành đầu tiên của tôi là Windows. Bây giờ, vào những năm 2020, tôi chủ yếu làm việc để xây dựng các ứng dụng web bằng trình duyệt. Qua nhiều năm, trình duyệt đã chuyển đổi thành một công cụ tuyệt vời và mạnh mẽ hỗ trợ một thế giới rộng lớn các ứng dụng phong phú. Nhiều trong số các ứng dụng này, với giao diện phức tạp và khả năng rộng lớn, thậm chí có thể khiến các chương trình đầu thiên niên kỷ khó khăn nhất cũng phải đỏ mặt.



Các tính năng của trình duyệt gốc như thành phần web đang được các công ty đa quốc gia và các nhà phát triển cá nhân áp dụng và sử dụng trên toàn bộ web.
Trong trường hợp bạn đang tự hỏi liệu có ai đang sử dụng Thành phần web không:

- GitHub
- YouTube
- Twitter (tweet được nhúng)
- SalesForce
- ING
- Ứng dụng web Photoshop
- Chrome devtools
- Giao diện người dùng Firefox hoàn chỉnh
- Ứng dụng web Apple Music
— Danny Moerkerke (@dannymoerkerke) 5 tháng 8, 2022
Vậy thì tại sao không nắm bắt công nghệ hiện tại bằng cách tôn vinh các giao diện của quá khứ?

Trong bài viết này, tôi hy vọng sẽ dạy bạn điều đó bằng cách tái hiện hiệu ứng cửa sổ vỡ mang tính biểu tượng.

1-creating-draggable-gifs-web-components-lit.gif


Chúng tôi sẽ sử dụng các thành phần web, mô hình thành phần gốc của trình duyệt, để xây dựng giao diện này. Chúng tôi cũng sẽ sử dụng thư viện Lit, giúp đơn giản hóa các API thành phần web gốc.

Nhiều khái niệm tôi nói đến ở đây là những bài học tôi đã học được từ việc xây dựng A2k, một thư viện giao diện người dùng được thiết kế để giúp bạn tạo giao diện người dùng cổ điển với các công cụ hiện đại.

Trong bài viết này, chúng tôi sẽ đề cập đến:
  • những điều cơ bản về việc tạo thành phần web bằng Lit;
  • cách tùy chỉnh dễ dàng hành vi của thành phần bằng các công cụ tích hợp của Lit;
  • cách đóng gói chức năng có thể tái sử dụng;
  • cách phân phối và phản hồi các sự kiện bằng các phương pháp luồng dữ liệu nâng cao.
Bạn nên biết HTML, CSS cốt lõi và một số JavaScript cơ bản để làm theo hướng dẫn này, nhưng không yêu cầu kiến thức cụ thể về khuôn khổ.

Bắt đầu Đã bắt đầu​

Bạn có thể theo dõi cho phép trong trình duyệt bằng cách sử dụng StackBlitz.

Sau khi StackBlitz hoàn tất thiết lập, bạn sẽ thấy thông tin sau trong cửa sổ trình duyệt:



Lưu ý: Nếu bạn không muốn sử dụng StackBlitz, bạn có thể sao chép kho lưu trữ và chạy hướng dẫn bên trong README.md. Bạn cũng có thể sử dụng tệp Lit VSCode để tô sáng cú pháp và các tính năng.

Tiếp theo, hãy mở dự án trong trình soạn thảo bạn chọn. Hãy cùng xem nhanh để xem mã khởi động của chúng ta trông như thế nào.

index.html

Chúng ta có một tệp HTML rất cơ bản, chỉ nhập một số tệp CSS và JavaScript.

Bạn cũng có thể phát hiện ra một phần tử hoàn toàn mới, phần tử a2k-window. Bạn sẽ chưa từng thấy phần tử này trước đây vì đây là phần tử tùy chỉnh mà chúng ta sẽ tự xây dựng. Vì chúng ta chưa tạo và đăng ký thành phần này nên trình duyệt sẽ chuyển sang hiển thị nội dung HTML bên trong.

Các tệp .js khác nhau​

Tôi đã thêm một số thành phần và chức năng mẫu cho một số thành phần và chức năng, nhưng chúng ta sẽ điền vào các khoảng trống trong suốt bài viết này. Tôi đã nhập tất cả mã của bên thứ nhất và bên thứ ba cần thiết mà chúng ta sẽ sử dụng trong suốt bài viết này.

Phần thưởng: Phông chữ​

Tôi cũng đã thêm một số phông chữ cổ điển để giải trí! Đây là một phông chữ tuyệt vời lấy cảm hứng từ MS-2000 do Lou tạo ra. Bạn có thể tải xuống và sử dụng trong các dự án của riêng bạn nếu bạn muốn thêm một chút hương vị thiên niên kỷ vào các thiết kế của mình.

Phần 1: Xây dựng Thành phần Web đầu tiên của chúng ta​

Viết Đánh dấu của chúng ta​

Điều đầu tiên chúng ta muốn làm là tạo một thành phần cửa sổ trông thật thuyết phục. Chỉ với một vài dòng mã, chúng ta sẽ có được thành phần sau.



Chúng ta hãy bắt đầu bằng cách nhảy vào tệp a2k-window.js của chúng ta. Chúng ta sẽ viết một đoạn mã mẫu nhỏ để đưa thành phần của mình vào hoạt động.

Chúng ta sẽ cần định nghĩa một lớp mở rộng lớp cơ sở LitElement của Lit. Bằng cách mở rộng từ LitElement, lớp của chúng ta có khả năng quản lý các trạng thái và thuộc tính phản ứng. Chúng ta cũng cần triển khai hàm render trên lớp trả về mã đánh dấu để render.

Một triển khai thực sự cơ bản của một lớp sẽ trông như thế này:
Mã:
class A2kWindow extends LitElement { render() { return html`    `; }}
Có hai điều đáng lưu ý:
  • Chúng ta có thể chỉ định ID phần tử sau đó được đóng gói trong thành phần web. Giống như tài liệu cấp cao nhất, không được phép có ID trùng lặp trong cùng một thành phần, nhưng các thành phần web khác hoặc các thành phần DOM bên ngoài có thể sử dụng cùng một ID.
  • Phần tử slot là một công cụ tiện dụng có thể hiển thị mã đánh dấu tùy chỉnh được truyền xuống từ phần tử cha. Đối với những ai quen thuộc với React, chúng ta có thể ví nó như một cổng thông tin React hiển thị nơi bạn đặt prop children. Bạn có thể làm nhiều hơn thế nữa với nó, nhưng điều đó nằm ngoài phạm vi của bài viết này.
Viết những điều trên không làm cho thành phần web của chúng ta khả dụng trong HTML. Chúng ta sẽ cần định nghĩa một phần tử tùy chỉnh mới để yêu cầu trình duyệt liên kết định nghĩa này với tên thẻ a2k-window. Bên dưới lớp thành phần của chúng ta, hãy viết mã sau:
Mã:
customElements.define("a2k-window", A2kWindow);
Bây giờ chúng ta hãy quay lại trình duyệt của mình. Chúng ta nên mong đợi thấy thành phần mới của mình được hiển thị trên trang, nhưng…



Mặc dù thành phần của chúng ta đã được kết xuất, chúng ta vẫn thấy một số nội dung đơn giản chưa được định dạng. Hãy tiếp tục và thêm một số HTML và CSS:
Mã:
class A2kWindow extends LitElement { static styles = css` :host { font-family: var(--font-primary); } #window { width: min(80ch, 100%); } #panel { border: var(--border-width) solid var(--color-gray-400); box-shadow: 2px 2px var(--color-black); background-color: var(--color-gray-500); } #draggable { background: linear-gradient(90deg, var(--color-blue-100) 0%, var(--color-blue-700) 100% ); user-select: none; } #draggable p { font-weight: bold; margin: 0; color: white; padding: 2px 8px; } [data-dragging="idle"] { cursor: grab; } [data-dragging="dragging"] { cursor: grabbing; } `; render() { return html`      `; }}
Có một vài điều đáng lưu ý trong đoạn mã trên:
  • Chúng tôi định nghĩa các kiểu được giới hạn trong phần tử tùy chỉnh này thông qua thuộc tính static styles. Do cách đóng gói kiểu hoạt động, thành phần của chúng tôi sẽ không bị ảnh hưởng bởi bất kỳ kiểu bên ngoài nào. Tuy nhiên, chúng tôi có thể sử dụng các biến CSS mà chúng tôi đã thêm vào styles.css để áp dụng các kiểu từ nguồn bên ngoài.
  • Tôi đã thêm một số kiểu cho các phần tử DOM hiện chưa có, nhưng chúng tôi sẽ sớm thêm chúng.
Lưu ý về kiểu: Kiểu trong Shadow DOM là một chủ đề quá lớn để đi sâu vào trong bài viết này. Để tìm hiểu thêm về kiểu trong Shadow DOM, bạn có thể tham khảo tài liệu Lit.

Nếu bạn làm mới, bạn sẽ thấy nội dung sau:



Điều này bắt đầu trông giống thành phần web lấy cảm hứng từ Windows của chúng tôi hơn. 🙌

Mẹo chuyên nghiệp: Nếu bạn không thấy trình duyệt, hãy áp dụng những thay đổi mà bạn mong đợi. Mở công cụ phát triển của trình duyệt. Trình duyệt có thể có một số thông báo lỗi hữu ích để giúp bạn tìm ra lỗi ở đâu.

Làm cho thành phần web của chúng ta có thể tùy chỉnh​

Bước tiếp theo của chúng ta là tạo tiêu đề cho thành phần cửa sổ. Một tính năng cốt lõi của các thành phần web là thuộc tính phần tử HTML. Thay vì mã hóa cứng nội dung văn bản của tiêu đề cửa sổ, chúng ta có thể biến nó thành đầu vào thuộc tính trên phần tử. Chúng ta có thể sử dụng Lit để làm cho các thuộc tính của mình phản ứng, kích hoạt các phương thức vòng đời khi thay đổi.

Để thực hiện điều này, chúng ta cần thực hiện ba việc:
  1. Xác định các thuộc tính phản ứng,
  2. Gán một giá trị mặc định,
  3. Kết xuất giá trị của thuộc tính phản ứng vào DOM.
Trước tiên, chúng ta cần chỉ định các thuộc tính phản ứng mà chúng ta muốn bật cho thành phần của mình:
Mã:
class A2kWindow extends LitElement { static styles = css`...`; static properties = { heading: {}, }; render() {...}}
Chúng ta sẽ thực hiện điều này bằng cách chỉ định đối tượng properties tĩnh trên lớp của mình. Sau đó, chúng ta chỉ định tên của các thuộc tính mà chúng ta muốn, cùng với một số tùy chọn được truyền qua dưới dạng đối tượng. Các tùy chọn mặc định của Lit xử lý chuyển đổi thuộc tính chuỗi theo mặc định. Điều này có nghĩa là chúng ta không cần áp dụng bất kỳ tùy chọn nào và có thể để heading làm đối tượng trống.

Bước tiếp theo của chúng ta là gán một giá trị mặc định. Chúng ta sẽ thực hiện việc này trong phương thức xây dựng của thành phần.
Mã:
class A2kWindow extends LitElement { static styles = css`...`; static properties = {...}; constructor() { super(); this.heading = "Building Retro Web Components with Lit"; } render() {...}}
Lưu ý: Đừng quên gọi super()!

Và cuối cùng, hãy thêm một chút đánh dấu và hiển thị giá trị vào DOM:
Mã:
class A2kWindow extends LitElement { static styles = css`...`; static properties = {...}; constructor() {...} render() { trả về html`    
${this.heading}
     `; }}
Sau khi hoàn tất, hãy quay lại trình duyệt và xem mọi thứ trông như thế nào:



Thật thuyết phục! 🙌

Phần thưởng​

Áp dụng tiêu đề tùy chỉnh cho a2k-element từ tệp index.html.

Giải lao ngắn 😮💨

Thật tuyệt khi thấy chúng ta có thể dễ dàng xây dựng giao diện người dùng từ năm 1998 với các nguyên mẫu hiện đại vào năm 2022 như thế nào!



Và chúng ta thậm chí còn chưa đến phần thú vị nữa! Trong các phần tiếp theo, chúng ta sẽ xem xét cách sử dụng một số khái niệm trung gian của Lit để tạo chức năng kéo theo cách có thể tái sử dụng trên các thành phần tùy chỉnh.

Phần 2: Làm cho thành phần của chúng ta có thể kéo được​

Đây là lúc mọi thứ trở nên hơi khó khăn! Chúng ta đang chuyển sang một số vùng Lit trung gian, vì vậy đừng lo lắng nếu mọi thứ không hoàn toàn hợp lý.

Trước khi bắt đầu viết mã, hãy cùng xem qua nhanh các khái niệm mà chúng ta sẽ sử dụng.

Directives​

Như bạn đã thấy, khi viết các mẫu HTML của chúng ta trong Lit, chúng ta viết chúng bên trong thẻ literals html. Điều này cho phép chúng ta sử dụng JavaScript để thay đổi hành vi của các mẫu. Chúng ta có thể làm những việc như đánh giá biểu thức:
Mã:
html`
${this.heading}
`
Chúng ta có thể trả về các mẫu cụ thể trong một số điều kiện nhất định:
Mã:
html`
${this.heading ? this.heading : “Vui lòng nhập tiêu đề”}
`
Sẽ có những lúc chúng ta cần thoát khỏi luồng kết xuất thông thường của hệ thống kết xuất Lit. Bạn có thể muốn kết xuất một cái gì đó sau hoặc mở rộng chức năng mẫu của Lit. Điều này có thể đạt được thông qua việc sử dụng chỉ thị. Lit có một số chỉ thị tích hợp sẵn.

Chúng ta sẽ sử dụng chỉ thị styleMap, cho phép chúng ta áp dụng các kiểu trực tiếp cho một phần tử thông qua một đối tượng JavaScript. Sau đó, đối tượng được chuyển đổi thành các kiểu nội tuyến của phần tử. Điều này sẽ hữu ích khi chúng ta điều chỉnh vị trí của phần tử cửa sổ vì vị trí của phần tử được quản lý bởi các thuộc tính CSS. Tóm lại, styleMap chuyển thành:
Mã:
const top = this.top // một biến mà chúng ta có thể lấy từ lớp, hàm hoặc bất kỳ nơi nàostyleMap({ position: "absolute", left: "100px", top})
into
Mã:
"position: absolute; top: 50px; left: 100px;"
Sử dụng styleMap giúp bạn dễ dàng sử dụng các biến để thay đổi kiểu.

Bộ điều khiển​

Lit có một số cách tiện dụng để tạo thành các thành phần phức tạp từ các đoạn mã nhỏ hơn, có thể tái sử dụng.

Một cách là xây dựng các thành phần từ nhiều thành phần nhỏ hơn. Ví dụ, một nút biểu tượng trông như thế này:



Đánh dấu có thể có đánh dấu sau:
Mã:
class IconButton extends LitElement { render() { return html`     ` }}
Trong ví dụ trên, chúng ta đang biên soạn IconButton của mình từ hai thành phần web đã tồn tại từ trước.

Một cách khác để biên soạn logic phức tạp là đóng gói trạng thái và hành vi cụ thể vào một lớp. Làm như vậy cho phép chúng ta tách các hành vi cụ thể khỏi mã đánh dấu của mình. Điều này có thể được thực hiện thông qua việc sử dụng bộ điều khiển, một cách liên khung để chia sẻ logic có thể kích hoạt kết xuất lại trong một thành phần. Chúng cũng có lợi ích là móc vào vòng đời của thành phần.

Lưu ý: Vì bộ điều khiển liên khung, chúng có thể được sử dụng trong React và Vue với các bộ điều hợp nhỏ.

Với bộ điều khiển, chúng ta có thể làm một số điều thú vị, như quản lý trạng thái kéo và vị trí của thành phần lưu trữ của nó. Thật thú vị, đó chính xác là những gì chúng tôi dự định làm!

Mặc dù bộ điều khiển có vẻ phức tạp, nhưng nếu chúng ta phân tích bộ khung của nó, chúng ta sẽ có thể hiểu được nó là gì và nó làm gì.
Mã:
export class DragController { x = 0; y = 0; state = "idle" styles = {...} constructor(host, options) { this.host = host; this.host.addController(this); } hostDisconnected() {...} onDragStart = (pointer, ev) => {...}; onDrag = (_, pointers) => {...};}
Chúng ta bắt đầu bằng cách khởi tạo bộ điều khiển của mình bằng cách đăng ký nó với thành phần máy chủ và lưu trữ tham chiếu đến máy chủ. Trong trường hợp của chúng tôi, phần tử host sẽ là thành phần a2k-window của chúng tôi.

Sau khi thực hiện xong, chúng tôi có thể hook vào các phương thức vòng đời của host, như hostConnected, hostUpdate, hostUpdated, hostDisconnected, v.v., để chạy logic kéo cụ thể. Trong trường hợp của chúng tôi, chúng tôi chỉ cần hook vào hostDisconnected cho mục đích dọn dẹp.

Cuối cùng, chúng ta có thể thêm các phương thức và thuộc tính của riêng mình vào bộ điều khiển mà sẽ có sẵn cho thành phần máy chủ của chúng ta. Ở đây, chúng ta đang định nghĩa một vài phương thức riêng tư sẽ được gọi trong các hành động kéo. Chúng ta cũng đang định nghĩa một vài thuộc tính mà phần tử máy chủ của chúng ta có thể truy cập.

Khi các hàm onDragonDragStart được gọi, chúng ta cập nhật thuộc tính styles của mình và yêu cầu thành phần máy chủ của chúng ta kết xuất lại. Vì thành phần lưu trữ của chúng ta chuyển đổi đối tượng kiểu này thành CSS nội tuyến (thông qua chỉ thị styleMap), nên thành phần của chúng ta sẽ áp dụng các kiểu mới.

Nếu điều này nghe có vẻ phức tạp, hy vọng sơ đồ luồng này sẽ trực quan hóa quy trình tốt hơn.


Viết bộ điều khiển của chúng ta​

Có thể nói đây là phần kỹ thuật nhất của bài viết, chúng ta hãy kết nối bộ điều khiển của mình!

Chúng ta hãy bắt đầu bằng cách hoàn thiện logic khởi tạo của bộ điều khiển:
Mã:
export class DragController { x = 0; y = 0; state = "idle"; styles = { position: "absolute", top: "0px", left: "0px", }; constructor(host, options) { const { getContainerEl = () => null, getDraggableEl = () => Promise.resolve(null), } = options; this.host = host; this.host.addController(this); this.getContainerEl = getContainerEl; getDraggableEl().then((el) => { if (!el) return; this.draggableEl = el; this.init(); }); } init() {...} hostDisconnected() {...} onDragStart = (pointer) => {...}; onDrag = (_, pointers) => {...};}
Sự khác biệt chính giữa đoạn mã này và bộ khung trước đó là việc bổ sung đối số options. Chúng tôi cho phép phần tử máy chủ của mình cung cấp các lệnh gọi lại cho phép chúng tôi truy cập vào hai phần tử khác nhau: phần tử chứa và phần tử có thể kéo. Chúng tôi sẽ sử dụng các phần tử này sau để tính toán các kiểu vị trí chính xác.

Vì những lý do tôi sẽ đề cập sau, getDraggableEl là một lời hứa trả về phần tử có thể kéo. Sau khi lời hứa được giải quyết, chúng ta lưu trữ phần tử trên phiên bản bộ điều khiển và chúng ta sẽ khởi chạy hàm khởi tạo, hàm này sẽ gắn trình lắng nghe sự kiện kéo vào phần tử có thể kéo.
Mã:
init() { this.pointerTracker = new PointerTracker(this.draggableEl, { start: (...args) => { this.onDragStart(...args); this.state = "dragging"; this.host.requestUpdate(); return true; }, move: (...args) => { this.onDrag(...args); }, end: (...args) => { this.state = "idle"; this.host.requestUpdate(); }, });}
Chúng ta sẽ sử dụng thư viện PointerTracker để theo dõi các sự kiện con trỏ một cách dễ dàng. Sử dụng thư viện này dễ chịu hơn nhiều so với việc viết logic chế độ nhập liệu chéo, đa trình duyệt để hỗ trợ các sự kiện con trỏ.

PointerTracker yêu cầu hai đối số, draggableEl và một đối tượng hàm hoạt động như trình xử lý sự kiện cho các sự kiện kéo:
  • start: được gọi khi con trỏ được nhấn xuống draggableEl;
  • move: được gọi khi kéo draggableEl xung quanh;
  • end: được gọi khi chúng ta nhả con trỏ khỏi draggableEl.
Đối với mỗi sự kiện, chúng ta sẽ cập nhật trạng thái kéo, gọi hàm gọi lại của bộ điều khiển hoặc cả hai. Phần tử máy chủ của chúng ta sẽ sử dụng thuộc tính state làm thuộc tính phần tử, vì vậy chúng ta kích hoạt this.host.requestUpdate để đảm bảo máy chủ được hiển thị lại.

Giống như với draggableEl, chúng ta gán tham chiếu đến thể hiện pointerTracker cho bộ điều khiển của chúng ta để sử dụng sau.

Tiếp theo, hãy bắt đầu thêm logic vào các hàm của lớp. Chúng ta sẽ bắt đầu với hàm onDragStart:
Mã:
onDragStart = (pointer, ev) => { this.cursorPositionX = Math.floor(pointer.pageX); this.cursorPositionY = Math.floor(pointer.pageY);};
Ở đây chúng ta lưu trữ vị trí hiện tại của con trỏ, vị trí này sẽ được sử dụng trong hàm onDrag.
Mã:
onDrag = (_, pointers) => { this.calculateWindowPosition(pointers[0]);};
Khi hàm onDrag được gọi, nó sẽ cung cấp danh sách các con trỏ đang hoạt động. Vì chúng ta sẽ chỉ phục vụ cho một cửa sổ được kéo tại một thời điểm, nên chúng ta có thể chỉ cần truy cập mục đầu tiên trong mảng một cách an toàn. Sau đó, chúng ta sẽ gửi mục đó đến một hàm xác định vị trí mới của phần tử. Hãy thắt dây an toàn vì nó hơi hoang dã:
Mã:
calculateWindowPosition(pointer) { const el = this.draggableEl; const containerEl = this.getContainerEl(); if (!el || !containerEl) return; const oldX = this.x; const oldY = this.y; //Các số thực của JavaScript có thể kỳ lạ, vì vậy chúng ta sẽ chuyển chúng thành số nguyên. const parsedTop = Math.floor(pointer.pageX); const parsedLeft = Math.floor(pointer.pageY); //Các số thực của JavaScript có thể kỳ lạ, vì vậy chúng ta sẽ chuyển chúng thành số nguyên. const cursorPositionX = Math.floor(pointer.pageX); const cursorPositionY = Math.floor(pointer.pageY); const hasCursorMoved = cursorPositionX !== this.cursorPositionX || cursorPositionY !== this.cursorPositionY; // Chúng ta chỉ cần tính toán vị trí cửa sổ nếu vị trí con trỏ đã thay đổi. if (hasCursorMoved) { const { bottom, height } = el.getBoundingClientRect(); const { right, width } = containerEl.getBoundingClientRect(); // Sự khác biệt giữa vị trí trước đó của con trỏ và vị trí hiện tại của nó. const xDelta = cursorPositionX - this.cursorPositionX; const yDelta = cursorPositionY - this.cursorPositionY; // Con đường hạnh phúc - nếu phần tử không cố gắng vượt ra ngoài ranh giới của trình duyệt. this.x = oldX + xDelta; this.y = oldY + yDelta; const outOfBoundsTop = this.y < 0; const outOfBoundsLeft = this.x < 0; const outOfBoundsBottom = bottom + yDelta > window.innerHeight; const outOfBoundsRight = right + xDelta >= window.innerWidth; const isOutOfBounds = outOfBoundsBottom || outOfBoundsLeft || outOfBoundsRight || outOfBoundsTop; // Đặt vị trí con trỏ cho lần tiếp theo hàm này được gọi. this.cursorPositionX = cursorPositionX; this.cursorPositionY = cursorPositionY; // Nếu không, chúng ta buộc cửa sổ phải nằm trong cửa sổ trình duyệt. if (outOfBoundsTop) { this.y = 0; } else if (outOfBoundsLeft) { this.x = 0; } else if (outOfBoundsBottom) { this.y = window.innerHeight - height; } else if (outOfBoundsRight) { this.x = Math.floor(window.innerWidth - width); } this.updateElPosition(); // Chúng ta kích hoạt bản cập nhật vòng đời. this.host.requestUpdate(); }}updateElPosition(x, y) { this.styles.transform = `translate(${this.x}px, ${this.y}px)`;}
Đây chắc chắn không phải là đoạn mã đẹp nhất, vì vậy tôi đã cố gắng hết sức để chú thích đoạn mã để làm rõ những gì đang diễn ra.

Tóm lại:
  • Khi hàm được gọi, chúng ta kiểm tra xem cả draggableElcontainerEl đều khả dụng.
  • Sau đó, chúng ta truy cập vị trí của phần tử và vị trí của con trỏ.
  • Sau đó, chúng ta tính toán xem con trỏ có di chuyển hay không. Nếu không, chúng ta không làm gì cả.
  • Chúng ta đặt vị trí xy mới cho phần tử.
  • Chúng ta xác định xem phần tử có cố gắng phá vỡ ranh giới của cửa sổ hay không.Nếu có, chúng ta sẽ cập nhật vị trí x hoặc y để đưa phần tử trở lại trong ranh giới của cửa sổ.
[*] Chúng ta cập nhật this.styles bằng các giá trị xy mới.
[*] Sau đó, chúng ta kích hoạt hàm vòng đời cập nhật của máy chủ, khiến phần tử của chúng ta áp dụng các kiểu.
Xem lại hàm nhiều lần để đảm bảo bạn tự tin về chức năng của hàm. Có rất nhiều thứ đang diễn ra, vì vậy đừng lo lắng nếu bạn không hiểu ngay.

Hàm updateElPosition là một trợ giúp nhỏ trong lớp để áp dụng các kiểu cho thuộc tính styles.

Chúng ta cũng cần thêm một chút dọn dẹp để đảm bảo rằng chúng ta ngừng theo dõi nếu thành phần của chúng ta vô tình bị ngắt kết nối trong khi đang kéo.
Mã:
hostDisconnected() { if (this.pointerTracker) { this.pointerTracker.stop(); }}
Cuối cùng, chúng ta cần quay lại tệp a2k-window.js và thực hiện ba việc:
  • khởi tạo bộ điều khiển,
  • áp dụng kiểu vị trí,
  • theo dõi trạng thái kéo.
Những thay đổi này trông như thế nào:
Mã:
class A2kWindow extends LitElement { static styles = css`...`; static properties = {...}; constructor() {...} drag = new DragController(this, { getContainerEl: () => this.shadowRoot.querySelector("#window"), getDraggableEl: () => this.getDraggableEl(), }); async getDraggableEl() { await this.updateComplete; return this.shadowRoot.querySelector("#draggable"); } render() { return html`    
${this.heading}
     `; }}
Chúng tôi đang sử dụng this.shadowRoot.querySelector(selector) để truy vấn shadow DOM của chúng tôi. Điều này cho phép bộ điều khiển của chúng tôi truy cập các phần tử DOM qua ranh giới shadow DOM.

Vì chúng ta dự định phân phối các sự kiện từ phần tử kéo của mình, nên chúng ta phải đợi cho đến khi quá trình kết xuất hoàn tất, do đó có câu lệnh await this.updateComplete.

Sau khi hoàn tất mọi việc, bạn sẽ có thể quay lại trình duyệt và kéo thành phần của mình xung quanh, như sau:

10-creating-draggable-gifs-web-components-lit.gif

(Xem trước lớn)

Phần 3: Tạo hiệu ứng Cửa sổ vỡ

Thành phần của chúng ta khá độc lập, điều này thật tuyệt. Chúng ta có thể sử dụng thành phần cửa sổ này ở bất kỳ đâu trên trang web của mình và kéo nó mà không cần viết thêm bất kỳ mã nào.

Và vì chúng ta đã tạo một bộ điều khiển có thể tái sử dụng để xử lý tất cả các chức năng kéo, nên chúng ta có thể thêm hành vi đó vào các thành phần trong tương lai như biểu tượng trên màn hình.

Bây giờ, hãy bắt đầu xây dựng hiệu ứng cửa sổ vỡ tuyệt vời đó khi chúng ta kéo thành phần của mình.

Chúng ta có thể đưa hành vi này vào chính thành phần cửa sổ, nhưng nó không thực sự hữu ích bên ngoài một trường hợp sử dụng cụ thể, tức là tạo ra một hiệu ứng hình ảnh thú vị. Thay vào đó, chúng ta có thể yêu cầu bộ điều khiển kéo của mình phát ra một sự kiện bất cứ khi nào lệnh gọi lại onDrag được gọi. Điều này có nghĩa là bất kỳ ai sử dụng thành phần của chúng tôi đều có thể lắng nghe sự kiện kéo và làm bất cứ điều gì họ muốn.

Để tạo hiệu ứng cửa sổ bị hỏng, chúng ta cần thực hiện hai việc:
  • gửi và lắng nghe sự kiện kéo;
  • thêm phần tử cửa sổ bị hỏng vào DOM.

Gửi và lắng nghe sự kiện trong Lit​

Lit có một số cách khác nhau để xử lý sự kiện. Bạn có thể thêm trình lắng nghe sự kiện trực tiếp trong các mẫu của mình, như sau:
Mã:
handleClick() { console.log("Clicked");}render() { html`Click me!`}
Chúng ta đang định nghĩa hàm mà chúng ta muốn kích hoạt khi nhấp vào nút và truyền nó đến phần tử sẽ được gọi khi nhấp. Đây là một tùy chọn hoàn toàn khả thi và là cách tiếp cận mà tôi sẽ sử dụng nếu phần tử và hàm gọi lại nằm gần nhau.

Như tôi đã đề cập trước đó, chúng ta sẽ không đưa hành vi cửa sổ bị hỏng vào thành phần, vì việc truyền xuống trình xử lý sự kiện thông qua một số thành phần web khác nhau sẽ trở nên cồng kềnh. Thay vào đó, chúng ta có thể tận dụng đối tượng sự kiện cửa sổ gốc để có một thành phần phân phối sự kiện và bất kỳ thành phần tổ tiên nào của nó lắng nghe và phản hồi. Hãy xem ví dụ sau:
Mã:
// Trình lắng nghe sự kiệnclass SpecialListener mở rộng LitElement { constructor() { super() this.specialLevel = ''; this.addEventListener('special-click', this.handleSpecialClick) } handleSpecialClick(e) { this.specialLevel = e.detail.specialLevel; } render() { html` 
${this.specialLevel}
  ` }}// Bộ điều phối sự kiệnlớp SpecialButton mở rộng LitElement { handleClick() { const event = new CustomEvent("special-click", { bubbles: true, combined: true, detail: { specialLevel: 'high', }, }); this.dispatchEvent(event); } render() { html`Click me!` }}
Lưu ý: Đừng quên xem qua các tài nguyên MDN nếu bạn cần ôn lại về Sự kiện DOM gốc.

Chúng ta có hai thành phần, một trình lắng nghe và một trình phân phối. Trình lắng nghe là một thành phần thêm trình lắng nghe sự kiện vào chính nó. Nó lắng nghe sự kiện special-click và đưa ra giá trị mà sự kiện gửi qua.

Thành phần thứ hai của chúng ta, SpecialButton, là hậu duệ của SpecialListener. Đây là một thành phần phân phối sự kiện khi nhấp. Mã bên trong phương thức handleClick rất thú vị, vì vậy chúng ta hãy cùng tìm hiểu những gì đang diễn ra ở đây:
  • Chúng ta tạo một đối tượng sự kiện bằng cách tạo một thể hiện của CustomEvent.
  • Đối số đầu tiên của CustomEvent là tên của sự kiện mà chúng ta muốn phân phối. Trong trường hợp của chúng ta, đó là special-click.
  • Đối số thứ hai của CustomEvent là đối số tùy chọn. Ở đây chúng ta thiết lập ba tùy chọn: bubbles, composeddetail.
  • Đặt bubbles thành true cho phép sự kiện của chúng ta chảy lên cây DOM đến các thành phần tổ tiên.
  • Đặt composed thành true cho phép sự kiện của chúng ta lan truyền ra ngoài gốc shadow của phần tử.
  • Cuối cùng, chúng ta phân phối sự kiện của mình bằng cách kích hoạt this.dispatchEvent(event).
Khi điều này xảy ra, trình lắng nghe sẽ phản ứng với sự kiện bằng cách gọi lại handleSpecialClick.

Chúng ta hãy tiếp tục và phân phối các sự kiện từ bộ điều khiển kéo của mình. Chúng ta sẽ muốn tạo một phiên bản của CustomEvent với tên sự kiện là window-drag. Chúng ta sẽ muốn đặt tùy chọn composedbubbles thành true.

Sau đó, chúng ta sẽ tạo tùy chọn detail với một thuộc tính duy nhất: containerEl. Cuối cùng, chúng ta sẽ muốn phân phối sự kiện.

Tiếp tục và thử triển khai logic này bên trong hàm onDrag.

Gợi ý: Chúng ta sẽ muốn phân phối sự kiện từ phần tử kéo của mình. Đừng quên rằng chúng ta đã lưu tham chiếu đến phần tử trên phiên bản của bộ điều khiển.

Trước khi tôi tiếp tục và tiết lộ câu trả lời, hãy thiết lập trình lắng nghe của chúng ta. Theo cách đó, chúng ta có thể xác định xem chúng ta đã kết nối đúng trình phân phối sự kiện hay chưa.

Nhảy vào tệp script.js và thêm các dòng sau:
Mã:
function onWindowDrag() { console.log('dragging');}window.addEventListener('window-drag', onWindowDrag);
Bây giờ bạn có thể nhảy vào trình duyệt, kéo phần tử của mình và xem nhật ký trong bảng điều khiển.

Bạn có thể kiểm tra giải pháp của mình với giải pháp của tôi bên dưới:
Mã:
onDrag = (_, pointers) => { this.calculateWindowPosition(pointers[0]); const event = new CustomEvent("window-drag", { bubbles: true, compose: true, detail: { containerEl: this.getContainerEl(), }, }); this.draggableEl.dispatchEvent(event);};
Tuyệt! Việc duy nhất còn lại cần làm là thêm phần tử cửa sổ bị hỏng vào DOM mỗi khi chúng ta nhận được sự kiện kéo.

Chúng ta sẽ cần tạo một thành phần cửa sổ bị hỏng mới trông giống như sau:



Cửa sổ bị hỏng của chúng ta sẽ trông đẹp hơn một chút so với cửa sổ thông thường không có nội dung. Đánh dấu cho thành phần sẽ rất đơn giản. Chúng ta sẽ có các div lồng nhau, mỗi div chịu trách nhiệm cho các khía cạnh khác nhau của phần tử:
  • div ngoài cùng sẽ chịu trách nhiệm về vị trí.
  • div ở giữa sẽ chịu trách nhiệm về giao diện.
  • div trong cùng sẽ chịu trách nhiệm về chiều rộng và chiều cao.
Sau đây là toàn bộ mã cho cửa sổ bị hỏng của chúng ta. Hy vọng rằng, đến thời điểm này, không có nội dung nào trong đoạn trích dưới đây là mới đối với bạn:
Mã:
export class BrokenWindow extends LitElement { static properties = { height: {}, width: {}, top: {}, left: {}, }; static styles = css` #outer-container { position: absolute; display: flex; } #middle-container { border: var(--border-width) solid var(--color-gray-400); box-shadow: 2px 2px var(--color-black); background-color: var(--color-gray-500); } `; render() { return html`      `; }}window.customElements.define("a2k-broken-window", BrokenWindow);
Sau khi bạn tạo thành phần, chúng ta có thể kiểm tra xem nó có hoạt động chính xác không bằng cách thêm nội dung sau vào tệp index.html của chúng ta:
Mã:
Nếu bạn thấy nội dung sau trong trình duyệt của mình, thì xin chúc mừng! Cửa sổ bị hỏng của bạn đang hoạt động hoàn hảo.


Phần thưởng​

Bạn có thể nhận thấy rằng cả thành phần a2k-window và thành phần a2k-broken-window của chúng tôi đều có nhiều kiểu giống nhau. Chúng ta có thể tận dụng một trong các kỹ thuật sáng tác của Lit để trừu tượng hóa các đánh dấu và kiểu lặp lại thành một thành phần riêng biệt, a2k-panel. Sau khi hoàn tất, chúng ta có thể sử dụng lại a2k-panel trong các thành phần cửa sổ của mình.

Tôi sẽ không tiết lộ câu trả lời ở đây, nhưng nếu bạn muốn thử, tài liệu hướng dẫn của Lit sẽ giúp ích nếu bạn gặp khó khăn.

Kết xuất cửa sổ bị hỏng khi kéo​

Chúng ta đang ở điểm dừng cuối cùng trong hành trình thành phần web cổ điển của mình.

Để tạo hiệu ứng cửa sổ bị hỏng, chúng ta chỉ cần thực hiện một số thao tác sau:
  • Lắng nghe sự kiện window-drag;
  • Truy cập vào các kiểu của vùng chứa;
  • Tạo phần tử a2k-broken-window mới;
  • Đặt top, left, height, width thuộc tính cho phần tử mới của chúng ta;
  • Chèn cửa sổ bị hỏng vào DOM.
Chúng ta hãy chuyển sang tệp script.js của chúng ta:
Mã:
function onWindowDrag(e) { ...}window.addEventListener("window-drag", onWindowDrag);
Chúng ta đang lắng nghe sự kiện window-drag và thiết lập lệnh gọi lại để nhận đối tượng sự kiện khi được gọi.
Mã:
function onWindowDrag(e) { const { containerEl } = e.detail; const { width, top, left, height } = containerEl.getBoundingClientRect();}window.addEventListener("window-drag", onWindowDrag);
Đoạn mã trên thực hiện hai việc:
  • Truy cập containerEl từ đối tượng chi tiết.
  • Sau đó, chúng ta sử dụng hàm getBoundingClientRect của containerEl để lấy các thuộc tính CSS của phần tử.
Mã:
function onWindowDrag(e) { const { containerEl } = e.detail; const { width, top, left, height } = containerEl.getBoundingClientRect(); const newEl = document.createElement("a2k-broken-window"); newEl.setAttribute("width", width); newEl.setAttribute("top", top); newEl.setAttribute("left", left); newEl.setAttribute("height", height);}
Ở đây, chúng ta bắt buộc phải tạo phần tử cửa sổ bị hỏng và áp dụng các kiểu của mình. Đối với bất kỳ ai quen thuộc với việc viết HTML bằng JavaScript (hoặc thậm chí là jQuery), thì đây không phải là một khái niệm xa lạ. Bây giờ, chúng ta sẽ thêm thành phần của mình vào DOM.

Chúng ta cần phải rất cụ thể về nơi chúng ta muốn đặt phần tử. Chúng ta không thể chỉ thêm nó vào phần thân; nếu không, nó sẽ che mất phần tử cửa sổ chính của chúng ta.



Chúng ta cũng không thể viết nó là phần tử đầu tiên của body; nếu không, cửa sổ cũ nhất sẽ xuất hiện phía trên các cửa sổ mới hơn.

Một giải pháp là thêm thành phần của chúng ta vào DOM ngay trước phần tử chứa của chúng ta. Tất cả các nhà phát triển JavaScript ngoài kia có thể háo hức viết tập lệnh riêng của họ để quản lý điều này nhưng may mắn thay, cửa sổ có chức năng hoàn hảo dành cho chúng ta:
Mã:
containerEl.insertAdjacentElement("beforebegin", newEl);
Hàm trên rất tiện dụng cho phép chúng ta kiểm soát vị trí thêm phần tử. Tập lệnh này chèn phần tử mới của chúng ta trước phần tử chứa.

Tập lệnh hoàn thiện của chúng ta trông như thế này:
Mã:
function onWindowDrag(e) { const { containerEl } = e.detail; const { width, top, left, height } = containerEl.getBoundingClientRect(); const newEl = document.createElement("a2k-broken-window"); newEl.setAttribute("width", width); newEl.setAttribute("top", top); newEl.setAttribute("left", left); newEl.setAttribute("height", height); containerEl.insertAdjacentElement("beforebegin", newEl);}window.addEventListener("window-kéo", onWindowDrag);
Quay lại trình duyệt và bắt đầu kéo cửa sổ của bạn. Bây giờ bạn sẽ thấy hiệu ứng cửa sổ tuyệt vời của mình!

Nếu tập lệnh của bạn không hoạt động, thì đừng lo lắng! Mở bảng điều khiển của bạn và xem bạn có thể gỡ lỗi sự cố không. Bạn thậm chí có thể chạy qua các đoạn mã ở trên và đảm bảo mọi thứ đã được sao chép chính xác.

Phần thưởng​

Chúng tôi đã tạo ra một hiệu ứng có thể kéo tuyệt vời bằng cách lắng nghe các sự kiện kéo và viết một số logic tùy chỉnh bên trong trình xử lý.

Nhưng Microsoft đã làm điều này cách đây 20 năm. Tôi rất muốn xem cộng đồng Smashing sáng tạo có thể tạo ra những hiệu ứng tuyệt vời nào thay thế! Đây là tôi đang có một chút vui vẻ:

rainbow-drag.gif

(Xem trước lớn)
Vui lòng gửi cho Twitter của tôi những gì bạn đã tạo ra bằng bài viết này. 😄

Kết luận​

Cảm ơn bạn đã theo dõi hết bài viết! Chúng ta đã đề cập đến nhiều vấn đề. Tôi hy vọng bài viết này đã giúp bạn thoải mái hơn khi viết các thành phần web bằng thư viện Lit tuyệt vời. Quan trọng nhất, tôi hy vọng bạn thích tham gia cùng tôi để xây dựng một cái gì đó thú vị.

Cửa sổ có thể kéo là một phần của thư viện giao diện người dùng thành phần web của tôi, A2k, mà bạn có thể sử dụng trong các dự án của riêng bạn. Bạn có thể thử bằng cách truy cập kho GitHub.

Nếu bạn muốn hỗ trợ dự án, bạn có thể theo dõi tôi trên Twitter để cập nhật hoặc để lại kho lưu trữ là một ngôi sao GitHub.

Tôi cũng muốn gửi lời cảm ơn đến Elliott Marquez, Nhà phát triển Lit tại Google, vì đã là người đánh giá kỹ thuật.
 
Back
Bên trên