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, Nhà phát triển front-end cấp cao tại Nordhealth. Tôi làm việc về 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 của chúng tôi. Tôi muốn chia sẻ cách chúng tôi giải quyết các vấn đề liên quan đến việc tạo kiểu Thành phần web bằng cách sử dụng Thuộc tính tuỳ chỉnh CSS và 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

Để tạo Thành phần web, chúng ta 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 trong phạm vi, tạo mẫu, v.v. Lit không chỉ nhẹ mà còn được xây dựng trên các API JavaScript gốc, nghĩa là chúng ta có thể phân phối một gói mã gọn nhẹ 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ó hoặc thậm chí không cần khung nào cả. 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 cho thấy đây không phải là phần tử HTML gốc là dấu gạch nối nhất quán trong các thẻ. Đây là tiêu chuẩn để cho trình duyệt biế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 được tạo ở trên trên một trang.

Đóng gói kiểu Shadow DOM

Tương tự như các phần tử HTML gốc có Shadow DOM, Thành phần web cũng vậy. Shadow DOM là một cây ẩn gồm 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 cây Shadow DOM". Sau khi thực hiện việc này, hãy thử xem một phần tử đầu vào gốc trong trình kiểm tra. Giờ đây, bạn có thể mở phần tử đầu vào đó và xem tất cả các phần tử trong đó. Bạn thậm chí có thể thử làm việc 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 Shadow DOM của thành phần đó.

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

Một trong những ưu điểm (hoặc nhược điểm, tuỳ thuộc vào quan điểm của bạn) của Shadow DOM là đóng gói kiểu. Nếu bạn viết CSS trong Thành phần web, các kiểu đó sẽ không thể rò rỉ 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 thể 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 ta đảm bảo hơn rằng khi người dùng sử dụng một trong các thành phần của chúng ta, thành phần đó sẽ trông như chúng ta mong muốn, bất kể kiểu nào được áp dụng cho trang mẹ. Để đảm bảo hơn nữa, chúng ta sẽ thêm all: unset; vào thư mục gốc hoặc "máy chủ lưu trữ" của tất cả 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 gốc bóng hoặc bộ chọn máy chủ lưu trữ.

Tuy nhiên, nếu người dùng Thành phần web của bạn có lý do chính đáng để thay đổi một số 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 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 tạo kiểu đó?

Đó là lúc bạn cần đến Thuộc tính tuỳ chỉnh CSS.

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 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 phải thêm hai dấu gạch nối vào đầu các từ khoá. Sau khi khai báo thuộc tính tuỳ chỉnh, bạn có thể sử dụng giá trị đó trong CSS bằng hàm var().


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

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

Khi nói đến tính kế thừa, tất cả Thuộc tính tuỳ chỉnh đều được kế thừa, tuân theo hành vi thông thường của các thuộc tính và giá trị CSS thông thường. Bạn có thể sử dụng bất kỳ thuộc tính tuỳ chỉnh nào được áp dụng cho 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 sử dụng nhiều thuộc tính tuỳ chỉnh cho mã thông báo thiết kế bằng cách áp dụng các thuộc tính này cho phần tử gốc thông qua Khung CSS. Điều này có nghĩa là tất cả các phần tử 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 một 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 bằng cách sử dụng hàm var() là cách chúng ta xuyên qua Shadow DOM của Thành phần web và cho phép nhà phát triển có nhiều quyền kiểm soát chi tiết hơn khi tạo kiểu cho các thành phần của chúng ta.

Thuộc tính tuỳ chỉnh trong thành phần web Nord

Bất cứ khi nào phát triển một thành phần cho hệ thống thiết kế, chúng tôi đều áp dụng một phương pháp cẩn thận cho CSS của thành phần đó. Chúng tôi muốn hướng đến mã gọn gàng nhưng rất dễ bảo trì. Các 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 của chúng ta 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);
  /* ... */
}
Các thuộc tính tuỳ chỉnh CSS đượ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 ta. Trong một số trường hợp, chúng ta 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 ta sẽ xác định một Thuộc tính tuỳ chỉnh theo ngữ cảnh mới và áp dụng giá trị đó.


: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);
  /* ... */
}
Các thuộc tính tuỳ chỉnh được xác định trên gốc bóng của thành phần, sau đó được sử dụng trong 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 ta cũng sẽ trừu tượng hoá một số giá trị dành riêng cho thành phần nhưng không có trong mã thông báo và biến chúng thành Thuộc tính tuỳ chỉnh theo ngữ cảnh. Các thuộc tính tuỳ chỉnh theo ngữ cảnh của thành phần mang lại cho chúng ta hai lợi ích chính. Thứ nhất, điều này có nghĩa là chúng ta có thể "khô" 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);
}
Thuộc tính tuỳ chỉnh theo ngữ cảnh khoảng đệm nhóm thẻ được sử dụng ở nhiều vị trí trong mã thành phần.

