Create InheritedHtmlElements interface and create an ElementOnce interface

This commit is contained in:
2026-04-01 19:47:54 +02:00
parent 39dee782cc
commit 6ef8b6b159
2 changed files with 98 additions and 72 deletions

View File

@@ -18,51 +18,22 @@ export function registerComponent(name: string, options?: ElementDefinitionOptio
};
}
/**
* Abstract class used to create HTML tags inheriting from HTMLElements
* You should not use this class outside of typing !
*
* Please, see the `inheritHtmlElement` factory if you want to use this class.
**/
export abstract class InheritedHtmlElement<
K extends keyof HTMLElementTagNameMap,
> extends HTMLElement {
abstract readonly inheritedTagName: K;
protected node: HTMLElementTagNameMap[K];
connectedCallback(autoAddNode?: boolean) {
this.node = document.createElement(this.inheritedTagName);
const attributes: Attr[] = []; // We need to make a copy to delete while iterating
for (const attr of this.attributes) {
if (attr.name in this.node) {
attributes.push(attr);
}
}
for (const attr of attributes) {
this.removeAttributeNode(attr);
this.node.setAttributeNode(attr);
}
this.node.innerHTML = this.innerHTML;
this.innerHTML = "";
// Automatically add node to DOM if autoAddNode is true or unspecified
if (autoAddNode === undefined || autoAddNode) {
this.appendChild(this.node);
}
}
}
/**
* Safari doesn't support inheriting from HTML tags on web components
* The technique is to:
* create a new web component
* create the desired type inside
* pass all attributes to the child component
* move all attributes to the child component
* store is at as `node` inside the parent
*
* Since we can't use the generic type to instantiate the node, we create a generator function
**/
export interface InheritedHtmlElement<K extends keyof HTMLElementTagNameMap>
extends HTMLElement {
readonly inheritedTagName: K;
node: HTMLElementTagNameMap[K];
}
/**
* Generator function that creates an InheritedHtmlElement compatible class
*
* ```js
* class MyClass extends inheritHtmlElement("select") {
@@ -71,7 +42,38 @@ export abstract class InheritedHtmlElement<
* ```
**/
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
return class InheritedHtmlElementImpl extends InheritedHtmlElement<K> {
return class InheritedHtmlElementImpl
extends HTMLElement
implements InheritedHtmlElement<K>
{
readonly inheritedTagName = tagName;
node: HTMLElementTagNameMap[K];
connectedCallback(autoAddNode?: boolean) {
this.node = document.createElement(this.inheritedTagName);
const attributes: Attr[] = []; // We need to make a copy to delete while iterating
for (const attr of this.attributes) {
if (attr.name in this.node) {
attributes.push(attr);
}
}
// We move compatible attributes to the child element
// This avoids weird inconsistencies between attributes
// when we manipulate the dom in the future
// This is especially important when using attribute based reactivity
for (const attr of attributes) {
this.removeAttributeNode(attr);
this.node.setAttributeNode(attr);
}
this.node.innerHTML = this.innerHTML;
this.innerHTML = "";
// Automatically add node to DOM if autoAddNode is true or unspecified
if (autoAddNode === undefined || autoAddNode) {
this.appendChild(this.node);
}
}
};
}