Nhiều loại thành phần, chẳng hạn như tab, menu, thư viện hình ảnh, cần có nội dung để hiển thị.

Tương tự như cách thẻ <select> tích hợp sẵn trong trình duyệt mong đợi các phần tử <option>, thẻ <custom-tabs> của chúng ta có thể yêu cầu nội dung tab thực tế được truyền vào. Và thẻ <custom-menu> có thể mong đợi các phần tử mục menu.

Mã sử dụng có thể trông như sau:

<custom-menu>
  <title>Candy menu</title>
  <item>Lollipop</item>
  <item>Fruit Toast</item>
  <item>Cup Cake</item>
</custom-menu>

…Sau đó, thành phần của chúng ta nên hiển thị đúng cách, như một menu đẹp với tiêu đề và các mục đã cho, xử lý các sự kiện menu, v.v.
Làm thế nào để thực hiện điều này?

Chúng ta có thể cố gắng phân tích nội dung của phần tử và sao chép, sắp xếp lại các nút DOM một cách động. Điều này có thể thực hiện được, nhưng nếu chúng ta di chuyển các phần tử vào shadow DOM, thì các kiểu CSS từ tài liệu sẽ không áp dụng trong đó, vì vậy kiểu dáng trực quan có thể bị mất. Đồng thời, điều này yêu cầu phải có mã hóa.
May mắn thay, chúng ta không cần phải làm vậy. Shadow DOM hỗ trợ các phần tử , tự động được điền bởi nội dung từ light DOM.

1. Các slot có tên

Hãy xem các slot hoạt động trên một ví dụ đơn giản.
Ở đây, shadow DOM của cung cấp hai slot, được điền từ light DOM:


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

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

Trong shadow DOM, định nghĩa một “điểm chèn”, nơi các phần tử có slot="X" được hiển thị.
Sau đó, trình duyệt thực hiện “composition”: nó lấy các phần tử từ light DOM và hiển thị chúng trong các slot tương ứng của shadow DOM. Cuối cùng, chúng ta có chính xác những gì chúng ta muốn — một thành phần có thể được điền dữ liệu.

Đây là cấu trúc DOM sau khi thực thi mã, không tính đến composition:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username"></slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
</user-card>

Chúng ta đã tạo shadow DOM, vì vậy đây là nó, dưới #shadow-root. Giờ thì phần tử có cả light và shadow DOM.
Để mục đích hiển thị, đối với mỗi trong shadow DOM, trình duyệt tìm slot="..." với cùng tên trong light DOM. Các phần tử này được hiển thị bên trong các slot:

Kết quả được gọi là DOM “flattened”:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        
        <span slot="username">John Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday">
        <span slot="birthday">01.01.2001</span>
      </slot>
    </div>
</user-card>

…Nhưng DOM flattened chỉ tồn tại để hiển thị và xử lý sự kiện. Nó giống như là “ảo”. Đó là cách mọi thứ được hiển thị. Nhưng các nút trong tài liệu thực sự không bị di chuyển!
Điều này có thể dễ dàng kiểm tra nếu chúng ta chạy querySelectorAll: các nút vẫn ở vị trí của chúng.

// light DOM <span> nodes are still at the same place, under `<user-card>`
alert( document.querySelectorAll('user-card span').length ); // 2

Vì vậy, DOM flattened được suy ra từ shadow DOM bằng cách chèn các slot. Trình duyệt hiển thị nó và sử dụng để kế thừa kiểu, truyền sự kiện (nhiều hơn về điều đó sau). Nhưng JavaScript vẫn thấy tài liệu như nó là, trước khi flattening.


Chỉ các phần tử con cấp cao nhất mới có thể có thuộc tính slot="…".

Thuộc tính slot="..." chỉ hợp lệ cho các phần tử con trực tiếp của shadow host (trong ví dụ của chúng ta, phần tử ). Đối với các phần tử lồng ghép, nó sẽ bị bỏ qua.

