1. 4.13 自定义元素
      1. 4.13.1 简介
        1. 4.13.1.1 创建自主自定义元素
        2. 4.13.1.2 创建与表单关联的自定义元素
        3. 4.13.1.3 创建具有默认可访问角色、状态和属性的自定义元素
        4. 4.13.1.4 创建自定义的内置元素
        5. 4.13.1.5 自主自定义元素的缺点
        6. 4.13.1.6 创建后升级元素
        7. 4.13.1.7 公开自定义元素状态
      2. 4.13.2 自定义元素构造函数和反应的必要条件
      3. 4.13.3 核心概念
      4. 4.13.4 CustomElementRegistry 接口
      5. 4.13.5 自定义元素反应
      6. 4.13.6 元素内部
        1. 4.13.6.1 阴影根访问
        2. 4.13.6.2 与表单关联的自定义元素
        3. 4.13.6.3 可访问性语义
        4. 4.13.6.4 自定义状态伪类

4.13 自定义元素

使用_自定义元素

所有当前引擎都支持。

Firefox63+Safari10.1+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

4.13.1 简介

自定义元素 为作者提供了一种构建他们自己的完整功能的 DOM 元素的方法。虽然作者始终可以在其文档中使用非标准元素,并通过脚本或类似方法在之后添加特定于应用程序的行为,但此类元素历史上一直不符合标准,而且功能性不强。通过 定义 自定义元素,作者可以通知解析器如何正确构造元素以及该类元素应该如何对更改做出反应。

自定义元素是“平台合理化”更大努力的一部分,它通过解释现有平台功能(例如 HTML 的元素)来解释更低级别的作者公开可扩展性点(例如自定义元素定义)。尽管目前自定义元素的功能和语义方面存在很多限制,这些限制阻止它们完全解释 HTML 现有元素的行为,但我们希望随着时间的推移缩小这一差距。

4.13.1.1 创建自主自定义元素

为了说明如何创建一个 自主自定义元素,让我们定义一个自定义元素,它封装了渲染一个国家国旗的小图标。我们的目标是能够像这样使用它

<flag-icon country="nl"></flag-icon>

为此,我们首先为自定义元素声明一个类,扩展 HTMLElement

class FlagIcon extends HTMLElement {
  constructor() {
    super();
    this._countryCode = null;
  }

  static observedAttributes = ["country"];

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "country" due to observedAttributes
    this._countryCode = newValue;
    this._updateRendering();
  }
  connectedCallback() {
    this._updateRendering();
  }

  get country() {
    return this._countryCode;
  }
  set country(v) {
    this.setAttribute("country", v);
  }

  _updateRendering() {
    // Left as an exercise for the reader. But, you'll probably want to
    // check this.ownerDocument.defaultView to see if we've been
    // inserted into a document with a browsing context, and avoid
    // doing any work if not.
  }
}

然后,我们需要使用此类来定义元素

customElements.define("flag-icon", FlagIcon);

在这一点上,我们上面的代码将起作用!解析器在看到 flag-icon 标签时,将构造我们 FlagIcon 类的新的实例,并告诉我们的代码关于其新的 country 属性,然后我们使用该属性来设置元素的内部状态并更新其渲染(在适当的时候)。

您还可以使用 DOM API 创建 flag-icon 元素

const flagIcon = document.createElement("flag-icon")
flagIcon.country = "jp"
document.body.appendChild(flagIcon)

最后,我们还可以使用 自定义元素构造函数 本身。也就是说,上面的代码等同于

const flagIcon = new FlagIcon()
flagIcon.country = "jp"
document.body.appendChild(flagIcon)
4.13.1.2 创建与表单关联的自定义元素

添加一个带有 true 值的静态 formAssociated 属性,将 自主自定义元素 变成 与表单关联的自定义元素ElementInternals 接口帮助您实现表单控件元素的常见函数和属性。

