Shadow DOM 201

CSS và kiểu

Bài viết này thảo luận thêm về những điều tuyệt vời mà bạn có thể làm với Shadow DOM. Lớp học này dựa trên các khái niệm được thảo luận trong bài viết Shadow DOM 101. Nếu bạn muốn xem phần giới thiệu, hãy xem bài viết đó.

Giới thiệu

Hãy đối mặt với thực tế. Không có gì hấp dẫn về mã đánh dấu không được tạo kiểu. May mắn cho chúng ta, những người xuất sắc đứng sau Thành phần web đã lường trước được điều này và không để chúng ta phải chờ đợi. CSS Scoping Module (Mô-đun xác định phạm vi CSS) xác định nhiều lựa chọn để tạo kiểu cho nội dung trong cây bóng.

Đóng gói kiểu

Một trong những tính năng cốt lõi của Shadow DOM là ranh giới bóng. Nó có rất nhiều thuộc tính hay, nhưng một trong những thuộc tính tốt nhất là nó cung cấp tính năng đóng gói kiểu miễn phí. Nói cách khác:

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

Có hai điểm thú vị về bản minh hoạ này:

  • Có các h3 khác trên trang này, nhưng h3 duy nhất khớp với bộ chọn h3 và do đó được tạo kiểu màu đỏ là h3 trong ShadowRoot. Xin nhắc lại, kiểu trong phạm vi theo mặc định.
  • Các quy tắc về kiểu khác được xác định trên trang này nhắm đến h3 không ảnh hưởng đến nội dung của tôi. Đó là vì bộ chọn không vượt qua ranh giới bóng.

Bài học rút ra là gì? Chúng ta có tính năng đóng gói kiểu từ bên ngoài. Cảm ơn Shadow DOM!

Định kiểu cho phần tử lưu trữ

:host cho phép bạn chọn và tạo kiểu cho phần tử lưu trữ cây bóng:

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

Một điểm cần lưu ý là các quy tắc trong trang mẹ có tính đặc thù cao hơn các quy tắc :host được xác định trong phần tử, nhưng có tính đặc thù thấp hơn thuộc tính style được xác định trên phần tử lưu trữ. Điều này cho phép người dùng ghi đè kiểu của bạn từ bên ngoài. :host cũng chỉ hoạt động trong bối cảnh của ShadowRoot, vì vậy, bạn không thể sử dụng nó bên ngoài Shadow DOM.

Dạng hàm của :host(<selector>) cho phép bạn nhắm đến phần tử lưu trữ nếu phần tử đó khớp với <selector>.