Ví dụ, thứ hai ở đây <span> bị bỏ qua (vì nó không phải là phần tử con cấp cao của <user-card> ):

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

Nếu có nhiều phần tử trong light DOM với cùng một tên slot, chúng sẽ được thêm vào slot, từng cái một.
Ví dụ, điều này:

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

Sẽ cho ra DOM flattened với hai phần tử trong <slot name="username">:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        <span slot="username">John</span>
        <span slot="username">Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
</user-card>

2. Nội dung dự phòng của slot

Nếu chúng ta đặt một cái gì đó vào bên trong <slot>, nó trở thành nội dung dự phòng, “mặc định”. Trình duyệt sẽ hiển thị nó nếu không có phần tử tương ứng trong light DOM.
Ví dụ, trong đoạn shadow DOM này, Anonymous sẽ được hiển thị nếu không có slot="username" trong light DOM.

<div>Name:
  <slot name="username">Anonymous</slot>
</div>

3. Slot mặc định: đầu tiên không tên

<slot> đầu tiên trong shadow DOM không có tên là một slot “mặc định”. Nó nhận tất cả các nút từ light DOM mà không được gán vào slot khác.
Ví dụ, hãy thêm slot mặc định vào <user-card> của chúng ta để hiển thị tất cả thông tin chưa được gán về người dùng:


<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
    <div>Name:
      <slot name="username"></slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
    <fieldset>
      <legend>Other information</legend>
*!*
      <slot></slot>
*/!*
    </fieldset>
    `;
  }
});
</script>

<user-card>
*!*
  <div>I like to swim.</div>
*/!*
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
*!*
  <div>...And play volleyball too!</div>
*/!*
</user-card>

Tất cả nội dung light DOM chưa được gán sẽ được đưa vào trường thông tin “Other information”.
Các phần tử được thêm vào slot lần lượt, vì vậy cả hai phần thông tin chưa được gán đều nằm trong slot mặc định cùng nhau.

DOM flattened trông như thế này:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        <span slot="username">John Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday">
        <span slot="birthday">01.01.2001</span>
      </slot>
    </div>
    <fieldset>
      <legend>Other information</legend>
*!*
      <slot>
        <div>I like to swim.</div>
        <div>...And play volleyball too!</div>
      </slot>
*/!*
    </fieldset>
</user-card>

4. Ví dụ về menu

Giờ hãy quay lại với <custom-menu>, đã được đề cập ở đầu chương.
Chúng ta có thể sử dụng các slot để phân phối các phần tử.

Đây là mã đánh dấu cho <custom-menu>:

<custom-menu>
  <span slot="title">Candy menu</span>
  <li slot="item">Lollipop</li>
  <li slot="item">Fruit Toast</li>
  <li slot="item">Cup Cake</li>
</custom-menu>

Template shadow DOM với các slot phù hợp:

<template id="tmpl">
  <style> /* menu styles */ </style>
  <div class="menu">
    <slot name="title"></slot>
    <ul><slot name="item"></slot></ul>
  </div>

1. <span slot="title"> sẽ vào <slot name="title">.
2. Có nhiều thẻ <li slot="item"> trong thẻ <custom-menu>, nhưng trong template chỉ có một thẻ <slot name="item">. Vì vậy, tất cả các thẻ <li slot="item"> này sẽ được thêm vào thẻ <slot name="item"> lần lượt, từ đó hình thành danh sách.

DOM flattened trở thành:

<custom-menu>
  #shadow-root
    <style> /* menu styles */ </style>
    <div class="menu">
      <slot name="title">
        <span slot="title">Candy menu</span>
      </slot>
      <ul>
        <slot name="item">
          <li slot="item">Lollipop</li>
          <li slot="item">Fruit Toast</li>
          <li slot="item">Cup Cake</li>
        </slot>
      </ul>
    </div>
</custom-menu>

Có thể nhận thấy rằng, trong một DOM hợp lệ, <li> phải là phần tử con trực tiếp của <ul>

Nhưng đây là DOM flattened, nó mô tả cách thành phần được hiển thị, việc này xảy ra tự nhiên ở đây.
Chúng ta chỉ cần thêm một trình xử lý sự kiện click để mở/đóng danh sách, và <custom-menu> đã sẵn sàng:

customElements.define('custom-menu', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});

    // tmpl is the shadow DOM template (above)
    this.shadowRoot.append( tmpl.content.cloneNode(true) );

    // we can't select light DOM nodes, so let's handle clicks on the slot
    this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
      // open/close the menu
      this.shadowRoot.querySelector('.menu').classList.toggle('closed');
    };
  }
});

Tất nhiên, chúng ta có thể thêm nhiều chức năng hơn: sự kiện, phương thức và v.v..

5. Cập nhật các slot

Nếu mã bên ngoài muốn thêm/xóa các mục menu một cách động?
Trình duyệt theo dõi các slot và cập nhật việc hiển thị nếu các phần tử được gán vào slot bị thêm/xóa.
Hơn nữa, vì các nút light DOM không được sao chép mà chỉ được hiển thị trong các slot, nên các thay đổi bên trong chúng ngay lập tức trở nên rõ ràng.
Vì vậy, chúng ta không cần phải làm gì để cập nhật việc hiển thị. Nhưng nếu mã thành phần muốn biết về các thay đổi slot, thì sự kiện slotchange có sẵn.

Ví dụ, ở đây, mục menu được chèn động sau 1 giây, và tiêu đề thay đổi sau 2 giây:

run untrusted height=80
<custom-menu id="menu">
  <span slot="title">Candy menu</span>
</custom-menu>

<script>
customElements.define('custom-menu', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div class="menu">
      <slot name="title"></slot>
      <ul><slot name="item"></slot></ul>
    </div>`;

    // shadowRoot can't have event handlers, so using the first child
    this.shadowRoot.firstElementChild.addEventListener('slotchange',
      e => alert("slotchange: " + e.target.name)
    );
  }
});

