DOM bóng 201

CSS và định 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. Mô hình này dựa trên các khái niệm đã thảo luận trong Shadow DOM 101. Nếu bạn muốn tìm phần giới thiệu, hãy xem bài viết đó.

Giới thiệu

Hãy đối mặt với nó. Chẳng có gì hấp dẫn trong việc đánh dấu chưa định kiểu. Thật may cho chúng tôi, những người xuất sắc của nhóm Web Components đã nhìn thấy điều này và không để chúng tôi chờ đợi. CSS Scoping Module (Mô-đun phạm vi của CSS) xác định nhiều tuỳ chọn để định 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 DOM bóng là ranh giới bóng. Lớp này có nhiều thuộc tính hay, nhưng một trong những thuộc tính tốt nhất là cung cấp miễn phí việc đóng gói kiểu. Trình bày theo 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 nhận xé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ó h3 phù hợp với bộ chọn h3 và do đó có kiểu màu đỏ là h3 trong ShadowRoot. Xin nhắc lại là kiểu 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 mục tiêu h3 không xuất hiện trong nội dung của tôi. Đó là vì trình chọn không vượt qua ranh giới bóng.

Đạo đức của câu chuyện? Chúng tôi có đóng gói kiểu từ thế giới 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 vấn đề là các quy tắc trong trang mẹ có mức độ đặc trưng cao hơn quy tắc :host được xác định trong phần tử, nhưng lại thấp hơn so với thuộc tính style được xác định trên phần tử lưu trữ. Nhờ vậy, người dùng có thể 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 nên bạn không thể sử dụng nó bên ngoài Shadow DOM.

Hình thức chức năng của :host(<selector>) cho phép bạn nhắm mục tiêu phần tử máy chủ lưu trữ nếu phần tử đó khớp với <selector>.

Ví dụ – chỉ 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

Trường hợp sử dụng phổ biến cho :host là khi bạn tạo một Phần tử tuỳ chỉnh và muốn phản ứng với nhiều trạng thái người dùng (:di chuột, :focus, :đang hoạt động, 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>

Sắp xếp theo chủ đề cho một phần tử

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

:host-context() thường 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 tuỳ chỉnh 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 <x-foo> khi đây là thành phần con của một phần tử có lớp .different:

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

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

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 cho :host là nếu bạn đang tạo thư viện giao diện và muốn hỗ trợ định kiểu cho nhiều loại phần tử lưu trữ từ trong cùng một DOM bóng.

: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 DOM bóng bên trong từ bên ngoài

Phần tử giả ::shadow và tổ hợp /deep/ giống như một thanh kiếm Vorpal của thẩm quyền CSS. Chúng cho phép xuyên qua ranh giới của DOM bóng để 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 đổ. Tính năng này cho phép bạn ghi các bộ chọn định kiểu cho các nút nội bộ thành giá trị bóng tối 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ể ghi #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> phần tử con trong DOM bóng. Mỗi bảng điều khiển có 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/

Trình kết hợp /deep/ tương tự như ::shadow, nhưng mạnh mẽ hơn. Chế độ xem này hoàn toàn bỏ qua mọi ranh giới bóng và chuyển vào số lượng cây bóng đổ bất kỳ. Nói một cách đơn giản, /deep/ cho phép bạn đi sâu vào bên trong một phần tử và nhắm mục tiêu đến bất kỳ nút nào.

Trình kết hợp /deep/ đặc biệt hữu ích trong các Thành phần tuỳ chỉnh, nơi thông thường sẽ có nhiều cấp độ DOM bóng. Ví dụ chính là lồng nhiều 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ả các phần tử <x-panel> là thành phần 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, ở vị trí bất kỳ trong cây bóng:

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

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

Cũng giống như .shadowRoot mở cây bóng để truyền tải DOM, trình kết hợp cũng mở cây bóng để truyền tải bộ chọn. Thay vì viết một chuỗi cơn điên rồ lồng ghé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');

Tạo kiểu cho phần tử gốc

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

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

Tạo điểm nhấn phong cách

Tuỳ chỉnh tốt. Trong một số trường hợp, bạn có thể chọc vào các lỗ trên khiên tạo kiểu của Bóng và tạo móc để người khác tạo kiểu.

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

/deep/ có rất nhiều quyền lực. Cách này cung cấp cho các tác giả thành phần cách chỉ định các phần tử riêng lẻ có thể tạo kiểu hoặc một loạt các phần tử có thể tạo kiểu.

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á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 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 thu nhỏ thanh trượt <span style="color:blue">blue</span> bằng cách nhắm mục tiêu ::-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 hook kiểu vào một số 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 mà người bên ngoài có thể tạo kiểu. Việc này được thực hiện 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 thành phần này phải có tiền tố "x-". Làm như vậy sẽ tạo mối liên kết với phần tử đó trong cây bóng đổ và cho người bên ngoài một làn được chỉ định để vượt qua ranh giới của 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 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 các biến CSS

Một cách hiệu quả để tạo chủ đề hấp dẫn là thông qua Biến CSS. Về cơ bản, 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ả của phần tử tuỳ chỉnh đánh dấu các phần giữ chỗ biến trong DOM bóng. Một để tạo kiểu cho phông chữ của nút nội bộ và một để tạo màu sắc:

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ẽ để phù hợp với chủ đề truyện tranh Comic Sans cực hay 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 có màu hồng đào và hoạt động rất đẹp! Toàn bộ hình ảnh sẽ 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 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 DOM bóng. 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à một cách để bắt đầu 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 từ Công cụ cho nhà phát triển

Việc tìm hiểu .resetStyleInheritance sẽ phức tạp hơn một chút, chủ yếu vì thuộc tính này chỉ ảnh hưởng đến các thuộc tính CSS có thể kế thừa. Như vậy, khi bạn đang 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ề những tài sản nào được 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 trạng thái kế thừa" trong bảng điều khiển 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 trong Light DOM và hiển thị các nút đó tại các vị trí được xác định trước trong DOM bóng. Các thành phần này không logic trong DOM tối; 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 chức năng hiển thị.

Các nút đã phân phối giữ lại các kiểu trong tài liệu chính. Tức là các quy tắc kiểu trên trang chính sẽ 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 có logic trong vùng ánh 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 DOM bóng, chúng có thể thực hiện thêm các kiểu được xác định bên trong cây bóng đổ.

phần tử giả ::content

Nút phân phối là nút con của phần tử máy chủ 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 này từ bên trong DOM bóng? Câu trả lời là phần tử giả ::content CSS. Đây là một cách để nhắm mục tiêu các nút DOM ánh sáng đi qua một điểm chèn. Ví dụ:

::content > h3 tạo kiểu cho bất kỳ thẻ h3 nào đi qua một điểm chèn.

Hãy xem một ví dụ:

<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. <content><shadow> điểm chèn 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 có nghĩa là các thuộc tính CSS kế thừa được đặt thành initial tại máy chủ lưu trữ, trước khi các điểm chèn này hiển thị nội dung bóng đổ. 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 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 tôi có rất nhiều tuỳ chọn để kiểm soát giao diện của nội dung. Bóng 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 thông tin đóng gói kiểu theo phạm vi và phương tiện để cho phép nhiều (hoặc ít) thế giới bên ngoài theo cách chúng ta chọn. Bằng cách xác định các phần tử giả tuỳ chỉnh hoặc bao gồm phần giữ chỗ biến CSS, tác giả có thể cung cấp các hook định kiểu thuận tiện của bên thứ ba để tuỳ chỉnh thêm nội dung. Nhìn chung, tác giả web có toàn quyền kiểm soát cách nội dung của họ được thể hiện.