createGroup
ts
createGroup<T extends GroupMember = GroupMember>(): Group<T>A membership set whose members list is a reactive value.
ts
interface Group<T extends GroupMember = GroupMember> {
readonly members: Signal<readonly T[]>;
join(member: T): () => void;
}A GroupMember is anything with a readonly $el: Element — which every Base instance is.
Usage
The coordinator owns the membership and gives out the ways in:
ts
class DisclosureGroup extends Base {
static config = { name: 'DisclosureGroup' };
#peers = createGroup<Base>();
api = this.$provide(DisclosureGroupContext, {
members: this.#peers.members, // the members to read, in document order
join: (peer: Base) => this.#peers.join(peer), // returns the leave function
open: (peer: Base) => this.open(peer), // the invariant stays here
});
mounted() {
// The membership is a value: re-check the invariant on each change.
return this.#peers.members.subscribe((members) => this.enforce(members));
}
open(peer: Base) {}
enforce(members: readonly Base[]) {}
}A member joins the nearest group whenever that group appears — see subscribeContext().
What it does, and what it does not
join()returns its ownleave, so a member that moves to a nearer group leaves the old one first.- The membership is a value. A coordinator subscribes to it and re-checks its invariant on each change, rather than being called back on a mutation.
- Document order is the tie-breaker, so the markup decides which peer keeps its state.
- Nothing sweeps disconnected members. The teardown of the member removes it.
- It names no group and resolves no scope. Scope comes from nearest-provider-wins, so a nested group takes its own members only.
The helper holds a Set and a Signal, and nothing else. That is the whole implementation.
It replaces withGroup
withGroup is not ported. A group is a provided value now, so its scope is the provider's subtree rather than a name in a global table — and a nested group is a nested provider, with nothing to configure.