setTimeout(() => {
  menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>')
}, 1000);

setTimeout(() => {
  menu.querySelector('[slot="title"]').innerHTML = "New menu";
}, 2000);
</script>

Việc hiển thị menu được cập nhật mỗi khi không cần sự can thiệp của chúng ta.
Có hai sự kiện slotchange ở đây:

  1. Tại thời điểm khởi tạo:
    slotchange: title được kích hoạt ngay lập tức, khi slot="title" từ light DOM vào slot tương ứng.
  2. Sau 1 giây:

slotchange: item được kích hoạt, khi một <li slot="item">mới được thêm vào.

Xin lưu ý: không có sự kiện slotchange sau 2 giây khi nội dung của slot="title" được thay đổi. Điều này là vì không có sự thay đổi slot. Chúng ta thay đổi nội dung bên trong phần tử được slot, đó là một chuyện khác.

Nếu chúng ta muốn theo dõi các thay đổi nội bộ của light DOM từ JavaScript, điều đó cũng có thể được thực hiện bằng cơ chế tổng quát hơn: MutationObserver.

6. API Slot

Cuối cùng, hãy đề cập đến các phương pháp JavaScript liên quan đến slot.
Như chúng ta đã thấy trước đó, JavaScript nhìn vào DOM “thực”, không thực hiện việc làm phẳng. Nhưng, nếu cây bóng có {mode: 'open'}, thì chúng ta có thể xác định các phần tử được gán cho một slot và, ngược lại, slot bởi phần tử bên trong nó:

  • node.assignedSlot — trả về phần tử <slot>node được gán vào.
  • slot.assignedNodes({flatten: true/false}) — các nút DOM được gán cho slot. Tùy chọn flatten mặc định là false. Nếu được đặt rõ ràng là true, thì nó sẽ tìm kiếm sâu hơn vào DOM đã làm phẳng, trả về các slot lồng ghép trong trường hợp các thành phần lồng ghép và nội dung dự phòng nếu không có nút nào được gán.
  • slot.assignedElements({flatten: true/false}) — các phần tử DOM được gán cho slot (giống như trên, nhưng chỉ các nút phần tử).
    Các phương pháp này hữu ích khi chúng ta không chỉ muốn hiển thị nội dung đã được slot, mà còn theo dõi nó trong JavaScript.
    Ví dụ, nếu thành phần <custom-menu> muốn biết nó đang hiển thị gì, thì nó có thể theo dõi slotchange và lấy các mục từ slot.assignedElements:
