CustomStateSet

基线 2024

新功能

2024 年 5 月,此功能适用于最新设备和浏览器版本。此功能可能在旧设备或浏览器中无法正常工作。

CustomStateSet 接口的 文档对象模型 存储了 自主自定义元素 的状态列表,并允许将状态添加到集合中或从集合中移除状态。

该接口可用于公开自定义元素的内部状态,从而使它们能够在 CSS 选择器中使用,这些选择器由使用该元素的代码使用。

实例属性

CustomStateSet.size

返回 CustomStateSet 中的值数量。

实例方法

CustomStateSet.add()

向集合添加一个值。

CustomStateSet.clear()

CustomStateSet 对象中删除所有元素。

CustomStateSet.delete()

CustomStateSet 对象中删除一个值。

CustomStateSet.entries()

返回一个新的迭代器,其中包含 CustomStateSet 中每个元素的值,按照插入顺序排列。

CustomStateSet.forEach()

CustomStateSet 对象中的每个值执行提供的函数。

CustomStateSet.has()

返回一个 Boolean ,断言给定值是否存在元素。

CustomStateSet.keys()

它是 CustomStateSet.values() 的别名。

CustomStateSet.values()

返回一个新的迭代器对象,该对象按插入顺序生成 CustomStateSet 对象中每个元素的值。

描述

内置 HTML 元素可以具有不同的状态,例如“启用”和“禁用”、“选中”和“未选中”、“初始”、“加载”和“就绪”。其中一些状态是公开的,可以通过属性/特性进行设置或查询,而另一些状态则是内部状态,不能直接设置。无论外部状态还是内部状态,元素状态通常可以通过使用 CSS 伪类 作为选择器进行选择和样式化。

CustomStateSet 允许开发人员为自主自定义元素(但不包括从内置元素派生的元素)添加和删除状态。然后,这些状态可以像内置元素的伪类一样,用作自定义状态伪类选择器。

设置自定义元素状态

要使 CustomStateSet 可用,自定义元素必须首先调用 HTMLElement.attachInternals() 以附加一个 ElementInternals 对象。然后,CustomStateSetElementInternals.states 返回。请注意,ElementInternals 不能附加到基于内置元素的自定义元素,因此此功能仅适用于自主自定义元素(请参阅 github.com/whatwg/html/issues/5166)。

CustomStateSet 实例是一个 Set 类对象,可以保存有序的状态值集。每个值都是一个自定义标识符。可以将标识符添加到集合中或从集合中删除。如果集合中存在标识符,则该特定状态为 true,而如果该标识符被删除,则该状态为 false

具有多个值的自定义元素状态可以通过多个布尔状态来表示,这些状态中只有一个为 true(存在于 CustomStateSet 中)一次。

这些状态可以在自定义元素内使用,但不能直接在自定义组件外部访问。

与 CSS 的交互

可以使用 :state() 自定义状态伪类 选择处于特定状态的自定义元素。此伪类的格式为 :state(mystatename),其中 mystatename 是在元素中定义的状态。只有当状态为 true(即,如果 CustomStateSet 中存在 mystatename)时,自定义状态伪类才会匹配自定义元素。

例如,以下 CSS 匹配一个 labeled-checkbox 自定义元素,当该元素的 CustomStateSet 包含 checked 状态时,并为该复选框应用一个 solid 边框

css
labeled-checkbox:state(checked) {
  border: solid;
}

CSS 也可以用于 在自定义元素的影子 DOM 内匹配自定义状态,方法是在 :host() 伪类函数中指定 :state()

此外,:state() 伪类可以在 ::part() 伪元素之后使用,以匹配处于特定状态的自定义元素的 影子部分

警告:尚不支持 :state() 的浏览器将使用 CSS <dashed-ident> 来选择自定义状态,这现在已被弃用。有关如何同时支持这两种方法的信息,请参阅下面的 <dashed-ident> 语法兼容 部分。

示例

匹配自定义复选框元素的自定义状态

此示例改编自规范,演示了一个具有内部“选中”状态的自定义复选框元素。这映射到 checked 自定义状态,允许使用 :state(checked) 自定义状态伪类应用样式。

JavaScript

首先,我们定义扩展自 HTMLElement 的类 LabeledCheckbox。在构造函数中,我们调用 super() 方法,添加对单击事件的侦听器,并调用 this.attachInternals() 以附加一个 ElementInternals 对象。

其余大部分“工作”留给 connectedCallback(),该方法在将自定义元素添加到页面时被调用。使用 <style> 元素定义元素的内容,使其为文本 [][x],后跟标签。这里值得注意的是,使用自定义状态伪类选择要显示的文本::host(:state(checked))。在下面的示例之后,我们将详细介绍代码段中发生的事情。