class MyCheckbox extends HTMLElement {
  static formAssociated = true;
  static observedAttributes = ['checked'];

  constructor() {
    super();
    this._internals = this.attachInternals();
    this.addEventListener('click', this._onClick.bind(this));
  }

  get form() { return this._internals.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }

  get checked() { return this.hasAttribute('checked'); }
  set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "checked" due to observedAttributes
    this._internals.setFormValue(this.checked ? 'on' : null);
  }

  _onClick(event) {
    this.checked = !this.checked;
  }
}
customElements.define('my-checkbox', MyCheckbox);

您可以将自定义元素 my-checkbox 像内置的与表单关联的元素一样使用。例如,将它放在 formlabel 中,会将 my-checkbox 元素与它们相关联,提交 form 将发送由 my-checkbox 实现提供的数据。

<form action="..." method="...">
  <label><my-checkbox name="agreed"></my-checkbox> I read the agreement.</label>
  <input type="submit">
</form>
4.13.1.3 创建具有默认可访问角色、状态和属性的自定义元素

通过使用 ElementInternals 的适当属性,您的自定义元素可以具有默认的可访问性语义。以下代码扩展了我们上一节中的与表单关联的复选框,以正确设置其默认角色和选中状态,如辅助技术所见

class MyCheckbox extends HTMLElement {
  static formAssociated = true;
  static observedAttributes = ['checked'];

  constructor() {
    super();
    this._internals = this.attachInternals();
    this.addEventListener('click', this._onClick.bind(this));

    this._internals.role = 'checkbox';
    this._internals.ariaChecked = 'false';
  }

  get form() { return this._internals.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }

  get checked() { return this.hasAttribute('checked'); }
  set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "checked" due to observedAttributes
    this._internals.setFormValue(this.checked ? 'on' : null);
    this._internals.ariaChecked = this.checked;
  }

  _onClick(event) {
    this.checked = !this.checked;
  }
}
customElements.define('my-checkbox', MyCheckbox);

请注意,与内置元素一样,这些只是默认值,可以由页面作者使用 rolearia-* 属性覆盖

<!-- This markup is non-conforming -->
<input type="checkbox" checked role="button" aria-checked="false">
<!-- This markup is probably not what the custom element author intended -->
<my-checkbox role="button" checked aria-checked="false">

鼓励自定义元素作者说明其可访问性语义的哪些方面是强原生语义,即不应由自定义元素的用户覆盖。在我们的示例中,my-checkbox 元素的作者会声明其 rolearia-checked 值是强原生语义,因此不鼓励使用类似上面的代码。

4.13.1.4 创建自定义的内置元素

自定义内置元素 是一种与众不同的 自定义元素,其定义方式略有不同,使用方式也与 自主自定义元素 相比有很大不同。它们的存在是为了允许重用 HTML 现有元素的行为,方法是将这些元素扩展为具有新的自定义功能。这很重要,因为 HTML 元素的许多现有行为不幸的是无法通过纯粹使用 自主自定义元素 来复制。相反,自定义内置元素 允许在现有元素上安装自定义构造行为、生命周期钩子以及原型链,本质上是在现有元素之上“混合”这些功能。

自定义内置元素 需要与 自主自定义元素 不同的语法,因为用户代理和其他软件依赖元素的本地名称来识别元素的语义和行为。也就是说,自定义内置元素 在现有行为之上构建的概念,关键在于扩展的元素保留其原始本地名称。

在此示例中,我们将创建一个名为 plastic-button自定义内置元素,它像普通按钮一样,但在您单击它时会添加花哨的动画效果。我们首先定义一个类,就像之前一样,虽然这次我们扩展了 HTMLButtonElement 而不是 HTMLElement

class PlasticButton extends HTMLButtonElement {
  constructor() {
    super();

    this.addEventListener("click", () => {
      // Draw some fancy animation effects!
    });
  }
}

