Cách Nordhealth sử dụng Thuộc tính tuỳ chỉnh trong Thành phần web

Lợi ích của việc sử dụng Thuộc tính tuỳ chỉnh trong hệ thống thiết kế và thư viện thành phần.

David Darnes
David Darnes

Tôi là Dave, một Nhà phát triển giao diện cấp cao tại Nordhealth. Tôi phụ trách việc thiết kế và phát triển hệ thống thiết kế Nord, bao gồm cả việc xây dựng Thành phần web cho thư viện thành phần. Tôi muốn chia sẻ cách chúng tôi giải quyết các vấn đề về việc tạo kiểu cho Thành phần web bằng cách sử dụng Thuộc tính tuỳ chỉnh của CSS, cũng như một số lợi ích khác của việc sử dụng Thuộc tính tuỳ chỉnh trong hệ thống thiết kế và thư viện thành phần.

Cách chúng tôi xây dựng Thành phần web

Để xây dựng Thành phần web, chúng tôi sử dụng Lit, một thư viện cung cấp nhiều mã nguyên mẫu như trạng thái, kiểu theo phạm vi, mẫu tạo mẫu, v.v. Lit không chỉ nhẹ mà còn được xây dựng dựa trên API JavaScript gốc, nghĩa là chúng tôi có thể phân phối một gói mã tinh gọn tận dụng các tính năng mà trình duyệt đã có.


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`; } } customElements.define('simple-greeting', SimpleGreeting);
Một thành phần web được viết bằng Lit.

Nhưng điều hấp dẫn nhất về Thành phần web là chúng hoạt động với hầu hết mọi khung JavaScript hiện có, hay thậm chí không sử dụng được bất kỳ khung nào. Sau khi gói JavaScript chính được tham chiếu trong trang, việc sử dụng Thành phần web rất giống với việc sử dụng phần tử HTML gốc. Dấu hiệu duy nhất có thể nhận biết được rằng đó không phải là phần tử HTML gốc chính là dấu gạch nối nhất quán trong các thẻ, đây là tiêu chuẩn để báo hiệu cho trình duyệt rằng đây là một Thành phần web.


// TODO: DevSite - Code sample removed as it used inline event handlers
Sử dụng Thành phần web đã tạo ở trên trên một trang.

Đóng gói kiểu DOM bóng

Tương tự như cách các phần tử HTML gốc có Shadow DOM, các Thành phần web cũng vậy. DOM bóng là cây ẩn các nút trong một phần tử. Cách tốt nhất để hình dung điều này là mở trình kiểm tra web và bật tuỳ chọn "Hiển thị cây DOM bóng". Sau khi hoàn tất việc này, hãy thử xem xét một phần tử đầu vào gốc trong công cụ kiểm tra – giờ đây, bạn sẽ có tuỳ chọn để mở dữ liệu đầu vào đó và xem tất cả các phần tử bên trong nó. Bạn thậm chí có thể thử điều này với một trong các Thành phần web của chúng tôi – hãy thử kiểm tra thành phần đầu vào tuỳ chỉnh của chúng tôi để xem DOM tối tương ứng.

DOM bóng được kiểm tra trong Công cụ cho nhà phát triển.
Ví dụ về Shadow DOM trong phần tử nhập văn bản thông thường và trong Thành phần web đầu vào Nord.

Một trong những ưu điểm (hoặc nhược điểm, tuỳ thuộc vào triển vọng của bạn) đối với DOM bóng là đóng gói kiểu. Nếu bạn viết CSS trong Thành phần web của mình, các kiểu đó không được rò rỉ ra và ảnh hưởng đến trang chính hoặc các phần tử khác; chúng hoàn toàn nằm trong thành phần. Ngoài ra, CSS được viết cho trang chính hoặc Thành phần web mẹ không được rò rỉ vào Thành phần web của bạn.

Việc đóng gói các kiểu này là một lợi ích trong thư viện thành phần của chúng ta. Điều này giúp chúng tôi đảm bảo nhiều hơn rằng khi có người sử dụng một trong các thành phần của chúng tôi, trang đó sẽ trông giống như chúng tôi dự định, bất kể kiểu được áp dụng cho trang mẹ. Và để đảm bảo hơn, chúng ta thêm all: unset; vào thư mục gốc hoặc "máy chủ" của mọi Thành phần web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Một số mã nguyên mẫu thành phần đang được áp dụng cho thư mục gốc đổ bóng hoặc bộ chọn máy chủ.

Tuy nhiên, nếu ai đó sử dụng Thành phần web của bạn có lý do chính đáng để thay đổi các kiểu nhất định thì sao? Có thể có một dòng văn bản cần độ tương phản cao hơn do ngữ cảnh hoặc đường viền cần phải dày hơn? Nếu không có kiểu nào có thể vào thành phần của bạn, làm cách nào để bạn có thể mở khoá các tuỳ chọn định kiểu đó?

Chính vì thế, Thuộc tính tuỳ chỉnh CSS đã phát huy tác dụng.

Thuộc tính tuỳ chỉnh CSS

Thuộc tính tuỳ chỉnh được đặt tên rất phù hợp. Đây là các thuộc tính CSS mà bạn hoàn toàn có thể tự đặt tên và áp dụng bất kỳ giá trị nào cần thiết. Yêu cầu duy nhất là bạn thêm hai dấu gạch nối vào tiền tố của chúng. Sau khi khai báo thuộc tính tuỳ chỉnh, bạn có thể dùng giá trị này trong CSS bằng cách dùng hàm var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Ví dụ từ Khung CSS của chúng tôi về mã thông báo thiết kế dưới dạng Thuộc tính tuỳ chỉnh và mã được sử dụng trên lớp trợ giúp.

Về tính kế thừa, tất cả Thuộc tính tuỳ chỉnh đều được kế thừa theo hành vi điển hình của các giá trị và thuộc tính CSS thông thường. Bạn có thể dùng bất kỳ thuộc tính tuỳ chỉnh nào áp dụng cho một phần tử mẹ hoặc chính phần tử đó làm giá trị trên các thuộc tính khác. Chúng tôi tận dụng Thuộc tính tuỳ chỉnh làm mã thông báo thiết kế bằng cách áp dụng chúng vào thành phần gốc thông qua Khung CSS. Tức là tất cả thành phần trên trang đều có thể sử dụng các giá trị mã thông báo này, cho dù đó là Thành phần web, lớp trợ giúp CSS hay nhà phát triển muốn lấy giá trị từ danh sách mã thông báo của chúng tôi.

Khả năng kế thừa Thuộc tính tuỳ chỉnh này (thông qua việc sử dụng hàm var()) là cách chúng tôi xâm nhập vào DOM tối của Thành phần web và cho phép nhà phát triển kiểm soát chi tiết hơn khi tạo kiểu cho các thành phần.

Thuộc tính tuỳ chỉnh trong một Thành phần Web Bắc

Bất cứ khi nào phát triển một thành phần cho hệ thống thiết kế của mình, chúng tôi đều xem xét kỹ lưỡng CSS của nó. Chúng tôi muốn hướng đến mã tinh gọn nhưng rất dễ bảo trì. Mã thông báo thiết kế mà chúng ta có được xác định là Thuộc tính tuỳ chỉnh trong Khung CSS chính trên phần tử gốc.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Thuộc tính tuỳ chỉnh của CSS đang được xác định trên bộ chọn gốc.

Sau đó, các giá trị mã thông báo này được tham chiếu trong các thành phần của chúng tôi. Trong một số trường hợp, chúng tôi sẽ áp dụng giá trị trực tiếp trên thuộc tính CSS, nhưng đối với các trường hợp khác, chúng tôi sẽ thực sự xác định một Thuộc tính tùy chỉnh theo ngữ cảnh mới và áp dụng giá trị đó cho thuộc tính đó.


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
Thuộc tính tuỳ chỉnh được xác định trên gốc bóng của thành phần và sau đó được dùng trong các kiểu thành phần. Các Thuộc tính tuỳ chỉnh trong danh sách mã thông báo thiết kế cũng đang được sử dụng.

Chúng tôi cũng sẽ tóm tắt một số giá trị cụ thể cho thành phần nhưng không có trong mã thông báo và chuyển các giá trị đó thành Thuộc tính tùy chỉnh theo ngữ cảnh. Thuộc tính tuỳ chỉnh theo bối cảnh của thành phần mang lại cho chúng tôi hai lợi ích chính. Thứ nhất, điều đó có nghĩa là chúng tôi có thể "không" (có thể) nhiều hơn với CSS vì giá trị đó có thể được áp dụng cho nhiều thuộc tính bên trong thành phần.


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
Nhóm thẻ khoảng đệm theo ngữ cảnh Thuộc tính tuỳ chỉnh được sử dụng ở nhiều vị trí trong mã thành phần.

Và thứ hai, nó giúp cho các thay đổi về trạng thái thành phần và biến thể thực sự rõ ràng. Đó chỉ là thuộc tính tuỳ chỉnh cần được thay đổi để cập nhật tất cả các thuộc tính đó khi, giả sử bạn đang tạo kiểu cho trạng thái di chuột hay trạng thái hoạt động, hay trong trường hợp này là một biến thể.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Biến thể của thành phần thẻ, trong đó khoảng đệm đang được thay đổi bằng cách sử dụng một lần cập nhật Thuộc tính tuỳ chỉnh thay vì nhiều nội dung cập nhật.

Nhưng lợi ích lớn nhất là khi xác định các Thuộc tính tuỳ chỉnh theo ngữ cảnh này trên một thành phần, chúng ta sẽ tạo một loại CSS API tuỳ chỉnh cho từng thành phần để người dùng thành phần đó có thể nhấn vào.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Sử dụng thành phần nhóm thẻ trên trang và cập nhật Thuộc tính tuỳ chỉnh khoảng đệm thành kích thước lớn hơn.

Ví dụ trước cho thấy một trong các Thành phần web có Thuộc tính tùy chỉnh theo ngữ cảnh được thay đổi thông qua bộ chọn. Kết quả của toàn bộ phương pháp này là một thành phần cung cấp đủ tính linh hoạt về định kiểu cho người dùng trong khi vẫn kiểm tra được hầu hết các kiểu thực tế. Ngoài ra, chúng tôi, với tư cách là nhà phát triển thành phần, có khả năng chặn các kiểu đó do người dùng áp dụng. Nếu muốn điều chỉnh hoặc mở rộng một trong các thuộc tính đó, chúng ta có thể thực hiện mà không cần người dùng phải thay đổi bất kỳ mã nào của họ.

Chúng tôi thấy cách tiếp cận này cực kỳ hiệu quả, không chỉ đối với chúng tôi, những người tạo ra các thành phần trong hệ thống thiết kế mà còn đối với nhóm phát triển của chúng tôi khi họ sử dụng các thành phần này trong các sản phẩm của chúng tôi.

Khai thác các Thuộc tính tuỳ chỉnh hơn nữa

Tại thời điểm viết bài này, chúng tôi không thực sự tiết lộ các Thuộc tính tuỳ chỉnh theo bối cảnh này trong tài liệu của mình. Tuy nhiên, chúng tôi dự định làm như vậy để nhóm phát triển chung của mình có thể hiểu được và tận dụng các thuộc tính đó. Các thành phần của chúng ta được đóng gói trên npm với một tệp kê khai, chứa mọi thông tin cần thiết về các thành phần đó. Sau đó, chúng tôi sử dụng tệp kê khai dưới dạng dữ liệu khi trang web tài liệu của chúng tôi được triển khai. Việc này được thực hiện bằng Eleventytính năng Dữ liệu toàn cầu của công cụ này. Chúng tôi dự định đưa các Thuộc tính tuỳ chỉnh theo bối cảnh này vào tệp dữ liệu kê khai này.

Một khía cạnh khác mà chúng tôi muốn cải thiện là cách các Thuộc tính tuỳ chỉnh theo bối cảnh này kế thừa giá trị. Ví dụ: hiện tại, nếu muốn điều chỉnh màu của 2 thành phần đường phân chia, bạn cần nhắm mục tiêu cụ thể đến cả 2 thành phần đó bằng bộ chọn hoặc áp dụng thuộc tính tuỳ chỉnh trực tiếp trên phần tử có thuộc tính kiểu. Điều này có vẻ ổn, nhưng sẽ hữu ích hơn nếu nhà phát triển có thể xác định các kiểu đó trên một phần tử chứa hoặc thậm chí ở cấp độ gốc.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Hai thực thể của thành phần phân chia cần 2 cách xử lý màu khác nhau. Một thứ nguyên được lồng bên trong một phần mà chúng ta có thể sử dụng để có bộ chọn cụ thể hơn, nhưng chúng ta phải nhắm mục tiêu cụ thể đến bộ chia.

Lý do bạn phải đặt giá trị Thuộc tính tùy chỉnh trực tiếp trên thành phần là vì chúng tôi đang xác định chúng trên cùng một phần tử thông qua bộ chọn máy chủ lưu trữ thành phần. Mã thông báo thiết kế chung mà chúng ta sử dụng trực tiếp trong thành phần truyền thẳng qua, không bị ảnh hưởng bởi vấn đề này và thậm chí có thể bị chặn trên các phần tử mẹ. Làm cách nào để chúng ta tận dụng được cả hai thế giới?

Thuộc tính tuỳ chỉnh riêng tư và công khai

Thuộc tính tuỳ chỉnh riêng tư là một thứ do Lea Verou kết hợp. Đây là một Thuộc tính tuỳ chỉnh "riêng tư" theo bối cảnh trên chính thành phần đó nhưng được đặt thành một Thuộc tính tuỳ chỉnh "công khai" có chức năng dự phòng.



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
Bộ phân chia CSS Thành phần web có Thuộc tính tuỳ chỉnh theo ngữ cảnh được điều chỉnh để CSS nội bộ dựa vào Thuộc tính tuỳ chỉnh riêng tư đã được đặt thành Thuộc tính tuỳ chỉnh công khai có chức năng dự phòng.

Việc xác định Thuộc tính tuỳ chỉnh theo ngữ cảnh theo cách này có nghĩa là chúng ta vẫn có thể làm mọi việc trước đây, chẳng hạn như kế thừa giá trị mã thông báo chung và sử dụng lại các giá trị trong toàn bộ mã thành phần; nhưng thành phần này cũng sẽ dễ dàng kế thừa các định nghĩa mới của thuộc tính đó trên chính nó hoặc trên bất kỳ phần tử mẹ nào.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Lại lại là hai thanh phân cách nhưng lần này thanh phân chia có thể được đổi màu bằng cách thêm Thuộc tính tuỳ chỉnh theo ngữ cảnh của bộ chia vào bộ chọn mục. Bộ chia sẽ kế thừa nó, tạo ra một đoạn mã rõ ràng và linh hoạt hơn.

Mặc dù có thể lập luận rằng phương pháp này không thực sự "riêng tư", nhưng chúng tôi vẫn cho rằng đây là một giải pháp khá tinh tế cho một vấn đề mà chúng tôi lo ngại. Khi có cơ hội, chúng tôi sẽ giải quyết vấn đề này trong các thành phần để nhóm phát triển có nhiều quyền kiểm soát hơn đối với việc sử dụng thành phần trong khi vẫn hưởng lợi từ những biện pháp bảo vệ đang được áp dụng.

Tôi hy vọng bạn thấy thông tin chi tiết này về cách chúng tôi sử dụng Thành phần web với Thuộc tính tuỳ chỉnh CSS hữu ích. Hãy cho chúng tôi biết suy nghĩ của bạn. Nếu bạn quyết định dùng bất kỳ phương pháp nào trong số này trong công việc của mình, bạn có thể tìm thấy tôi trên Twitter @DavidDarnes. Bạn cũng có thể tìm thấy Nordhealth @NordhealthHQ trên Twitter và các thành viên còn lại trong nhóm của tôi. Họ đã nỗ lực kết hợp hệ thống thiết kế này và thực thi các tính năng được đề cập trong bài viết này: @Viljamis, @WickyNilliams@eric_habich.

Hình ảnh chính của Dan Cristian Pădure Vi,