run untrusted height=120
<custom-menu id="menu">
  <span slot="title">Candy menu</span>
  <li slot="item">Lollipop</li>
  <li slot="item">Fruit Toast</li>
</custom-menu>

<script>
customElements.define('custom-menu', class extends HTMLElement {
  items = []

  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div class="menu">
      <slot name="title"></slot>
      <ul><slot name="item"></slot></ul>
    </div>`;

    // triggers when slot content changes
*!*
    this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
      let slot = e.target;
      if (slot.name == 'item') {
        this.items = slot.assignedElements().map(elem => elem.textContent);
        alert("Items: " + this.items);
      }
    });
*/!*
  }
});

// items update after 1 second
setTimeout(() => {
  menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Cup Cake</li>')
}, 1000);
</script>

7. Tóm tắt

Thông thường, nếu một phần tử có shadow DOM, thì light DOM của nó không được hiển thị. Các slot cho phép hiển thị các phần tử từ light DOM ở các vị trí chỉ định trong shadow DOM.
Có hai loại slot:

  • Slot được đặt tên: <slot name="X">...</slot> — nhận các con cái light với slot="X".
  • Slot mặc định: <slot>đầu tiên không có tên (các slot không có tên sau đó bị bỏ qua) — nhận các con cái light không được slot.
  • Nếu có nhiều phần tử cho cùng một slot — chúng sẽ được thêm vào liên tiếp.
  • Nội dung của phần tử <slot> được sử dụng làm nội dung dự phòng. Nó sẽ được hiển thị nếu không có con cái light cho slot.
    Quy trình hiển thị các phần tử đã được slot bên trong các slot của chúng được gọi là “composition”. Kết quả được gọi là “flattened DOM”.
    Composition thực ra không di chuyển các nút, từ góc độ JavaScript, DOM vẫn giữ nguyên.

JavaScript có thể truy cập các slot bằng các phương pháp:

Dưới đây là đoạn mở bài cho chủ đề:

  • slot.assignedNodes/Elements() — trả về các nút/phần tử bên trong slot.
  • node.assignedSlot — thuộc tính ngược lại, trả về slot bởi một nút.
    Nếu chúng ta muốn biết những gì đang được hiển thị, chúng ta có thể theo dõi nội dung slot bằng cách:
  • Sự kiện slotchange — kích hoạt lần đầu tiên khi một slot được lấp đầy, và trong bất kỳ thao tác thêm/xóa/thay thế nào của phần tử đã được slot, nhưng không phải của các con cái của nó. Slot là event.target.
  • MutationObserver để đi sâu vào nội dung của slot, theo dõi các thay đổi bên trong nó.
    Bây giờ, khi chúng ta đã biết cách hiển thị các phần tử từ light DOM trong shadow DOM, hãy xem cách định kiểu chúng một cách chính xác. Quy tắc cơ bản là các phần tử shadow được định kiểu bên trong, và các phần tử light — bên ngoài, nhưng có những ngoại lệ đáng lưu ý.
    Chúng ta sẽ xem chi tiết trong chương tiếp theo.

Chào mừng bạn đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá cách sử dụng các slot trong Shadow DOM và quy trình composition để quản lý giao diện web một cách hiệu quả. Từ những khái niệm cơ bản đến các kỹ thuật nâng cao, Cafedev sẽ hướng dẫn bạn từng bước để nắm vững các tính năng mạnh mẽ này và áp dụng chúng vào dự án của bạn. Hãy cùng bắt đầu hành trình học JavaScript từ cơ bản đến nâng cao với chúng tôi!

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 dev

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nế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:

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!