定义自定义元素时,我们还必须指定 extends 选项

customElements.define("plastic-button", PlasticButton, { extends: "button" });

通常,扩展的元素名称不能仅通过查看它扩展的元素接口来确定,因为许多元素共享相同的接口(例如,qblockquote 都共享 HTMLQuoteElement)。

要从解析的 HTML 源文本构造我们的 自定义内置元素,我们使用 is 属性在 button 元素上

<button is="plastic-button">Click Me!</button>

尝试将 自定义内置元素 作为 自主自定义元素 使用,将不会起作用;也就是说,<plastic-button>Click me?</plastic-button> 仅创建一个没有特殊行为的 HTMLElement

如果您需要以编程方式创建一个自定义内置元素,可以使用以下形式的 createElement()

const plasticButton = document.createElement("button", { is: "plastic-button" });
plasticButton.textContent = "Click me!";

和之前一样,构造函数也可以使用

const plasticButton2 = new PlasticButton();
console.log(plasticButton2.localName);  // will output "button"
console.assert(plasticButton2 instanceof PlasticButton);
console.assert(plasticButton2 instanceof HTMLButtonElement);

请注意,以编程方式创建自定义内置元素时,is 属性将不会出现在 DOM 中,因为它没有显式设置。但是,在序列化时会将其添加到输出

console.assert(!plasticButton.hasAttribute("is"));
console.log(plasticButton.outerHTML); // will output '<button is="plastic-button"></button>'

无论它如何创建,button 的所有特殊方式都适用于此类“塑料按钮”:它们的焦点行为、参与表单提交 的能力、disabled 属性等等。

自定义内置元素 旨在允许扩展具有有用的用户代理提供的行为或 API 的现有 HTML 元素。因此,它们只能扩展此规范中定义的现有 HTML 元素,而不能扩展诸如 bgsoundblinkisindexkeygenmulticolnextidspacer 这些已定义为使用 HTMLUnknownElement 作为其 元素接口 的旧元素。

此要求的原因之一是未来的兼容性:如果定义了一个 自定义内置元素,它扩展了当前未知的元素,例如 combobox,这将阻止此规范在将来定义 combobox 元素,因为派生 自定义内置元素 的使用者将开始依赖其基本元素没有有趣的用户代理提供的行为。

4.13.1.5 自主自定义元素的缺点

如以下所述以及上面提到的,仅仅定义和使用一个名为taco-button的元素并不意味着这种元素代表按钮。也就是说,诸如网络浏览器、搜索引擎或辅助技术之类的工具不会仅仅根据其定义的名称自动将生成的元素视为按钮。

为了向各种用户传达所需的按钮语义,同时仍然使用一个自主自定义元素,需要采用多种技术。

考虑到这些要点,一个全功能的taco-button,它承担了传达按钮语义的责任(包括能够被禁用),可能看起来像这样。

class TacoButton extends HTMLElement {
  static observedAttributes = ["disabled"];

  constructor() {
    super();
    this._internals = this.attachInternals();
    this._internals.role = "button";

    this.addEventListener("keydown", e => {
      if (e.code === "Enter" || e.code === "Space") {
        this.dispatchEvent(new PointerEvent("click", {
          bubbles: true,
          cancelable: true
        }));
      }
    });

    this.addEventListener("click", e => {
      if (this.disabled) {
        e.preventDefault();
        e.stopImmediatePropagation();
      }
    });

    this._observer = new MutationObserver(() => {
      this._internals.ariaLabel = this.textContent;
    });
  }

  connectedCallback() {
    this.setAttribute("tabindex", "0");

    this._observer.observe(this, {
      childList: true,
      characterData: true,
      subtree: true
    });
  }

  disconnectedCallback() {
    this._observer.disconnect();
  }