js
class LabeledCheckbox extends HTMLElement {
  constructor() {
    super();
    this._boundOnClick = this._onClick.bind(this);
    this.addEventListener("click", this._boundOnClick);

    // Attach an ElementInternals to get states property
    this._internals = this.attachInternals();
  }

  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = `<style>
  :host {
    display: block;
  }
  :host::before {
    content: "[ ]";
    white-space: pre;
    font-family: monospace;
  }
  :host(:state(checked))::before {
    content: "[x]";
  }
</style>
<slot>Label</slot>
`;
  }

  get checked() {
    return this._internals.states.has("checked");
  }

  set checked(flag) {
    if (flag) {
      this._internals.states.add("checked");
    } else {
      this._internals.states.delete("checked");
    }
  }

  _onClick(event) {
    // Toggle the 'checked' property when the element is clicked
    this.checked = !this.checked;
  }

  static isStateSyntaxSupported() {
    return CSS.supports("selector(:state(checked))");
  }
}

customElements.define("labeled-checkbox", LabeledCheckbox);

// Display a warning to unsupported browsers
document.addEventListener("DOMContentLoaded", () => {
  if (!LabeledCheckbox.isStateSyntaxSupported()) {
    if (!document.getElementById("state-warning")) {
      const warning = document.createElement("div");
      warning.id = "state-warning";
      warning.style.color = "red";
      warning.textContent = "This feature is not supported by your browser.";
      document.body.insertBefore(warning, document.body.firstChild);
    }
  }
});

LabeledCheckbox 类中

  • get checked()set checked() 中,我们使用 ElementInternals.states 获取 CustomStateSet
  • set checked(flag) 方法在设置标志时将 "checked" 标识符添加到 CustomStateSet 中,并在标志为 false 时删除该标识符。
  • get checked() 方法仅检查 checked 属性是否在集合中定义。
  • 单击元素时,属性值会切换。

然后,我们调用由 Window.customElements 返回的对象上的 define() 方法,以注册自定义元素

js
customElements.define("labeled-checkbox", LabeledCheckbox);

HTML

注册自定义元素后,我们可以在 HTML 中使用该元素,如所示

html
<labeled-checkbox>You need to check this</labeled-checkbox>

CSS

最后,我们使用 :state(checked) 自定义状态伪类选择复选框选中时的 CSS。

css
labeled-checkbox {
  border: dashed red;
}
labeled-checkbox:state(checked) {
  border: solid;
}

结果

单击该元素,查看复选框 checked 状态切换时应用的不同边框。

匹配自定义元素影子部分中的自定义状态

此示例改编自规范,演示了自定义状态可用于针对自定义元素的 影子部分 应用样式。影子部分是影子树中有意暴露给使用自定义元素的页面的部分。

该示例创建了一个 <question-box> 自定义元素,它显示一个问题提示和一个标有“是”的复选框。该元素使用 前一个示例 中定义的 <labeled-checkbox> 作为复选框。

JavaScript

首先,我们定义自定义元素类 QuestionBox,该类扩展自 HTMLElement。和往常一样,构造函数首先调用 super() 方法。接下来,我们通过调用 attachShadow() 将影子 DOM 树附加到自定义元素。

js
class QuestionBox extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = `<div><slot>Question</slot></div>
<labeled-checkbox part="checkbox">Yes</labeled-checkbox>
`;
  }
}

使用 innerHTML 设置影子根的内容。这定义了一个 <slot> 元素,其中包含该元素的默认提示文本“问题”。然后,我们定义一个标有默认文本 "Yes"<labeled-checkbox> 自定义元素。该复选框使用 part 属性,作为名为 checkbox 的问题框的影子部分公开。

请注意,<labeled-checkbox> 元素的代码和样式与 前一个示例 完全相同,因此此处不再重复。

接下来,我们调用 define() 方法,该方法是在 Window.customElements 返回的对象上调用的,用于使用名称 question-box 注册自定义元素。

js
customElements.define("question-box", QuestionBox);

HTML

注册自定义元素后,我们可以在 HTML 中使用该元素,如下所示。

html
<!-- Question box with default prompt "Question" -->
<question-box></question-box>

<!-- Question box with custom prompt "Continue?" -->
<question-box>Continue?</question-box>

CSS

第一段 CSS 使用 ::part() 选择器匹配名为 checkbox 的公开阴影部分,默认将其样式设置为 red

css
question-box::part(checkbox) {
  color: red;
}

第二段在 ::part() 后面跟着 :state(),目的是匹配处于 checked 状态的 checkbox 部分。

css
question-box::part(checkbox):state(checked) {
  color: green;
}

结果

单击任一复选框,查看颜色在 checked 状态切换时从 red 变为 green 的效果。

非布尔内部状态

此示例演示了如何处理自定义元素具有多个可能值的内部属性的情况。

在这种情况下,自定义元素具有一个 state 属性,其允许的值为:"loading"、"interactive" 和 "complete"。为了使此功能正常工作,我们将每个值映射到其自定义状态,并创建代码以确保仅设置与内部状态相对应的标识符。您可以在 set state() 方法的实现中看到这一点:我们设置内部状态,将匹配的自定义状态的标识符添加到 CustomStateSet,并删除与所有其他值关联的标识符。

