所以我正在尝试使用 vanilla javascript 构建一个自定义组件,它将根据它拥有的子级数量执行某些操作,这意味着它必须计算所述子级的数量
如果我有以下标记(其中自定义组件称为“我的组件”)
<my-component>
<div></div>
<!-- ...arbitrary number of child elements -->
</my-component>
以及
<head></head>
中的以下JavaScript代码,以确保在解析<body></body>
之前加载它
class MyComponent extends HTMLElement {
constructor(){
super()
this.children.length
//do stuff depending on the number of children
}
//or
connectedCallback () {
this.children.length
//do stuff depending on the numbre of children
}
}
customElements.define("my-component",MyComponent)
尽管元素随后显示在屏幕上,但 this.children.length
在这两种情况下都将返回 0,并且能够检查控制台上的自定义元素并使用 Element.children.length
获取预期的子元素数量。我想这意味着子元素在 constructor
和 connectedCallback
运行时尚不可用。
有什么方法可以在我的元素的类定义中指定一个函数,该函数将在子元素可用时触发,以便我可以对它们进行操作?我希望有一个“childElementsReady”回调或类似的东西,但我猜它不存在。我不知道是否有一个真正明显的方法来处理我所缺少的这个问题,因为这似乎是我应该能够相对轻松地完成的事情
A MutationObserver 是处理这个问题的最佳方法。您可以在
connectedCallback
中设置一个来观察 Light DOM 的变化 - 在这种情况下,仅观察 childList
就足够了:
class MyElement extends HTMLElement {
constructor() {
super();
this.onMutation = this.onMutation.bind(this);
}
connectedCallback() {
// Set up observer
this.observer = new MutationObserver(this.onMutation);
// Watch the Light DOM for child node changes
this.observer.observe(this, {
childList: true
});
}
disconnectedCallback() {
// remove observer if element is no longer connected to DOM
this.observer.disconnect();
}
onMutation(mutations) {
const added = [];
// A `mutation` is passed for each new node
for (const mutation of mutations) {
// Could test for `mutation.type` here, but since we only have
// set up one observer type it will always be `childList`
added.push(...mutation.addedNodes);
}
console.log({
// filter out non element nodes (TextNodes etc.)
added: added.filter(el => el.nodeType === Node.ELEMENT_NODE),
});
}
}
customElements.define('my-element', MyElement);
这里
onMutation
每次将节点添加到 Light DOM 时都会被调用,以便您可以处理此处的任何设置。
请注意,根据 Light DOM 中的节点,当元素连接到 DOM 时,可以多次调用
onMutation
,因此不可能说所有子级在任何时候都已“准备好” - 相反,您可以必须处理每个出现的突变。
我写这个回复是为了回答我自己的问题,因为我发现这是一种在你有影子 dom 时观察添加的孩子的有用方法,所以希望它可以帮助任何人在这种情况下,但 lamplightdev 的答案是最完整的答案无论你是否使用shadow dom,它都有效,所以也请查看他的答案
如果你的自定义元素使用了shadow dom,你可以这样做:
class MyComponent extends HTMLElement {
childAddedCustomCallback () {
let watchedSlot = this
/*"this" here is not the custom element, but a slot that the custom
element will have embedded in its shadow root, to which this function
will be attached as an event listener in the constructor*/
let children = watchedSlot.assignedElements()
let numberOfChildren = children.length
//do stuff depending on the number of children
}
constructor(){
super()
let shadowRoot = this.attachShadow({mode:"open"})
shadowRoot.innerHTML = "<slot></slot>"
let slotToWatch = shadowRoot.querySelector("slot")
slotToWatch.addEventListener("slotchange",this.childAddedCustomCallback)
}
}
customElements.define("my-component",MyComponent)
这样,每次向自定义元素添加子元素时,它都会反映到未命名的插槽,并且插槽的事件侦听器将在发生这种情况时触发回调,为您提供可靠且清晰的方法来访问元素的子元素一旦有货
如果您向自定义元素添加 n 个子元素,例如,这会执行 n 次。如果您有以下标记:
<my-component>
<div></div>
<div></div>
<div></div>
<div></div>
</my-component>
添加最后一个子元素时不会执行一次,而是执行 4 次(每个子元素一次),所以要小心
只是一些其他相关信息。有时,当您克隆或使用
importNode
时,子节点可能已经被插入,并且突变观察器不会捕获它们,在这种情况下,您将必须查看 this.children
来查找当前插入的子节点。
因此,对儿童有期望的自定义元素需要:
- 检查connectedCallback中是否有孩子,做一些事情
- 设置突变观察者并在添加节点时执行操作(这将在升级的情况下发生)。
- 在disconnectedCallback中断开突变观察者
最后这是一个使用 Mutation Observer 的简单示例,只需运行它并查看控制台即可:
<script>
window.customElements.define(
'elmt-container',
class extends HTMLElement {
observeMutations() {
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes) {
if (mutation.addedNodes[0] instanceof HTMLElement) {
console.log('A child node has been added:')
console.log(mutation.addedNodes[0])
}
}
}
}
}
const observer = new MutationObserver(callback)
this.observer = observer
const config = { childList: true, subtree: true }
observer.observe(this, config)
}
disconnectedCallback() {
const observer = this.observer
observer.disconnect()
}
constructor() {
super()
this.observeMutations()
}
}
)
window.customElements.define('elmt-test', class extends HTMLElement {})
</script>
<elmt-container>
<div>1</div>
<elmt-test>2</elmt-test>
<div><span>3</span></div>
</elmt-container>
这个问题的解决办法非常简单。只需等待 HTML 被解析,然后再调用 customElements.register 即可。当必须在 connectCallback 中处理 HTML 子级时,我通常使用的方案:
class MyComponent extends HTMLElement{ static { globalThis.addEventListener('load', ()=>{customElements.define('my-component', MyComponent)}); } }