Ví dụ – chỉ so khớp nếu chính phần tử đó có lớp .different (ví dụ: <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

Phản ứng với trạng thái của người dùng

Một trường hợp sử dụng phổ biến cho :host là khi bạn đang tạo một Phần tử tuỳ chỉnh và muốn phản ứng với các trạng thái khác nhau của người dùng (:hover, :focus, :active, v.v.).

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

Tạo giao diện cho một phần tử

Lớp giả :host-context(<selector>) khớp với phần tử lưu trữ nếu phần tử đó hoặc bất kỳ phần tử gốc nào của phần tử đó khớp với <selector>.

:host-context() thường được dùng để tạo giao diện cho một phần tử dựa trên môi trường xung quanh. Ví dụ: nhiều người tạo giao diện bằng cách áp dụng một lớp cho <html> hoặc <body>:

<body class="different">
  <x-foo></x-foo>
</body>

Bạn có thể :host-context(.different) để tạo kiểu cho <x-foo> khi phần tử này là phần tử con của một phần tử có lớp .different:

:host-context(.different) {
  color: red;
}

Điều này cho phép bạn đóng gói các quy tắc định kiểu trong Shadow DOM của một phần tử để định kiểu riêng cho phần tử đó, dựa trên ngữ cảnh của phần tử.

Hỗ trợ nhiều loại máy chủ lưu trữ trong một gốc bóng

Một trường hợp sử dụng khác cho :host là nếu bạn đang tạo một thư viện giao diện và muốn hỗ trợ tạo kiểu cho nhiều loại phần tử lưu trữ trong cùng một Shadow DOM.

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

Tạo kiểu cho nội dung bên trong Shadow DOM từ bên ngoài

Phần tử giả ::shadow và bộ kết hợp /deep/ giống như việc có một thanh kiếm Vorpal của cơ quan CSS. Các bộ chọn này cho phép xuyên qua ranh giới của Shadow DOM để tạo kiểu cho các phần tử trong cây bóng.

Phần tử giả ::shadow

Nếu một phần tử có ít nhất một cây bóng, thì phần tử giả ::shadow sẽ khớp với chính gốc bóng. Bộ chọn này cho phép bạn viết các bộ chọn tạo kiểu cho các nút bên trong DOM bóng của một phần tử.

Ví dụ: nếu một phần tử đang lưu trữ một gốc bóng, bạn có thể viết #host::shadow span {} để tạo kiểu cho tất cả các span trong cây bóng của phần tử đó.

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

Ví dụ (phần tử tuỳ chỉnh) – <x-tabs> có <x-panel> phần tử con trong Shadow DOM. Mỗi bảng điều khiển lưu trữ cây bóng riêng chứa các tiêu đề h2. Để tạo kiểu cho các tiêu đề đó từ trang chính, bạn có thể viết:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

Bộ kết hợp /deep/

Bộ kết hợp /deep/ tương tự như ::shadow nhưng mạnh mẽ hơn. Nó hoàn toàn bỏ qua tất cả ranh giới bóng và vượt qua bất kỳ số lượng cây bóng nào. Nói một cách đơn giản, /deep/ cho phép bạn đi sâu vào nội dung của một phần tử và nhắm đến bất kỳ nút nào.

Bộ kết hợp /deep/ đặc biệt hữu ích trong thế giới Phần tử tuỳ chỉnh, nơi thường có nhiều cấp độ Shadow DOM. Ví dụ điển hình là lồng một loạt phần tử tuỳ chỉnh (mỗi phần tử lưu trữ cây bóng riêng) hoặc tạo một phần tử kế thừa từ một phần tử khác bằng cách sử dụng <shadow>.

Ví dụ (phần tử tuỳ chỉnh) – chọn tất cả phần tử <x-panel> là phần tử con của <x-tabs>, ở bất kỳ vị trí nào trong cây:

x-tabs /deep/ x-panel {
    ...
}

Ví dụ – tạo kiểu cho tất cả phần tử có lớp .library-theme, ở bất kỳ đâu trong cây bóng:

body /deep/ .library-theme {
    ...
}

Làm việc với querySelector()

Giống như .shadowRoot mở cây bóng cho việc truyền tải DOM, các tổ hợp mở cây bóng cho việc truyền tải bộ chọn. Thay vì viết một chuỗi lồng nhau phức tạp, bạn có thể viết một câu lệnh duy nhất:

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

Định kiểu cho các phần tử gốc

Các chế độ điều khiển HTML gốc là một thách thức đối với kiểu. Nhiều người chỉ đơn giản là bỏ cuộc và tự làm. Tuy nhiên, với ::shadow và /deep/, mọi phần tử trong nền tảng web sử dụng Shadow DOM đều có thể được tạo kiểu. Ví dụ điển hình là các loại <input> và <video>:

video /deep/ input[type="range"] {
  background: hotpink;
}

Tạo hook kiểu

Tuỳ chỉnh là một lựa chọn phù hợp. Trong một số trường hợp, bạn có thể muốn tạo lỗ hổng trong lá chắn định kiểu của Shadow và tạo các điểm nối để người khác định kiểu.

Sử dụng ::shadow và /deep/

/deep/ có rất nhiều tính năng mạnh mẽ. API này cho phép tác giả thành phần chỉ định các phần tử riêng lẻ là có thể tạo kiểu hoặc một loạt phần tử là có thể tạo giao diện.

Ví dụ – tạo kiểu cho tất cả phần tử có lớp .library-theme, bỏ qua tất cả cây bóng:

body /deep/ .library-theme {
    ...
}

Sử dụng phần tử giả tuỳ chỉnh

Cả WebKit và Firefox đều xác định các phần tử giả để tạo kiểu cho các phần bên trong của phần tử trình duyệt gốc. Một ví dụ điển hình là input[type=range]. Bạn có thể tạo kiểu cho nút trượt <span style="color:blue">blue</span> bằng cách nhắm đến ::-webkit-slider-thumb:

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

Tương tự như cách trình duyệt cung cấp các điểm móc tạo kiểu cho một số thành phần nội bộ, tác giả của nội dung DOM tối có thể chỉ định một số phần tử nhất định là có thể tạo kiểu bởi người bên ngoài. Bạn có thể thực hiện việc này thông qua phần tử giả tuỳ chỉnh.

Bạn có thể chỉ định một phần tử làm phần tử giả tuỳ chỉnh bằng cách sử dụng thuộc tính pseudo. Giá trị hoặc tên của giá trị này cần có tiền tố "x-". Việc này sẽ tạo ra mối liên kết với phần tử đó trong cây bóng và cung cấp cho người bên ngoài một làn đường được chỉ định để vượt qua ranh giới bóng.

Dưới đây là ví dụ về cách tạo một tiện ích thanh trượt tuỳ chỉnh và cho phép người dùng tạo kiểu cho nút trượt màu xanh dương:

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

Sử dụng biến CSS

Một cách hiệu quả để tạo các hook giao diện là thông qua Biến CSS. Về cơ bản, bạn sẽ tạo "phần giữ chỗ cho kiểu" để người dùng khác điền vào.

Hãy tưởng tượng một tác giả phần tử tuỳ chỉnh đánh dấu các phần giữ chỗ biến trong Shadow DOM của họ. Một lớp để tạo kiểu cho phông chữ của nút bên trong và một lớp khác để tạo kiểu cho màu sắc của nút:

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

Sau đó, trình nhúng của phần tử sẽ xác định các giá trị đó theo ý thích của họ. Có lẽ là để phù hợp với chủ đề Comic Sans cực kỳ thú vị trên trang của họ:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

Do cách kế thừa của Biến CSS, mọi thứ đều ổn và điều này hoạt động rất hiệu quả! Toàn bộ bức tranh sẽ có dạng như sau:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

Đang đặt lại kiểu

Các kiểu có thể kế thừa như phông chữ, màu sắc và chiều cao dòng tiếp tục ảnh hưởng đến các phần tử trong Shadow DOM. Tuy nhiên, để có được sự linh hoạt tối đa, DOM tối cung cấp cho chúng ta thuộc tính resetStyleInheritance để kiểm soát những gì xảy ra ở ranh giới tối. Hãy coi đây là một cách để bắt đầu lại từ đầu khi tạo một thành phần mới.

resetStyleInheritance

Dưới đây là bản minh hoạ cho thấy cây bóng đổ bị ảnh hưởng như thế nào khi thay đổi resetStyleInheritance:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
Thuộc tính kế thừa của Công cụ cho nhà phát triển

Việc tìm hiểu về .resetStyleInheritance sẽ phức tạp hơn một chút, chủ yếu là vì giá trị này chỉ ảnh hưởng đến các thuộc tính CSS có thể kế thừa. Nó cho biết: khi bạn đang tìm một thuộc tính để kế thừa, tại ranh giới giữa trang và ShadowRoot, đừng kế thừa các giá trị từ máy chủ lưu trữ mà hãy sử dụng giá trị initial (theo thông số kỹ thuật CSS).

Nếu bạn không chắc chắn về những thuộc tính kế thừa trong CSS, hãy xem danh sách hữu ích này hoặc bật/tắt hộp đánh dấu "Show inherited" (Hiện thuộc tính kế thừa) trong bảng điều khiển Element (Phần tử).

Tạo kiểu cho các nút được phân phối

Nút phân phối là các phần tử hiển thị tại một điểm chèn (phần tử <content>). Phần tử <content> cho phép bạn chọn các nút từ DOM sáng và hiển thị các nút đó tại các vị trí được xác định trước trong DOM tối. Chúng không nằm trong Shadow DOM theo logic; chúng vẫn là phần tử con của phần tử lưu trữ. Điểm chèn chỉ là một thành phần hiển thị.

Các nút được phân phối sẽ giữ lại kiểu từ tài liệu chính. Tức là các quy tắc về kiểu từ trang chính vẫn tiếp tục áp dụng cho các phần tử, ngay cả khi chúng hiển thị tại một điểm chèn. Xin nhắc lại, các nút được phân phối vẫn nằm trong DOM sáng và không di chuyển. Chúng chỉ hiển thị ở nơi khác. Tuy nhiên, khi các nút được phân phối vào Shadow DOM, chúng có thể áp dụng các kiểu bổ sung được xác định bên trong cây bóng.

phần tử giả ::content

Các nút phân phối là phần tử con của phần tử lưu trữ, vậy làm cách nào để chúng ta có thể nhắm mục tiêu các nút đó từ bên trong Shadow DOM? Câu trả lời là phần tử giả ::content của CSS. Đây là một cách để nhắm đến các nút DOM sáng đi qua một điểm chèn. Ví dụ:

::content > h3 tạo kiểu cho mọi thẻ h3 đi qua một điểm chèn.

Hãy xem ví dụ sau:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

Đặt lại kiểu tại điểm chèn

Khi tạo ShadowRoot, bạn có thể đặt lại các kiểu kế thừa. Điểm chèn <content> và <shadow> cũng có lựa chọn này. Khi sử dụng các phần tử này, hãy đặt .resetStyleInheritance trong JS hoặc sử dụng thuộc tính boolean reset-style-inheritance trên chính phần tử đó.

  • Đối với điểm chèn ShadowRoot hoặc <shadow>: reset-style-inheritance có nghĩa là các thuộc tính CSS có thể kế thừa được đặt thành initial tại máy chủ lưu trữ, trước khi chúng truy cập vào nội dung bóng của bạn. Vị trí này được gọi là ranh giới trên.

  • Đối với điểm chèn <content>: reset-style-inheritance có nghĩa là các thuộc tính CSS có thể kế thừa được đặt thành initial trước khi các phần tử con của máy chủ lưu trữ được phân phối tại điểm chèn. Vị trí này được gọi là ranh giới dưới.

Kết luận

Là tác giả của các phần tử tuỳ chỉnh, chúng ta có rất nhiều lựa chọn để kiểm soát giao diện của nội dung. Shadow DOM tạo nền tảng cho thế giới mới dũng cảm này.

Shadow DOM cung cấp cho chúng ta khả năng đóng gói kiểu trong phạm vi và một phương tiện để cho phép nhiều (hoặc ít) thế giới bên ngoài vào tuỳ ý. Bằng cách xác định các phần tử giả tuỳ chỉnh hoặc thêm phần giữ chỗ Biến CSS, tác giả có thể cung cấp cho bên thứ ba các điểm nối tạo kiểu thuận tiện để tuỳ chỉnh thêm nội dung của họ. Nói chung, tác giả web có toàn quyền kiểm soát cách nội dung của họ được trình bày.