htmlcollections — a collection of HTMLCollection implementations
A small toolkit for building WebIDL-conformant
HTMLCollection
variants powered by
@t15i/webidl-decorators,
@t15i/webidl-types, and
@t15i/webspecs.
Heads up! These collections use push semantics — membership is managed by the caller through
insertAfterandremove. The primary intended use case is the Web Components API, whose lifecycle callbacks (connectedCallback,disconnectedCallback) give each element a natural place to register or deregister itself with the owning collection.Once membership is set, attribute-level observation (id and name changes on existing members) is handled for you.
The decorator proposal used is the TC39 stage-3 / 2023-11 variant. Make sure your toolchain supports it.
Install
npm install htmlcollections
Usage
Wire it into a Web Component
The intended pattern is to instantiate a BlinklikeHTMLCollectionData rooted
on the custom element itself, wrap it in a BlinklikeHTMLCollection, and
maintain membership from the element's lifecycle:
import {
BlinklikeHTMLCollection,
BlinklikeHTMLCollectionData,
} from "htmlcollections";
class HTMLCustomListElement extends HTMLElement {
data_ = new BlinklikeHTMLCollectionData(this);
#coll = new BlinklikeHTMLCollection(this.data_);
get items(): HTMLCollection {
return this.#coll;
}
}
class HTMLCustomItemElement extends HTMLElement {
#list: HTMLCustomListElement | null = null
connectedCallback() {
this.#list = getClosestListElement(this)
if (this.#list) {
this.#list.data_.insertAfter(this, getPreviousItemElement(this))
}
}
disconnectedCallback() {
if (this.#list) {
this.#list.data_.remove(this)
}
this.#list = null
}
}
customElements.define("custom-list", HTMLCustomListElement);
customElements.define("custom-item", HTMLCustomItemElement);
const list = document.createElement("custom-list");
list.innerHTML = `<custom-item id="a"></custom-item><custom-item id="b"></custom-item>`;
document.body.append(list);
list.items.length; // 2
list.items.item(0); // <div id="a">
list.items.namedItem("b"); // <div id="b">
[...list.items]; // [<div id="a">, <div id="b">]
Live id/name lookups
Once an element is a member, named access — namedItem(name), coll[name],
and name in coll — follows id and name attribute mutations through a
single MutationObserver rooted on the element you passed to
BlinklikeHTMLCollectionData. Reads synchronously drain pending observer
records, so callers never see stale state:
const el = document.createElement("div");
el.id = "hero";
list.append(el);
list.items.namedItem("hero"); // <div id="hero">
list.items["hero"]; // <div id="hero">
el.id = "champion";
list.items.namedItem("hero"); // null
list.items.namedItem("champion"); // <div id="champion">
"champion" in list.items; // true
Extending
Every part of BlinklikeHTMLCollection is exposed, so you can plug the backing
store and the supported-property views into your own DerivedHTMLCollection class:
import {
Attribute,
Exposed,
Interface,
Internals,
} from "@t15i/webidl-decorators";
import { UnsignedLong } from "@t15i/webidl-types";
import {
BlinklikeHTMLCollection,
BlinklikeHTMLCollectionData,
type BlinklikeHTMLCollectionInternals,
} from "htmlcollections";
interface DerivedHTMLCollectionInternals
extends BlinklikeHTMLCollectionInternals {
// ...
}
@Exposed("Window")
@Interface
class DerivedHTMLCollection extends BlinklikeHTMLCollection {
/** @internal */
declare [Internals]: DerivedHTMLCollectionInternals;
constructor(data: BlinklikeHTMLCollectionData) {
super(data);
// this[Internals] ...
}
@Attribute(UnsignedLong)
override get length(): number {
return this[Internals].data.length;
}
@Attribute(UnsignedLong)
override set length(value: number) {
// ...
}
}
@t15i/webidl-types provides the WebIDL type wrappers (Nullable,
InterfaceType, UnsignedLong, …) you pass to the decorators.
@t15i/webidl-decorators provides the decorator API, and @t15i/webspecs
provides the runtime platform-object semantics behind it.
Every interface must declare where it is exposed: @Exposed("Window") is
applied outside @Interface and is required — an interface that is never
exposed is rejected when @Interface finalizes it. @Interface uses the class
name as the WebIDL identifier by default, or you can pass one explicitly, e.g.
@Interface("HTMLCollection") (as BlinklikeHTMLCollection does).