CustomStateSet

Baseline 2024
新推出

自 2024 年 5 月以来,此功能已在最新设备和浏览器版本中可用。此功能可能不适用于较旧的设备或浏览器。

CustomStateSet 接口是 文档对象模型 (DOM) 的一部分,用于存储 自主自定义元素 的状态列表,并允许在集合中添加和移除状态。

该接口可用于暴露自定义元素的内部状态,使其能够被使用该元素的代码在 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(my-state-name),其中 my-state-name 是在元素中定义的状态。仅当状态为 true(即,如果 my-state-name 存在于 CustomStateSet 中)时,自定义状态伪类才会匹配自定义元素。

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

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

CSS 也可以通过在 :host() 伪类函数内指定 :state() 来匹配自定义元素影子 DOM 中的自定义状态

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

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

示例

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

此示例改编自规范,演示了一个具有内部“checked”状态的自定义复选框元素。这被映射到 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
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) 方法会在 flag 设置为 true 时将 "checked" 标识符添加到 CustomStateSet,并在 flag 为 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> 自定义元素,该元素显示一个问题提示以及一个标有“Yes”的复选框。该元素使用上一个示例中定义的 <labeled-checkbox> 作为复选框。

JavaScript

首先,我们定义了扩展自 HTMLElement 的自定义元素类 QuestionBox。一如既往,构造函数首先调用 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> 元素,其中包含元素的默认提示文本“Question”。然后,我们定义一个 <labeled-checkbox> 自定义元素,其默认文本为 "Yes"。该复选框使用 part 属性作为问题框的影子部分暴露,名称为 checkbox

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

接下来,我们在 Window.customElements 返回的对象上调用 define() 方法,以名称 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;
  outline: dashed 1px 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);

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> 作为状态的值,并同时使用 :--my-state:state(--my-state) 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

浏览器兼容性

另见

使用自定义元素