Chào mừng bạn đến với Cafedev! Trong hành trình học JavaScript từ cơ bản đến nâng cao, việc làm quen với phong cách Shadow DOM là rất quan trọng. Cafedev sẽ hướng dẫn bạn cách sử dụng Shadow DOM để tạo ra các thành phần web độc lập với kiểu dáng riêng biệt, đồng thời giữ cho giao diện của bạn nhất quán và dễ bảo trì. Hãy cùng khám phá cách thiết lập và tùy chỉnh các kiểu trong Shadow DOM để nâng cao hiệu quả phát triển ứng dụng web của bạn.

Shadow DOM có thể bao gồm cả các thẻ <style><link rel="stylesheet" href="…">. Trong trường hợp sau, các stylesheet được lưu vào bộ nhớ cache HTTP, vì vậy chúng không được tải lại cho nhiều thành phần sử dụng cùng một mẫu.
Theo quy tắc chung, các kiểu cục bộ chỉ hoạt động bên trong cây bóng, và các kiểu tài liệu hoạt động bên ngoài nó. Nhưng có một số ngoại lệ.

1. :host

Bộ chọn :host cho phép chọn chủ thể của shadow (phần tử chứa cây bóng(Shadow)).
Ví dụ, chúng ta đang tạo phần tử <custom-dialog> cần phải được căn giữa. Để làm điều đó, chúng ta cần phải định kiểu cho chính phần tử <custom-dialog>.

Đó chính xác là những gì :host thực hiện:


<template id="tmpl">
  <style>
    /* the style will be applied from inside to the custom-dialog element */
    :host {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>

<custom-dialog>
  Hello!
</custom-dialog>

2. Kế thừa

Chủ thể của shadow (<custom-dialog> chính nó) nằm trong light DOM, vì vậy nó bị ảnh hưởng bởi các quy tắc CSS của tài liệu.
Nếu có một thuộc tính được định kiểu cả trong :host cục bộ và trong tài liệu, thì kiểu tài liệu sẽ có ưu tiên.

Ví dụ, nếu trong tài liệu chúng ta có:

<style>
custom-dialog {
  padding: 0;
}
</style>

…Thì <custom-dialog> sẽ không có khoảng đệm.

Điều này rất tiện lợi, vì chúng ta có thể thiết lập các kiểu mặc định cho thành phần trong quy tắc :host của nó, và sau đó dễ dàng ghi đè chúng trong tài liệu.

Ngoại lệ là khi một thuộc tính cục bộ được gán !important, đối với các thuộc tính như vậy, kiểu cục bộ sẽ có ưu tiên.

3. :host(selector)

Giống như :host, nhưng chỉ áp dụng nếu chủ thể của shadow khớp với selector.
Ví dụ, chúng ta muốn căn giữa <custom-dialog> chỉ nếu nó có thuộc tính centered:


<template id="tmpl">
  <style>
*!*
    :host([centered]) {
*/!*
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border-color: blue;
    }

    :host {
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>

<custom-dialog centered>
  Centered!
</custom-dialog>

<custom-dialog>
  Not centered.
</custom-dialog>

Bây giờ các kiểu căn giữa bổ sung chỉ được áp dụng cho hộp thoại đầu tiên: <custom-dialog centered>.
Tóm lại, chúng ta có thể sử dụng các bộ chọn thuộc họ :host để định kiểu cho phần tử chính của thành phần. Những kiểu này (trừ khi !important) có thể bị ghi đè bởi tài liệu.

4. Định kiểu nội dung được chèn

Bây giờ, hãy xem xét tình huống với các slot.
Các phần tử được chèn đến từ light DOM, vì vậy chúng sử dụng các kiểu của tài liệu. Các kiểu cục bộ không ảnh hưởng đến nội dung được chèn.

Trong ví dụ dưới đây, <span> được chèn là đậm, theo kiểu của tài liệu, nhưng không lấy background từ kiểu cục bộ:

run autorun="no-epub" untrusted height=80
<style>
*!*
  span { font-weight: bold }
*/!*
</style>

<user-card>
  <div slot="username">*!*<span>John Smith</span>*/!*</div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
*!*
      span { background: red; }
*/!*
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

Kết quả là đậm, nhưng không có màu đỏ.
Nếu chúng ta muốn định kiểu cho các phần tử được chèn trong thành phần của mình, có hai lựa chọn.

Đầu tiên, chúng ta có thể định kiểu cho chính phần tử <slot> và dựa vào sự kế thừa CSS:

run autorun="no-epub" untrusted height=80
<user-card>
  <div slot="username">*!*<span>John Smith</span>*/!*</div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
*!*
      slot[name="username"] { font-weight: bold; }
*/!*
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

Ở đây <p>John Smith*** trở nên đậm, vì sự kế thừa CSS có hiệu lực giữa <slot> và nội dung của nó. Nhưng trong CSS không phải tất cả các thuộc tính đều được kế thừa.
Một tùy chọn khác là sử dụng pseudo-class ::slotted(selector). Nó khớp với các phần tử dựa trên hai điều kiện:

  1. Đó là một phần tử được chèn từ light DOM. Tên slot không quan trọng. Chỉ bất kỳ phần tử được chèn nào, nhưng chỉ là phần tử đó, không phải các phần tử con của nó.
  2. Phần tử khớp với selector.

Trong ví dụ của chúng ta, ::slotted(div) chọn chính xác thẻ <div slot="username">, nhưng không chọn các phần tử con của nó.


<user-card>
  <div slot="username">
    <div>John Smith</div>
  </div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
*!*
      ::slotted(div) { border: 1px solid red; }
*/!*
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

Xin lưu ý, bộ chọn ::slotted không thể xuống sâu hơn vào slot. Các bộ chọn này không hợp lệ:

::slotted(div span) {
  /* our slotted <div> does not match this */
}

::slotted(div) p {
  /* can't go inside light DOM */
}

Ngoài ra, ::slotted chỉ có thể được sử dụng trong CSS. Chúng ta không thể sử dụng nó trong querySelector.

5. Các hook CSS với thuộc tính tùy chỉnh

Làm thế nào chúng ta có thể định kiểu cho các phần tử nội bộ của một thành phần từ tài liệu chính?
Các bộ chọn như :host áp dụng các quy tắc cho phần tử <custom-dialog> hoặc <user-card>, nhưng làm thế nào để định kiểu cho các phần tử shadow DOM bên trong chúng?

Không có bộ chọn nào có thể trực tiếp ảnh hưởng đến kiểu shadow DOM từ tài liệu. Nhưng cũng giống như chúng ta cung cấp các phương thức để tương tác với thành phần của mình, chúng ta có thể cung cấp các biến CSS (thuộc tính CSS tùy chỉnh) để định kiểu cho nó.

Các thuộc tính CSS tùy chỉnh tồn tại ở tất cả các cấp, cả trong light DOM và shadow DOM.

Ví dụ, trong shadow DOM, chúng ta có thể sử dụng biến CSS --user-card-field-color để định kiểu các trường, và tài liệu bên ngoài có thể đặt giá trị của nó:

<style>
  .field {
    color: var(--user-card-field-color, black);
    /* if --user-card-field-color is not defined, use black color */
  }
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>

Sau đó, chúng ta có thể khai báo thuộc tính này trong tài liệu bên ngoài cho <user-card>:

user-card {
  --user-card-field-color: green;
}

Các thuộc tính CSS tùy chỉnh xuyên qua shadow DOM, chúng có thể nhìn thấy ở khắp mọi nơi, vì vậy quy tắc .field bên trong sẽ sử dụng chúng.
Đây là ví dụ đầy đủ:


<style>
*!*
  user-card {
    --user-card-field-color: green;
  }
*/!*
</style>

<template id="tmpl">
  <style>
*!*
    .field {
      color: var(--user-card-field-color, black);
    }
*/!*
  </style>
  <div class="field">Name: <slot name="username"></slot></div>
  <div class="field">Birthday: <slot name="birthday"></slot></div>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true));
  }
});
</script>