Thứ hai, việc này giúp trạng thái thành phần và các thay đổi về biến thể trở nên rõ ràng hơn – bạn chỉ cần thay đổi thuộc tính tuỳ chỉnh để cập nhật tất cả các thuộc tính đó khi bạn định kiểu cho trạng thái di chuột hoặc trạng thái đang hoạt động hoặc trong trường hợp này là một biến thể.


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

Nhưng lợi ích mạnh mẽ 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 API CSS tuỳ chỉnh cho từng thành phần. Người dùng của thành phần đó có thể khai thác API này.


<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ủa chúng ta có Thuộc tính tuỳ 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ề kiểu cho người dùng trong khi vẫn kiểm soát được hầu hết các kiểu thực tế. Ngoài ra, chúng ta (với vai trò là nhà phát triển thành phần) có thể chặn những kiểu mà 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 thay đổi bất kỳ mã nào.

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

Tận dụng Thuộc tính tuỳ chỉnh

Tại thời điểm viết bài, chúng tôi không thực sự tiết lộ các Thuộc tính tuỳ chỉnh theo ngữ cảnh này trong tài liệu của chúng tôi; tuy nhiên, chúng tôi dự định sẽ làm như vậy để nhóm phát triển rộng rãi hơn có thể hiểu và tận dụng các thuộc tính này. Các thành phần của chúng tôi được đóng gói trên npm bằng tệp kê khai, chứa mọi thông tin cần biết về các thành phần đó. Sau đó, chúng ta sử dụng tệp kê khai dưới dạng dữ liệu khi triển khai trang web tài liệu. Việc này được thực hiện bằng cách sử dụng Eleventytính năng Dữ liệu toàn cầu của Eleventy. Chúng tôi dự định đưa các Thuộc tính tuỳ chỉnh theo ngữ cảnh này vào tệp dữ liệu tệp 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 hai thành phần đường phân chia, bạn cần nhắm đến cả hai thành phần đó một cách cụ thể 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 đường phân chia cần được xử lý màu theo hai cách khác nhau. Một phần lồng bên trong một phần mà chúng ta có thể sử dụng cho một bộ chọn cụ thể hơn, nhưng chúng ta phải nhắm mục tiêu cụ thể đến đường phân chia.

Lý do bạn phải đặt giá trị thuộc tính tuỳ chỉnh trực tiếp trên thành phần là vì chúng ta đang xác định các giá trị đó 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ế toàn cục mà chúng ta sử dụng trực tiếp trong thành phần sẽ truyền thẳng qua, không bị vấn đề này ảnh hưởng và thậm chí có thể bị chặn trên các phần tử mẹ. Làm cách nào để có được những ưu điểm tốt nhất của cả hai phương pháp?

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 thuộc tính do Lea Verou tổng hợp. Đây là một Thuộc tính tuỳ chỉnh "riêng tư" theo ngữ cảnh trên chính thành phần đó, nhưng được đặt thành Thuộc tính tuỳ chỉnh "công khai" có phương án 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);
  /* ... */
}
CSS Thành phần web của đường phân chia với cá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 một 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ó 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 tất cả những việc đã làm trước đây, chẳng hạn như kế thừa các 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 cũng sẽ kế thừa một cách linh hoạt các định nghĩa mới của thuộc tính đó trên chính thành phần đó hoặc 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>
Hai đường phân chia lại xuất hiện, nhưng lần này bạn có thể đổi màu đường phân chia bằng cách thêm Thuộc tính tuỳ chỉnh theo ngữ cảnh của đường phân chia vào bộ chọn phần. Đường phân chia sẽ kế thừa thuộc tính này, tạo ra một đoạn mã rõ ràng và linh hoạt hơn.

Mặc dù có thể cho rằng phương thức 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 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 của mình để 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, đồng thời vẫn được hưởng lợi từ các giới hạn an toàn mà chúng tôi đã á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 ý kiến của bạn. Nếu quyết định sử dụng một trong những phương pháp này trong công việc của riêng 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, cũng như các thành viên còn lại trong nhóm của tôi. Họ đã nỗ lực để xây dựng hệ thống thiết kế này và triển khai 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ț