剩余代码的大部分与演示单个布尔状态的示例类似(我们显示了用户在每个状态之间切换时每个状态的不同文本)。

JavaScript

js
class ManyStateElement extends HTMLElement {
  constructor() {
    super();
    this._boundOnClick = this._onClick.bind(this);
    this.addEventListener("click", this._boundOnClick);
    // Attach an ElementInternals to get states property
    this._internals = this.attachInternals();
  }

  connectedCallback() {
    this.state = "loading";

    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = `<style>
  :host {
    display: block;
    font-family: monospace;
  }
  :host::before {
    content: "[ unknown ]";
    white-space: pre;
  }
  :host(:state(loading))::before {
    content: "[ loading ]";
  }
  :host(:state(interactive))::before {
    content: "[ interactive ]";
  }
  :host(:state(complete))::before {
    content: "[ complete ]";
  }
</style>
<slot>Click me</slot>
`;
  }

  get state() {
    return this._state;
  }

  set state(stateName) {
    // Set internal state to passed value
    // Add identifier matching state and delete others
    if (stateName === "loading") {
      this._state = "loading";
      this._internals.states.add("loading");
      this._internals.states.delete("interactive");
      this._internals.states.delete("complete");
    } else if (stateName === "interactive") {
      this._state = "interactive";
      this._internals.states.delete("loading");
      this._internals.states.add("interactive");
      this._internals.states.delete("complete");
    } else if (stateName === "complete") {
      this._state = "complete";
      this._internals.states.delete("loading");
      this._internals.states.delete("interactive");
      this._internals.states.add("complete");
    }
  }

  _onClick(event) {
    // Cycle the state when element clicked
    if (this.state === "loading") {
      this.state = "interactive";
    } else if (this.state === "interactive") {
      this.state = "complete";
    } else if (this.state === "complete") {
      this.state = "loading";
    }
  }

  static isStateSyntaxSupported() {
    return CSS.supports("selector(:state(loading))");
  }
}

customElements.define("many-state-element", ManyStateElement);

document.addEventListener("DOMContentLoaded", () => {
  if (!LabeledCheckbox.isStateSyntaxSupported()) {
    if (!document.getElementById("state-warning")) {
      const warning = document.createElement("div");
      warning.id = "state-warning";
      warning.style.color = "red";
      warning.textContent = "This feature is not supported by your browser.";
      document.body.insertBefore(warning, document.body.firstChild);
    }
  }
});

HTML

注册新元素后,我们将其添加到 HTML。这与演示单个布尔状态的示例类似,只是我们没有指定值,而是使用插槽中的默认值(<slot>Click me</slot>)。

html
<many-state-element></many-state-element>

CSS

在 CSS 中,我们使用三个自定义状态伪类来选择每个内部状态值的 CSS::state(loading):state(interactive):state(complete)。请注意,自定义元素代码确保一次只能定义这些自定义状态之一。

css
many-state-element:state(loading) {
  border: dotted grey;
}
many-state-element:state(interactive) {
  border: dashed blue;
}
many-state-element:state(complete) {
  border: solid green;
}

结果

单击元素以查看状态更改时应用的不同边框。

<dashed-ident> 语法兼容

以前,具有自定义状态的自定义元素使用 <dashed-ident> 而不是 :state() 函数进行选择。不支持 :state() 的浏览器版本在提供没有双破折号前缀的标识符时会抛出错误。如果需要对这些浏览器的支持,请使用 try...catch 块来支持两种语法,或者使用 <dashed-ident> 作为状态的值,并使用 :--mystate:state(--mystate) CSS 选择器进行选择。

使用 try...catch 块

此代码演示了如何使用 try...catch 尝试添加不使用 <dashed-ident> 的状态标识符,如果抛出错误则回退到 <dashed-ident>

JavaScript

js
class CompatibleStateElement extends HTMLElement {
  constructor() {
    super();
    this._internals = this.attachInternals();
  }

  connectedCallback() {
    // The double dash is required in browsers with the
    // legacy syntax, not supplying it will throw
    try {
      this._internals.states.add("loaded");
    } catch {
      this._internals.states.add("--loaded");
    }
  }
}

CSS

css
compatible-state-element:is(:--loaded, :state(loaded)) {
  border: solid green;
}

使用双破折号前缀的标识符

另一种解决方案是在 JavaScript 中使用 <dashed-ident>。这种方法的缺点是,在使用 CSS :state() 语法时,必须包含破折号。

JavaScript

js
class CompatibleStateElement extends HTMLElement {
  constructor() {
    super();
    this._internals = this.attachInternals();
  }
  connectedCallback() {
    // The double dash is required in browsers with the
    // legacy syntax, but works with the modern syntax
    this._internals.states.add("--loaded");
  }
}

CSS

css
compatible-state-element:is(:--loaded, :state(--loaded)) {
  border: solid green;
}

规范

规范
HTML 标准
# customstateset

浏览器兼容性

BCD 表格仅在启用 JavaScript 的浏览器中加载。

另请参阅