<user-card>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
</user-card>

6. Tóm tắt

Shadow DOM có thể bao gồm các kiểu, chẳng hạn như <style> hoặc <link rel="stylesheet">.
Các kiểu cục bộ có thể ảnh hưởng đến:

  • cây bóng(shadow tree),
  • chủ thể bóng với các pseudo-class :host:host(),
  • các phần tử được chèn (đến từ light DOM), ::slotted(selector) cho phép chọn các phần tử được chèn, nhưng không phải các phần tử con của chúng.
    Các kiểu tài liệu có thể ảnh hưởng đến:
  • chủ thể bóng (vì nó nằm trong tài liệu bên ngoài)
  • các phần tử được chèn và nội dung của chúng (vì điều đó cũng nằm trong tài liệu bên ngoài)

    Khi các thuộc tính CSS xung đột, thường thì kiểu tài liệu có ưu tiên, trừ khi thuộc tính được gán !important. Khi đó, kiểu cục bộ có ưu tiên.
    Các thuộc tính CSS tùy chỉnh xuyên qua shadow DOM. Chúng được sử dụng như các “hook” để định kiểu thành phần:

  • Thành phần sử dụng một thuộc tính CSS tùy chỉnh để định kiểu các yếu tố chính, chẳng hạn như var(--component-name-title, <default value>).
  • Tác giả thành phần công bố các thuộc tính này cho các nhà phát triển, chúng quan trọng như các phương thức công khai khác của thành phần.
  • Khi một nhà phát triển muốn định kiểu cho tiêu đề, họ gán thuộc tính CSS --component-name-title cho chủ thể bóng hoặc cao hơn.
  • Lợi ích!

    Hy vọng rằng hướng dẫn về phong cách Shadow DOM đã giúp bạn mở rộng kiến thức JavaScript của mình. Tại Cafedev, chúng tôi luôn nỗ lực mang đến những tài liệu và kỹ thuật mới nhất để hỗ trợ bạn trong quá trình phát triển ứng dụng web. Đừng quên áp dụng những kiến thức này vào dự án của bạn để tạo ra các thành phần web độc lập và dễ quản lý. Hãy tiếp tục theo dõi Cafedev để cập nhật thêm nhiều thông tin hữu ích về JavaScript và các công nghệ web khác.

    Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình devCác nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đâyNếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

  • Group Facebook
  • Fanpage
  • Youtube
  • Instagram
  • Twitter
  • Linkedin
  • Pinterest
  • Reddit
  • Tumblr
  • Trang chủ
  • Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!