2021 年 5 月 17 日

Shadow DOM 造型

Shadow DOM 可能同時包含 <style><link rel="stylesheet" href="…"> 標籤。在後一種情況下,樣式表會快取在 HTTP 中,因此不會為使用相同範本的多個元件重新下載。

一般來說,區域樣式只會在 shadow tree 內運作,而文件樣式則會在 shadow tree 外運作。但有少數例外。

:host

:host 選擇器允許選擇 shadow host(包含 shadow tree 的元素)。

例如,我們正在製作應該置中的 <custom-dialog> 元素。為此,我們需要設定 <custom-dialog> 元素本身的樣式。

這正是 :host 所做的

<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>
</template>

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

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

串接

影子主機 (<custom-dialog> 本身) 位於 light DOM 中,因此會受到文件 CSS 規則的影響。

如果有一個屬性在 :host 本地和文件中都設定了樣式,則文件樣式優先。

例如,如果在文件中我們有

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

…則 <custom-dialog> 將沒有內距。

這非常方便,因為我們可以在其 :host 規則中設定元件的「預設」樣式,然後在文件中輕鬆覆寫它們。

例外情況是,當一個本地屬性標記為 !important 時,對於此類屬性,本地樣式優先。

:host(selector)

:host 相同,但僅在影子主機與 selector 匹配時套用。

例如,我們希望僅當 <custom-dialog> 具有 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>
</template>

<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>

現在,額外的置中樣式僅套用於第一個對話方塊:<custom-dialog centered>

總之,我們可以使用 :host 家族的選取器來設定元件主元素的樣式。這些樣式(除非 !important)可以被文件覆寫。

設定插槽內容的樣式

現在讓我們考慮插槽的情況。

插槽元素來自 light DOM,因此它們使用文件樣式。本地樣式不會影響插槽內容。

在以下範例中,插槽 <span> 根據文件樣式為粗體,但不會從本地樣式取得 background

<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>

結果是粗體,但不是紅色。

如果我們想要在元件中設定插槽元素的樣式,有兩種選擇。

首先,我們可以設定 <slot> 本身的樣式,並依賴 CSS 繼承

<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>

這裡 <p>John Smith</p> 變成粗體,因為 CSS 繼承在 <slot> 及其內容之間有效。但在 CSS 本身中,並非所有屬性都會繼承。

另一個選項是使用 ::slotted(selector) 偽類別。它根據兩個條件來匹配元素

  1. 這是一個插槽元素,來自 light DOM。插槽名稱無關緊要。只是任何插槽元素,但僅限元素本身,不包括其子元素。
  2. 該元素與 selector 相符。

在我們的範例中,::slotted(div) 精確選取 <div slot="username">,但不會選取其子元素

<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>

請注意,::slotted 選擇器無法進一步深入槽。下列選擇器無效

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

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

此外,::slotted 只能用於 CSS。我們無法在 querySelector 中使用它。

使用自訂屬性的 CSS 掛鉤

我們如何從主文件設定元件的內部元素樣式?

例如 :host 的選擇器會將規則套用至 <custom-dialog> 元素或 <user-card>,但如何設定它們內部的 Shadow DOM 元素樣式?

沒有任何選擇器可以直接從文件影響 Shadow DOM 樣式。但就像我們公開方法以與元件互動一樣,我們可以公開 CSS 變數(自訂 CSS 屬性)來設定其樣式。

自訂 CSS 屬性存在於所有層級,無論是光 DOM 還是 Shadow DOM。

例如,在 Shadow DOM 中,我們可以使用 --user-card-field-color CSS 變數來設定欄位的樣式,而外部文件可以設定其值

<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>

然後,我們可以在外部文件為 <user-card> 宣告此屬性

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

自訂 CSS 屬性會穿透 Shadow DOM,它們在所有地方都可見,因此內部的 .field 規則會使用它。

以下是完整範例

<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>
</template>

<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>

摘要

Shadow DOM 可以包含樣式,例如 <style><link rel="stylesheet">

區域樣式會影響

  • Shadow 樹
  • 使用 :host:host() 偽類別的 Shadow 主機
  • 槽元素(來自光 DOM),::slotted(selector) 允許選擇槽元素本身,但不包含其子元素。

文件樣式會影響

  • Shadow 主機(因為它存在於外部文件)
  • 槽元素及其內容(因為它們也存在於外部文件)

當 CSS 屬性衝突時,通常文件樣式有優先權,除非屬性標示為 !important。此時,區域樣式有優先權。

CSS 自訂屬性會穿透 Shadow DOM。它們用作設定元件樣式的「掛鉤」

  1. 元件使用自訂 CSS 屬性來設定關鍵元素的樣式,例如 var(--component-name-title, <default value>)
  2. 元件作者會為開發人員公開這些屬性,它們與其他公開元件方法一樣重要。
  3. 當開發人員想要設定標題樣式時,他們會為 Shadow 主機或其上層指定 --component-name-title CSS 屬性。
  4. 獲利!
教學課程地圖

留言

在評論之前請先閱讀此內容…
  • 如果您有改善建議 - 請 提交 GitHub 議題 或提交拉取請求,而不是發表評論。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行程式碼 - 將其包裝在 <pre> 標籤中,對於超過 10 行的程式碼 - 使用沙盒 (plnkrjsbincodepen…)