DOM bóng 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. Bài viết này dựa trên các khái niệm được thảo luận trong phần Shadow DOM 101. Nếu bạn đang tìm hiểu về cách sử dụng, hãy xem bài viết đó.

Giới thiệu

Hãy đối mặt với nó. Mã đánh dấu chưa được định kiểu chả có gì hấp dẫn cả. May mắn thay, những người tuyệt vời đứng sau Web Components đã lường trước được điều này và không để chúng tôi phải chờ đợi. Mô-đun CSS Scoping (Mô-đun CSS xác định phạm vi) xác định nhiều tuỳ 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 đặc điểm cốt lõi của DOM tối là ranh giới bóng đổ. Kiểu này có nhiều thuộc tính thú vị, nhưng một trong những thuộc tính tốt nhất là 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 quan sát thú vị về bản minh hoạ này:

  • Có các h3 khác trên trang này, nhưng chỉ có một h3 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, các kiểu theo phạm vi theo mặc định.
  • Các quy tắc 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à do các bộ chọn không vượt qua ranh giới bóng.

Đạo đức của câu chuyện? Chúng ta có đó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>

Có một vấn đề là các quy tắc trên trang mẹ có tính cụ thể cao hơn so với các quy tắc :host được xác định trong phần tử, nhưng có độ cụ thể thấp hơn so với thuộc tính style được xác định trên phần tử máy chủ 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 ngữ 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 chức năng của :host(<selector>) cho phép bạn nhắm đến phần tử máy chủ nếu phần tử đó khớp với <selector>.

Ví dụ – chỉ so khớp nếu 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 người dùng

Một trường hợp sử dụng phổ biến của :host là khi bạn tạo một Phần tử tuỳ chỉnh và muốn phản ứng với các trạng thái người dùng khác nhau (: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>

Tuỳ chỉnh giao diện cho một phần tử

Lớp giả lập :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ử cấp trên nào khớp với <selector>.

:host-context() thường được dùng để thiết kế giao diện cho một phần tử dựa trên 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 kiểu trong Shadow DOM của một phần tử để tạo 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 cách sử dụng khác của :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>. */
}

Định kiểu nội bộ Shadow DOM từ bên ngoài

Phần tử giả ::shadow và bộ kết hợp /deep/ giống như một thanh kiếm Vorpal có thẩm quyền CSS. Các lớp 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. Nó cho phép bạn ghi các bộ chọn tạo kiểu cho các nút nội bộ trong Shadow dom 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><x-panel> 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 những tiêu đề đó từ trang chính, bạn có thể viết:

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

Toán tử kết hợp /deep/

Toán tử kết hợp /deep/ tương tự như ::shadow, nhưng mạnh mẽ hơn. Nó bỏ qua hoàn toàn mọi ranh giới bóng và đi vào số lượng cây bóng tối bất kỳ. 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 phần tử tuỳ chỉnh, nơi thường có nhiều cấp độ DOM tối. Ví dụ điển hình là lồng một nhóm các 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ừ 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ả các phần tử bằng lớp .library-theme, ở bất kỳ vị trí nào 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 khi di chuyển qua DOM, các toán tử kết hợp sẽ mở cây bóng khi di chuyển qua bộ chọn. Thay vì viết một chuỗi điên rồ lồng nhau, 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 thành phần điều khiển HTML gốc là một thách thức đối với kiểu. Nhiều người chỉ cần bỏ cuộc và tự tạo. Tuy nhiên, với ::shadow/deep/, bạn có thể tạo kiểu cho mọi phần tử trong nền tảng web sử dụng Shadow DOM. Ví dụ điển hình là các loại <input><video>:

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

Tạo phần lồng ghép kiểu

Có thể tuỳ chỉnh. Trong một số trường hợp, bạn có thể muốn tạo lỗ trong khi tạo kiểu cho Shadow và tạo các móc để người khác tạo kiểu.

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

Có rất nhiều sức mạnh phía sau /deep/. Phương thức này giú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 các phần tử là có thể tạo giao diện.

Ví dụ – tạo kiểu cho tất cả cá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ả WebKitFirefox đều xác định các phần tử giả để tạo kiểu cho các phần nội bộ 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 móc định kiểu vào một số thành phần nội bộ, tác giả của nội dung Shadow DOM có thể chỉ định một số phần tử nhất định là có thể định kiểu bởi các thành phần bên ngoài. Bạn có thể thực hiện việc này thông qua các 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 phần tử đó cần phải có tiền tố là "x-". Việc này sẽ tạo ra một 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 định kiểu cho con trỏ thanh 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 móc giao diện là thông qua Biến CSS. Về cơ bản, việc này sẽ tạo "phần giữ chỗ 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 phần giữ chỗ biến trong Shadow DOM của họ. Một để định kiểu phông chữ của nút nội bộ và một để định kiểu màu 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ó thể là để phù hợp với giao diện Comic Sans cực kỳ ngầu trên trang của họ:

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

Do cách Biến CSS kế thừa, mọi thứ đều ổn và hoạt động rất tốt! Toàn bộ ảnh 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>

Đặ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, Shadow DOM 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 bóng. Hãy coi đây là cách để bắt đầu lại khi tạo một thành phần mới.

resetStyleInheritance

Dưới đây là bản minh hoạ cho thấy cách cây bóng đổ chịu ảnh hưởng 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 DevTools

Việc hiểu .resetStyleInheritance sẽ khó khăn hơn một chút, chủ yếu là vì thuộc tính này chỉ ảnh hưởng đến các thuộc tính CSS có thể kế thừa. Nội dung này cho biết: khi bạn tìm kiếm một thuộc tính để kế thừa, ở 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ề thuộc tính nào 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 "Hiện thuộc tính kế thừa" trong bảng điều khiển Thành phần.

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 Shadow DOM. Theo logic, chúng không thuộc Shadow DOM; chúng vẫn là thành phần con của phần tử máy chủ lưu trữ. Điểm chèn chỉ là một phần tử hiển thị.

Các nút được phân phối giữ lại các kiểu từ tài liệu chính. Điều đó có nghĩa là các quy tắc kiểu trong trang chính sẽ tiếp tục áp dụng cho các phần tử, ngay cả khi các phần tử đó hiển thị ở điểm chèn. Xin nhắc lại, các nút được phân phối vẫn nằm trong luồng ánh sáng theo logic 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, các nút này có thể sử 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 được 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 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 định 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 các đ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><shadow> cũng có tuỳ 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 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 các thuộc tính này 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 các đ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òn 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 là nền tảng cho thế giới mới mẻ này.

Shadow DOM cho phép chúng tôi đóng gói kiểu trong phạm vi và là một phương thức để đưa nhiều (hoặc ít) vào thế giới bên ngoài tuỳ theo lựa chọn của chúng tôi. Bằng cách xác định phần tử giả tuỳ chỉnh hoặc đưa vào phần giữ chỗ Biến CSS, tác giả có thể cung cấp cho bên thứ ba các móc định kiểu thuận tiện để tuỳ chỉnh thêm nội dung của họ. Tóm lại, 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.