  get disabled() {
    return this.hasAttribute("disabled");
  }
  set disabled(flag) {
    this.toggleAttribute("disabled", Boolean(flag));
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "disabled" due to observedAttributes
    if (this.disabled) {
      this.removeAttribute("tabindex");
      this._internals.ariaDisabled = "true";
    } else {
      this.setAttribute("tabindex", "0");
      this._internals.ariaDisabled = "false";
    }
  }
}

即使有了这个相当复杂的元素定义,该元素对于使用者来说也不是很方便:它会不断地“萌发”tabindex属性,并且它选择tabindex="0"可获得焦点行为可能与当前平台上的button行为不匹配。这是因为截至目前,还没有办法为自定义元素指定默认的焦点行为,迫使使用tabindex属性来做到这一点(即使它通常用于允许消费者覆盖默认行为)。

相比之下,一个简单的定制内置元素,如上一节所示,将自动继承button元素的语义和行为,无需手动实现这些行为。一般来说,对于任何基于 HTML 中现有元素构建的具有非平凡行为和语义的元素,定制内置元素将更容易开发、维护和使用。

4.13.1.6 在元素创建后升级元素

由于元素定义可以在任何时候发生,因此非自定义元素可以创建,然后在注册适当的定义后成为自定义元素。我们将此过程称为将元素从普通元素升级为自定义元素。

升级允许在可能更希望在相关元素最初创建后(例如,通过解析器)注册自定义元素定义的场景中使用。它们允许对自定义元素中的内容进行渐进式增强。例如,在以下 HTML 文档中,img-viewer的元素定义是异步加载的

<!DOCTYPE html>
<html lang="en">
<title>Image viewer example</title>

<img-viewer filter="Kelvin">
  <img src="images/tree.jpg" alt="A beautiful tree towering over an empty savannah">
</img-viewer>

<script src="js/elements/img-viewer.js" async></script>

这里的img-viewer元素的定义是使用一个标记为async属性的script元素加载的,该元素放置在标记中的<img-viewer>标签之后。当脚本正在加载时,img-viewer元素将被视为未定义的元素,类似于span。一旦脚本加载,它将定义img-viewer元素,并且页面上现有的img-viewer元素将被升级,应用自定义元素的定义(该定义可能包括应用由字符串“Kelvin”标识的图像过滤器,增强图像的视觉外观)。


请注意,升级仅适用于文档树中的元素。(正式地说,是连接的元素。)未插入文档中的元素将保持未升级状态。以下示例说明了这一点

<!DOCTYPE html>
<html lang="en">
<title>Upgrade edge-cases example</title>

<example-element></example-element>

<script>
  "use strict";

  const inDocument = document.querySelector("example-element");
  const outOfDocument = document.createElement("example-element");

  // Before the element definition, both are HTMLElement:
  console.assert(inDocument instanceof HTMLElement);
  console.assert(outOfDocument instanceof HTMLElement);

  class ExampleElement extends HTMLElement {}
  customElements.define("example-element", ExampleElement);

  // After element definition, the in-document element was upgraded:
  console.assert(inDocument instanceof ExampleElement);
  console.assert(!(outOfDocument instanceof ExampleElement));

  document.body.appendChild(outOfDocument);

  // Now that we've moved the element into the document, it too was upgraded:
  console.assert(outOfDocument instanceof ExampleElement);
</script>
4.13.1.7 公开自定义元素状态

用户代理提供的内置元素具有某些状态,这些状态会随着时间的推移而发生变化,具体取决于用户交互和其他因素,并且通过伪类公开给 Web 作者。例如,某些表单控件具有“invalid”状态,该状态通过:invalid伪类公开。

与内置元素类似,自定义元素也可以处于各种状态,并且自定义元素作者希望以类似于内置元素的方式公开这些状态。

这是通过:state()伪类完成的。自定义元素作者可以使用ElementInternalsstates属性来添加和删除此类自定义状态,这些状态随后会作为:state()伪类的参数公开。

