原文:JavaScript :Web components之給 Shadow DOM 添加樣式(六)
shadow DOM 可以包含 <style>
和 <link rel="stylesheet" href="…">
標簽。在后一種情況下,樣式表是 HTTP 緩存的,因此不會為使用同一模板的多個組件重新下載樣式表。
一般來說,局部樣式只在 shadow 樹內(nèi)起作用,文檔樣式在 shadow 樹外起作用。但也有少數(shù)例外。
:host
:host
選擇器允許選擇 shadow 宿主(包含 shadow 樹的元素)。
例如,我們正在創(chuàng)建 <custom-dialog>
元素,并且想使它居中。為此,我們需要對 <custom-dialog>
元素本身設置樣式。
這正是 :host
所能做的:
<template id="tmpl">
<style>
/* 這些樣式將從內(nèi)部應用到 custom-dialog 元素上 */
: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>
級聯(lián)
shadow 宿主( <custom-dialog>
本身)駐留在 light DOM 中,因此它受到文檔 CSS 規(guī)則的影響。
如果在局部的 :host
和文檔中都給一個屬性設置樣式,那么文檔樣式優(yōu)先。
例如,如果在文檔中我們有如下樣式:
<style>
custom-dialog {
padding: 0;
}
</style>
……那么 <custom-dialog>
將沒有 padding。
這是非常有利的,因為我們可以在其 :host
規(guī)則中設置 “默認” 組件樣式,然后在文檔中輕松地覆蓋它們。
唯一的例外是當局部屬性被標記 !important
時,對于這樣的屬性,局部樣式優(yōu)先。
:host(selector)
與 :host
相同,但僅在 shadow 宿主與 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>
現(xiàn)在附加的居中樣式只應用于第一個對話框:<custom-dialog centered>
。
:host-context(selector)
與 :host
相同,但僅當外部文檔中的 shadow 宿主或它的任何祖先節(jié)點與 selector
匹配時才應用樣式。
例如,:host-context(.dark-theme)
只有在 <custom-dialog>
或者 <custom-dialog>
的任何祖先節(jié)點上有 dark-theme
類時才匹配:
<body class="dark-theme">
<!-- :host-context(.dark-theme) 只應用于 .dark-theme 內(nèi)部的 custom-dialog -->
<custom-dialog>...</custom-dialog>
</body>
總之,我們可以使用 :host
-family 系列的選擇器來對組件的主元素進行樣式設置,具體取決于上下文。這些樣式(除 !important
外)可以被文檔樣式覆蓋。
給占槽( slotted )內(nèi)容添加樣式
現(xiàn)在讓我們考慮有插槽的情況。
占槽元素來自 light DOM,所以它們使用文檔樣式。局部樣式不會影響占槽內(nèi)容。
在下面的例子中,按照文檔樣式,占槽的 <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>
和它的內(nèi)容之間有效。但是在 CSS 中,并不是所有的屬性都是繼承的。
另一個選項是使用 ::slotted(selector)
偽類。它根據(jù)兩個條件來匹配元素:
- 這是一個占槽元素,來自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
- 該元素與
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
選擇器不能用于任何插槽中更深層的內(nèi)容。下面這些選擇器是無效的:
::slotted(div span) { /* 我們插入的 <div> 不會匹配這個選擇器 */ }
::slotted(div) p { /* 不能進入 light DOM 中選擇元素 */ }
此外,::sloated
只能在 CSS 中使用,不能在 querySelector
中使用。
用自定義 CSS 屬性作為勾子
如何在主文檔中設置組件的內(nèi)建元素的樣式?
像 :host
這樣的選擇器應用規(guī)則到 <custom-dialog>
元素或 <user-card>
,但是如何設置在它們內(nèi)部的 shadow DOM 元素的樣式呢?
沒有選擇器可以從文檔中直接影響 shadow DOM 樣式。但是,正如我們暴露用來與組件交互的方法那樣,我們也可以暴露 CSS 變量(自定義 CSS 屬性)來對其進行樣式設置。
自定義 CSS 屬性存在于所有層次,包括 light DOM 和 shadow DOM。
例如,在 shadow DOM 中,我們可以使用 --user-card-field-color
CSS 變量來設置字段的樣式,而外部文檔可以設置它的值:
<style>
.field {
color: var(--user-card-field-color, black); /* 如果 --user-card-field-color 沒有被聲明過,則取值為 black */
}
</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,它們在任何地方都可見,因此內(nèi)部的 .field
規(guī)則將使用它。
以下是完整的示例:
<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 樹,
- shadow 宿主(通過
:host
-family 系列偽類), - 占槽元素(來自 light DOM),
::slotted(selector)
允許選擇占槽元素本身,但不能選擇它們的子元素。
文檔樣式可以影響:
- shadow 宿主(因為它位于外部文檔中)
- 占槽元素及占槽元素的內(nèi)容(因為它們同樣位于外部文檔中)
當 CSS 屬性沖突時,通常文檔樣式具有優(yōu)先級,除非屬性被標記為 !important
,那么局部樣式優(yōu)先。
CSS 自定義屬性穿透 shadow DOM。它們被用作 “勾子” 來設計組件的樣式:
- 組件使用自定義 CSS 屬性對關鍵元素進行樣式設置,比如
var(--component-name-title, <default value>)
。 - 組件作者為開發(fā)人員發(fā)布這些屬性,它們和其他公共的組件方法一樣重要。
- 當開發(fā)人員想要對一個標題進行樣式設計時,他們會為 shadow 宿主或宿主上層的元素賦值
--component-name-title
CSS 屬性。 - 奧力給!