无法访问 Web 组件的 setter 方法
Failing to access setter method of a web-component
我构建了一个 Web 组件,并在其构造函数中使用模板来初始化其子结构。此子结构的一部分是另一个 Web 组件,我希望调用其 setter 方法。我通过查询通过模板创建的 DOM 树来获取子组件,但是通过这个只能访问标准 Element
属性。
这是一个有点复杂的问题,可能是我遗漏了一些基本的东西。这似乎与一个 Web 组件通过模板克隆使用另一个 Web 组件这一事实有关。有人建议 问题可能是由于子组件不是 loaded/defined。我不明白这一点,尤其是因为我无法使建议的解决方案起作用。我还假设 运行ning 的任何 JS 引擎浏览器都足够智能,可以解决 import
依赖关系,并且如果其导入未准备好,则不会 运行 代码。我是不是把这个想得太简单了?
确定性失败的简单可重现示例是必不可少的。所以我设法创建了一个简单的副本来演示这个问题。为了保持一致性,我使用了与原始设计相同的多文件结构:
- component_a.js
class CompA extends HTMLElement
{
constructor()
{
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(CompA.template.content.cloneNode(true));
this._value = 0;
this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
}
set value(x)
{
this._value = 2*x;
this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
console.log('Value set on CompA');
}
}
CompA.template = document.createElement("template");
CompA.template.innerHTML = `<div id='top'></div>`;
customElements.define("comp-a", CompA);
export { CompA };
- component_b.js
import { CompA } from "./component_a.js"
class CompB extends HTMLElement
{
constructor()
{
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(CompB.template.content.cloneNode(true));
let s = this.shadowRoot.getElementById('subcomponent');
console.log(s.constructor.name);
console.log(s.matches(':defined'));
s.value = 1;
}
set value(x)
{
this.shadowRoot.getElementById('subcomponent').value = x;
console.log('Value set on CompB');
}
}
CompB.template = document.createElement("template");
CompB.template.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
customElements.define("comp-b", CompB);
export { CompB };
- question.js
import { CompB } from "./component_b.js"
window.onload = (event) =>
{
let x = document.createElement('comp-b');
document.body.append(x);
x.value = 10;
}
- question.html
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>question</title>
<script type='module' src='./question.js'></script>
</head>
<body>
</body>
</html>
预期行为:
在 question.html 加载 <comp-b>
被创建并插入到页面上。它的 setter 方法使用参数 10 调用。在其构造函数中创建 <comp-b>
期间,<comp-a>
通过提供的模板附加。一旦实例化,它就会被变量 s
引用,并且应该调用它的 setter 方法 - 生成 A=20
innerHTML。所以页面应该是:
Component B:
A=20
具有预期的控制台输出:
CompB
true
Value set on CompA
Value set on CompA
Value set on CompB
观察到的行为:
变量 s
确实指向了正确的元素,但是 s.value = 1;
没有调用 CompA
的 setter 而是简单地将值 1 的 属性 分配给元素,从而保持其默认值。页面是:
Component B:
A=0
控制台输出:
HTMLElement
false
Value set on CompB
问题:
有人能解释一下为什么会失败以及如何强制 JS 将整个指定的 CompA
与 s
相关联,而不仅仅是 Element
吗?
请随时提出进一步可能的问题诊断建议?
像这样包装对子组件 API 的任何访问,假设 s
持有对您的 <comp-a>
DOM 节点的引用:
customElements.whenDefined('comp-a').then(() => s.value = 1);
另一个解决方案(这是我在我目前为客户开发的网络组件库中使用的)是给每个组件一个静态的 getter TAG_NAME
像这样:
export class CompA extends HTMLElement {
static get TAG_NAME() { return 'comp-a'; }
}
并从组件文件中删除对 customElements.define
的调用。
然后,创建一个名为 components.js
:
的文件
export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';
const componentsToRegister = {
CompA,
CompB,
}
for (const clazz of Object.values(componentsToRegister)) {
customElements.define(clazz.TAG_NAME, clazz);
}
这允许非常明确地控制哪些组件以什么顺序注册,只需对 componentsToRegister
中的属性进行排序即可。
忘记 import
和 whenDefined
胡言乱语。
您的问题有 2 个根本原因:
template
升级a-sync
当你将它附加到影子DOM时,constructor
代码继续,
一旦 the Event Loop 完成,模板 HTML(现在处于影子DOM)升级。
.getElementById
找到 HTMLUnknownElements
HTMLUnknownElements
是 HTMLElements
,因此您的 constructor.name
表示 HTMLElement
,而您 can do anything you want with them.
它们只是不是升级 Web组件还,
正如您在 matches(":defined")
代码 中找到的那样
而且,是的,唉几乎每个博客都显示 createElement("template")
模式。
你也不需要这个庞然大物:
CompB.template = document.createElement("template");
CompB.template.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
解决方案
让你的constructor
做到:
super()
.attachShadow({mode: "open"})
.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
并且您的 HTML 将是 parsed/upgraded 同步的 (渲染阻塞)
不想使用 innerHTML
?使用 .createElement("div")
构建您的 HTML
.createElement("template")
和<template>
升级为A-sync。
然后理解
一般来说
做不做DOM(我不是说shadowDOM!!) constructor
,该工作应该在 connectedCallback
完成。有些情况下 constructor
中没有 DOM(想想 SSR 和 .createElement("my-component")
PS。我不喜欢这种模式:
export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';
const componentsToRegister = {
CompA,
CompB,
}
for (const clazz of Object.values(componentsToRegister)) {
customElements.define(clazz.TAG_NAME, clazz);
}
您正在创建依赖项。
When 或 Where Web Component 的定义应该无关紧要,就像乐高积木就是乐高积木一样。
“导出”一个 class 被高估了。
customElements.define("my-element", class extends HTMLElement{
})
(几乎)总能完成任务。当您开始为自己的元素使用 BaseClasses 时,请使用 export
。
您不需要(总是)导出 class,您可以 steal someone else Components
<script>
customElements.define( "poker-card",
class extends customElements.get("card-t") {})
</script>
我构建了一个 Web 组件,并在其构造函数中使用模板来初始化其子结构。此子结构的一部分是另一个 Web 组件,我希望调用其 setter 方法。我通过查询通过模板创建的 DOM 树来获取子组件,但是通过这个只能访问标准 Element
属性。
这是一个有点复杂的问题,可能是我遗漏了一些基本的东西。这似乎与一个 Web 组件通过模板克隆使用另一个 Web 组件这一事实有关。有人建议 import
依赖关系,并且如果其导入未准备好,则不会 运行 代码。我是不是把这个想得太简单了?
确定性失败的简单可重现示例是必不可少的。所以我设法创建了一个简单的副本来演示这个问题。为了保持一致性,我使用了与原始设计相同的多文件结构:
- component_a.js
class CompA extends HTMLElement
{
constructor()
{
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(CompA.template.content.cloneNode(true));
this._value = 0;
this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
}
set value(x)
{
this._value = 2*x;
this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
console.log('Value set on CompA');
}
}
CompA.template = document.createElement("template");
CompA.template.innerHTML = `<div id='top'></div>`;
customElements.define("comp-a", CompA);
export { CompA };
- component_b.js
import { CompA } from "./component_a.js"
class CompB extends HTMLElement
{
constructor()
{
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(CompB.template.content.cloneNode(true));
let s = this.shadowRoot.getElementById('subcomponent');
console.log(s.constructor.name);
console.log(s.matches(':defined'));
s.value = 1;
}
set value(x)
{
this.shadowRoot.getElementById('subcomponent').value = x;
console.log('Value set on CompB');
}
}
CompB.template = document.createElement("template");
CompB.template.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
customElements.define("comp-b", CompB);
export { CompB };
- question.js
import { CompB } from "./component_b.js"
window.onload = (event) =>
{
let x = document.createElement('comp-b');
document.body.append(x);
x.value = 10;
}
- question.html
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>question</title>
<script type='module' src='./question.js'></script>
</head>
<body>
</body>
</html>
预期行为:
在 question.html 加载 <comp-b>
被创建并插入到页面上。它的 setter 方法使用参数 10 调用。在其构造函数中创建 <comp-b>
期间,<comp-a>
通过提供的模板附加。一旦实例化,它就会被变量 s
引用,并且应该调用它的 setter 方法 - 生成 A=20
innerHTML。所以页面应该是:
Component B:
A=20
具有预期的控制台输出:
CompB
true
Value set on CompA
Value set on CompA
Value set on CompB
观察到的行为:
变量 s
确实指向了正确的元素,但是 s.value = 1;
没有调用 CompA
的 setter 而是简单地将值 1 的 属性 分配给元素,从而保持其默认值。页面是:
Component B:
A=0
控制台输出:
HTMLElement
false
Value set on CompB
问题:
有人能解释一下为什么会失败以及如何强制 JS 将整个指定的 CompA
与 s
相关联,而不仅仅是 Element
吗?
请随时提出进一步可能的问题诊断建议?
像这样包装对子组件 API 的任何访问,假设 s
持有对您的 <comp-a>
DOM 节点的引用:
customElements.whenDefined('comp-a').then(() => s.value = 1);
另一个解决方案(这是我在我目前为客户开发的网络组件库中使用的)是给每个组件一个静态的 getter TAG_NAME
像这样:
export class CompA extends HTMLElement {
static get TAG_NAME() { return 'comp-a'; }
}
并从组件文件中删除对 customElements.define
的调用。
然后,创建一个名为 components.js
:
export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';
const componentsToRegister = {
CompA,
CompB,
}
for (const clazz of Object.values(componentsToRegister)) {
customElements.define(clazz.TAG_NAME, clazz);
}
这允许非常明确地控制哪些组件以什么顺序注册,只需对 componentsToRegister
中的属性进行排序即可。
忘记 import
和 whenDefined
胡言乱语。
您的问题有 2 个根本原因:
template
升级a-sync
当你将它附加到影子DOM时,constructor
代码继续,
一旦 the Event Loop 完成,模板 HTML(现在处于影子DOM)升级。.getElementById
找到HTMLUnknownElements
HTMLUnknownElements
是HTMLElements
,因此您的constructor.name
表示HTMLElement
,而您 can do anything you want with them.
它们只是不是升级 Web组件还,
正如您在matches(":defined")
代码 中找到的那样
而且,是的,唉几乎每个博客都显示 createElement("template")
模式。
你也不需要这个庞然大物:
CompB.template = document.createElement("template");
CompB.template.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
解决方案
让你的constructor
做到:
super()
.attachShadow({mode: "open"})
.innerHTML = `<div>
<span>Component B:</span>
<comp-a id='subcomponent'></comp-a>
</div>`;
并且您的 HTML 将是 parsed/upgraded 同步的 (渲染阻塞)
不想使用 innerHTML
?使用 .createElement("div")
.createElement("template")
和<template>
升级为A-sync。
然后理解
一般来说
做不做DOM(我不是说shadowDOM!!) constructor
,该工作应该在 connectedCallback
完成。有些情况下 constructor
中没有 DOM(想想 SSR 和 .createElement("my-component")
PS。我不喜欢这种模式:
export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';
const componentsToRegister = {
CompA,
CompB,
}
for (const clazz of Object.values(componentsToRegister)) {
customElements.define(clazz.TAG_NAME, clazz);
}
您正在创建依赖项。
When 或 Where Web Component 的定义应该无关紧要,就像乐高积木就是乐高积木一样。
“导出”一个 class 被高估了。
customElements.define("my-element", class extends HTMLElement{
})
(几乎)总能完成任务。当您开始为自己的元素使用 BaseClasses 时,请使用 export
。
您不需要(总是)导出 class,您可以 steal someone else Components
<script>
customElements.define( "poker-card",
class extends customElements.get("card-t") {})
</script>