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>
本身)位于光 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
)可以被文档覆盖。
设置插槽内容的样式
现在让我们考虑插槽的情况。
插槽元素来自光 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)
伪类。它根据两个条件匹配元素
- 这是一个来自光 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
选择器无法进一步深入到插槽中。以下选择器无效。
::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 DOM 树,
- 使用
:host
和:host()
伪类的 Shadow 宿主, - 插槽元素(来自光 DOM),
::slotted(selector)
允许选择插槽元素本身,但不包括其子元素。
文档样式可以影响
- Shadow 宿主(因为它位于外部文档中)
- 插槽元素及其内容(因为它们也位于外部文档中)
当 CSS 属性冲突时,通常文档样式具有优先级,除非该属性被标记为!important
。然后本地样式具有优先级。
CSS 自定义属性可以穿透 Shadow DOM。它们被用作“钩子”来设置组件的样式。
- 组件使用自定义 CSS 属性来设置关键元素的样式,例如
var(--component-name-title, <default value>)
。 - 组件作者为开发人员发布这些属性,它们与其他公共组件方法一样重要。
- 当开发者想要为标题设置样式时,他们会为影子宿主或其上层元素分配
--component-name-title
CSS 属性。 - 盈利!
评论
<code>
标签,对于多行代码,请将其包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)