以下示例显示了如何使用:state()来样式化自定义复选框元素。假设LabeledCheckbox没有通过内容属性公开其“checked”状态。

<script>
class LabeledCheckbox extends HTMLElement {
  constructor() {
    super();
    this._internals = this.attachInternals();
    this.addEventListener('click', this._onClick.bind(this));

    const shadowRoot = this.attachShadow({mode: 'closed'});
    shadowRoot.innerHTML =
      `<style>
       :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) {
    this.checked = !this.checked;
  }
}

customElements.define('labeled-checkbox', LabeledCheckbox);
</script>

<style>
labeled-checkbox { border: dashed red; }
labeled-checkbox:state(checked) { border: solid; }
</style>

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

自定义伪类甚至可以定位影子部分。上面的示例的扩展展示了这一点

<script>
class QuestionBox extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'closed'});
    shadowRoot.innerHTML =
      `<div><slot>Question</slot></div>
       <labeled-checkbox part='checkbox'>Yes</labeled-checkbox>`;
  }
}
customElements.define('question-box', QuestionBox);
</script>

<style>
question-box::part(checkbox) { color: red; }
question-box::part(checkbox):state(checked) { color: green; }
</style>

<question-box>Continue?</question-box>

4.13.2 自定义元素构造函数和反应的要求

在编写自定义元素构造函数时,作者受以下一致性要求的约束

这些要求中的几个在元素创建期间被直接或间接地检查,未能遵守这些要求将导致无法通过解析器或 DOM API 实例化的自定义元素。即使工作是在构造函数启动的微任务中完成的,情况也是如此,因为在构造之后可能立即发生微任务检查点

在编写自定义元素反应时,作者应避免操作节点树,因为这会导致意外结果。

元素的connectedCallback可以在元素断开连接之前排队,但由于回调队列仍在处理,因此会导致对不再连接的元素进行connectedCallback

class CParent extends HTMLElement {
  connectedCallback() {
    this.firstChild.remove();
  }
}
customElements.define("c-parent", CParent);

class CChild extends HTMLElement {
  connectedCallback() {
    console.log("CChild connectedCallback: isConnected =", this.isConnected);
  }
}
customElements.define("c-child", CChild);

const parent = new CParent(),
      child = new CChild();
parent.append(child);
document.body.append(parent);

// Logs:
// CChild connectedCallback: isConnected = false

4.13.3 核心概念

一个自定义元素是一个自定义的元素。非正式地说,这意味着它的构造函数和原型是由作者定义的,而不是由用户代理定义的。此作者提供的构造函数称为自定义元素构造函数

可以定义两种不同类型的自定义元素

Global_attributes/is

Firefox63+SafariNoChrome67+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?
  1. 一个自主自定义元素,它是在没有extends选项的情况下定义的。这些类型的自定义元素的本地名称与其定义的名称相同。

  2. 一个定制内置元素,它是在有extends选项的情况下定义的。这些类型的自定义元素的本地名称等于其extends选项中传递的值,并且它们的定义的名称用作is属性的值,因此必须是有效的自定义元素名称

自定义元素创建之后,更改is属性的值不会更改元素的行为。

自主自定义元素具有以下元素定义

类别:
流内容.
短语内容.
可感知内容.
对于 与表单关联的自定义元素可列出可标记可提交可重置与表单关联的元素
可以使用此元素的上下文:
在预期使用 短语内容 的地方。
内容模型:
透明.
内容属性:
全局属性,除了 is 属性
form,对于 与表单关联的自定义元素 — 将元素与 form 元素关联
disabled,对于 与表单关联的自定义元素 — 是否禁用表单控件
readonly,对于 与表单关联的自定义元素 — 影响 willValidate,以及自定义元素作者添加的任何行为
name,对于 与表单关联的自定义元素 — 用于 表单提交form.elements API 的元素名称
任何其他没有命名空间的属性(见文字)。
可访问性注意事项:
对于 与表单关联的自定义元素针对作者针对实现者
否则:针对作者针对实现者
DOM 接口:
由元素作者提供(继承自 HTMLElement

自主自定义元素 没有特殊含义:它 代表 其子元素。一个 定制内置元素 继承了它所扩展的元素的语义。

任何与元素功能相关的无命名空间属性,由元素作者确定,都可以指定在 自主自定义元素 上,只要属性名称是 XML 兼容 并且不包含 ASCII 大写字母。例外是 is 属性,它不能指定在 自主自定义元素 上(并且如果存在,它将不起作用)。

定制内置元素 遵循属性的正常要求,基于它们扩展的元素。要添加基于属性的自定义行为,请使用 data-* 属性。


一个 自主自定义元素 如果与 自定义元素定义 关联,该定义的 form-associated 字段设置为 true,则称为 与表单关联的自定义元素

name 属性代表 与表单关联的自定义元素 的名称。 disabled 属性用于使 与表单关联的自定义元素 无法交互并阻止其 提交值 提交。 form 属性用于显式地将 与表单关联的自定义元素 与其 表单所有者 关联。

与表单关联的自定义元素readonly 属性指定元素 禁止约束验证。用户代理不会为该属性提供任何其他行为,但自定义元素作者应该在可能的情况下使用其存在来以某种适当的方式使他们的控件不可编辑,类似于内置表单控件上 readonly 属性的行为。

约束验证:如果 readonly 属性指定在 与表单关联的自定义元素 上,则元素 禁止约束验证

与表单关联的自定义元素重置算法将一个自定义元素回调反应排队 与元素、回调名称“formResetCallback”和一个空参数列表。


有效的自定义元素名称 是满足以下所有要求的字符序列 name

这些要求确保了 有效的自定义元素名称 的一些目标

除了这些限制之外,还允许大量名称,以最大限度地提高像 <math-α><emotion-😍> 这样的用例的灵活性。

4.13.4 CustomElementRegistry 接口

CustomElementRegistry

所有当前引擎都支持。

Firefox63+Safari10.1+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

自定义元素注册表与 Window 对象而不是 Document 对象关联,因为每个 自定义元素构造函数 继承自 HTMLElement 接口,并且每个 Window 对象只有一个 HTMLElement 接口。

window.customElements.define(name, constructor)
定义一个新的 自定义元素,将给定名称映射到给定构造函数作为 自主自定义元素
window.customElements.define(name, constructor, { extends: baseLocalName })
定义一个新的 自定义元素,将给定名称映射到给定构造函数作为 定制内置元素,用于由提供的 baseLocalName 标识的 元素类型。尝试扩展 自定义元素 或未知元素时,将抛出 "NotSupportedError" DOMException
window.customElements.get(name)
检索为给定 名称 定义的 自定义元素构造函数。如果不存在具有给定 名称自定义元素定义,则返回 undefined。
window.customElements.getName(constructor)

CustomElementRegistry/getName

Firefox116+Safari🔰 预览版+Chrome117+
Opera?Edge117+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?
检索为给定 构造函数 定义的 自定义元素 的给定名称。如果不存在具有给定 构造函数自定义元素定义,则返回 null。
window.customElements.whenDefined(name)
返回一个 Promise,当使用给定名称定义 自定义元素 时,该 Promise 将使用 自定义元素 的构造函数来完成。(如果这样的 自定义元素 已经定义,则返回的 Promise 将立即完成。)如果未提供 有效的自定义元素名称,则返回一个使用 "SyntaxError" DOMException 被拒绝的 Promise。
window.customElements.upgrade(root)
尝试升级 root 的所有 包含阴影的包含后代 元素,即使它们没有 连接

元素定义 是将 自定义元素定义 添加到 CustomElementRegistry 的过程。这是通过 define() 方法实现的。

whenDefined() 方法可用于避免在所有适当的 自定义元素 定义 之前执行操作。在此示例中,我们将它与 :defined 伪类结合使用,以隐藏动态加载的文章内容,直到我们确定它使用的所有 自主自定义元素 都已定义。

articleContainer.hidden = true;

fetch(articleURL)
  .then(response => response.text())
  .then(text => {
    articleContainer.innerHTML = text;

    return Promise.all(
      [...articleContainer.querySelectorAll(":not(:defined)")]
        .map(el => customElements.whenDefined(el.localName))
    );
  })
  .then(() => {
    articleContainer.hidden = false;
  });

upgrade() 方法允许根据需要升级元素。通常,元素在变成 连接 时会自动升级,但如果您需要在准备好连接元素之前进行升级,可以使用此方法。

const el = document.createElement("spider-man");

class SpiderMan extends HTMLElement {}
customElements.define("spider-man", SpiderMan);

console.assert(!(el instanceof SpiderMan)); // not yet upgraded

customElements.upgrade(el);
console.assert(el instanceof SpiderMan);    // upgraded!

4.13.5 自定义元素反应

一个 自定义元素 能够通过运行作者代码来响应某些事件。

我们将这些反应统称为 自定义元素反应

调用 自定义元素反应 的方式非常小心,以避免在执行复杂操作时运行作者代码。实际上,它们会被延迟到“在返回用户脚本之前”。这意味着对于大多数目的,它们看起来是同步执行的,但在复杂复合操作(如 克隆范围 操作)的情况下,它们将被延迟到所有相关的用户代理处理步骤完成后,然后作为一个批次一起运行。

保证 自定义元素反应 始终以与其触发操作相同的顺序调用,至少在单个 自定义元素 的本地上下文中是如此。(因为 自定义元素反应 代码可以执行自己的变异,所以无法对多个元素进行全局排序保证。)

4.13.6 元素内部

某些功能旨在供自定义元素作者使用,但不供自定义元素使用者使用。这些功能由 element.attachInternals() 方法提供,该方法返回一个 ElementInternals 实例。ElementInternals 的属性和方法允许控制用户代理提供给所有元素的内部功能。

element.attachInternals()

HTMLElement/attachInternals

所有当前引擎都支持。

Firefox93+Safari16.4+Chrome77+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

返回一个针对 自定义元素 elementElementInternals 对象。如果 element 不是 自定义元素,如果 "internals" 功能在元素定义中被禁用,或者如果在同一个元素上调用两次,则会抛出异常。

4.13.6.1 阴影根访问
internals.shadowRoot

如果 internals目标元素阴影宿主,则返回 ShadowRoot,否则返回 null。

4.13.6.2 与表单关联的自定义元素
internals.setFormValue(value)

internals目标元素状态提交值 都设置为 value

如果 value 为 null,则元素不会参与表单提交。

internals.setFormValue(value, state)

internals目标元素提交值 设置为 value,并将它的 状态 设置为 state

如果 value 为 null,则元素不会参与表单提交。

internals.form

返回 internals目标元素表单所有者

internals.setValidity(flags, message [, anchor ])

标记 internals目标元素 遭受由 flags 参数指示的约束,并将元素的验证消息设置为 message。如果指定了 anchor,用户代理可能会使用它来指示当 表单所有者 以交互方式验证或调用 reportValidity() 时,internals目标元素 约束存在问题。

internals.setValidity({})

标记 internals目标元素 满足其约束

internals.willValidate

如果 internals目标元素 在表单提交时将被验证,则返回 true;否则返回 false。

internals.validity

返回 internals目标元素ValidityState 对象。

internals.validationMessage

返回如果 internals目标元素 要进行有效性检查,将向用户显示的错误消息。

valid = internals.checkValidity()

如果 internals目标元素 没有有效性问题,则返回 true;否则返回 false。在后一种情况下,在元素上触发一个 invalid 事件。

valid = internals.reportValidity()

如果 internals目标元素 没有有效性问题,则返回 true;否则返回 false,在元素上触发一个 invalid 事件,并且(如果事件没有被取消)向用户报告问题。

internals.labels

返回一个 NodeList,其中包含 internals目标元素 所关联的所有 label 元素。

每个 与表单关联的自定义元素 都有一个 提交值。它用于在表单提交时提供一个或多个 条目提交值 的初始值为 null,提交值 可以是 null、字符串、File列表,其中包含 条目

每个 与表单关联的自定义元素 都有一个 状态。它是用户代理可以使用它来恢复元素中用户输入的信息。状态 的初始值为 null,状态 可以是 null、字符串、File列表,其中包含 条目

setFormValue() 方法由自定义元素作者用来设置元素的 提交值状态,从而将这些值传达给用户代理。

当用户代理认为恢复 与表单关联的自定义元素状态 是一个好主意时,例如 导航后 或重新启动用户代理,他们可能会 将一个自定义元素回调反应入队,其中包含该元素、回调名称 "formStateRestoreCallback"、一个包含要恢复的状态的参数列表和 "restore"。

如果用户代理具有表单填充辅助功能,则当该功能被调用时,它可以将自定义元素回调反应入队,并带有一个与表单关联的自定义元素,回调名称为“formStateRestoreCallback”,一个包含由状态值历史记录和一些启发式方法确定的状态值的参数列表,以及“autocomplete”。

通常,状态是由用户指定的信息,而提交值是在规范化或清理之后的值,适用于提交到服务器。以下示例将使这一点具体化

假设我们有一个与表单关联的自定义元素,它要求用户指定日期。用户指定了"3/15/2019",但控件希望将"2019-03-15"提交到服务器。 "3/15/2019"将是元素的状态,而"2019-03-15"将是提交值

假设您开发了一个模拟现有复选框输入类型的自定义元素。其提交值将是其value内容属性的值,或字符串"on"。其状态将是"checked""unchecked""checked/indeterminate""unchecked/indeterminate"之一。

4.13.6.3 可访问性语义
internals.role [ = value ]

设置或检索internals目标元素的默认 ARIA 角色,除非页面作者使用role属性覆盖它,否则将使用此角色。

internals.aria* [ = value ]

设置或检索internals目标元素的各种默认 ARIA 状态或属性值,除非页面作者使用aria-*属性覆盖它们,否则将使用这些值。

通过使用ElementInternalsrolearia*属性,自定义元素作者可以为其自定义元素设置默认的可访问角色、状态和属性值,类似于原生元素的行为。有关更多详细信息,请参阅上面的示例

4.13.6.4 自定义状态伪类
internals.states.add(value)

将字符串value添加到元素的状态集中,以便将其作为伪类公开。

internals.states.has(value)

如果元素的状态集中包含value,则返回 true,否则返回 false。

internals.states.delete(value)

如果元素的状态集包含value,则将其删除并返回 true。否则,将返回 false。

internals.states.clear()

从元素的状态集中删除所有值。

for (const stateName of internals.states)
for (const stateName of internals.states.entries())
for (const stateName of internals.states.keys())
for (const stateName of internals.states.values())

迭代元素的状态集中的所有值。

internals.states.forEach(callback)

通过对每个值调用callback一次,迭代元素的状态集中的所有值。

internals.states.size

返回元素的状态集中的值数量。

状态集可以公开由字符串值的出现/不存在表示的布尔状态。如果作者希望公开具有三个值的 state,则可以将其转换为三个互斥的布尔状态。例如,名为readyState的状态,其值为"loading""interactive""complete",可以映射到三个互斥的布尔状态:"loading""interactive""complete"

// Change the readyState from anything to "complete".
this._readyState = "complete";
this._internals.states.delete("loading");
this._internals.states.delete("interactive");
this._internals.states.add("complete");