Instance methods
$mount()
$mount(): thisStarts a mount cycle. On a page the registry calls it; calling it yourself is legitimate but nearly always a sign that a mount strategy is the answer instead.
$unmount()
$unmount(): thisThe reversible opposite of mount. It unbinds the cycle's listeners, runs the mounted() cleanups, cancels the instance's scheduled tasks, calls unmounted() and announces the change.
The instance stays on its element, so $mount() can start a new cycle with the same identity.
$emit()
$emit<K extends EmitName<T>>(event: K, ...payload: EmitArgs<T, K>): CustomEvent<EmitDetail<T, K>>Dispatches a bubbling, cancelable CustomEvent on $el, with the payload as detail.
import { Base } from '@studiometa/js-toolkit';
class Dialog extends Base {
static config = { name: 'Dialog' };
close() {
const event = this.$emit('close');
if (event.defaultPrevented) return;
this.$el.removeAttribute('data-option-open');
}
goto(index) {
this.$emit('goto', { index });
}
}- The payload is one object, or nothing. An omitted payload leaves
detailat the platform valuenull. - A value that is not an object is refused by the type and reported as
event.invalid-emit-payload. The event still dispatches. - Nothing in the framework is gated on cancellation.
Declare the names and payloads through the props type's $emits key. See Events.
$on()
$on(type: string, listener: EventListener, options?: AddEventListenerOptions): () => voidAdds a listener on $el and returns its own remover, which makes it a mounted() cleanup as it is:
mounted() {
return this.$on('transitionend', this.handleEnd, { once: true });
}Reach for it when the event name is data rather than a method name, or when you need listener options.
$off()
$off(type: string, listener: EventListener, options?: AddEventListenerOptions): void$query()
$query<T extends Base = Base>(name: string): T[]Every mounted instance of name among the descendants of $el, in document order. A flat array at any depth, not a keyed object.
import { Base } from '@studiometa/js-toolkit';
class Slider extends Base {
static config = { name: 'Slider' };
reset() {
for (const item of this.$query('SliderItem')) {
item.$el.removeAttribute('data-option-active');
}
}
}For a collection kept over time rather than queried each time, prefer $watchChildren().
$closest()
$closest<T extends Base = Base>(name: string): T | nullThe nearest ancestor instance of name, or null.
Always guard it
It is resolved on every access and returns null when no matching ancestor instance has been constructed. Whether an ancestor is resolvable from a child's own mounted() depends on mount order, so never dereference it unguarded there.
this.$closest('Slider')?.goNext();A child that reaches for its parent is usually a child that should $emit() instead.
$watchChildren()
$watchChildren<T extends Base>(name: string, callbacks?: WatchChildrenCallbacks<T>): ChildrenCollection<T>
$watchChildren<T extends BaseConstructor>(ComponentClass: T, callbacks?: WatchChildrenCallbacks<InstanceType<T>>): ChildrenCollection<InstanceType<T>>A live collection of descendant instances, in document order whatever the mount order is.
import { Base } from '@studiometa/js-toolkit';
class SliderItem extends Base {
static config = { name: 'SliderItem' };
}
class Slider extends Base {
static config = { name: 'Slider', components: { SliderItem } };
// Exact `config.name`.
items = this.$watchChildren('SliderItem', {
added(item) {},
removed(item) {},
});
// Also every named subclass, through `instanceof`.
allItems = this.$watchChildren(SliderItem);
}interface ChildrenCollection<T extends Base = Base> extends Iterable<T> {
readonly size: number;
readonly items: T[];
}- The string overload looks up the exact
config.name. - The constructor overload walks the descendant elements in document order, reads their instance maps, keeps the instances where
instance instanceof ComponentClass, excludes the watching instance, and removes duplicates. - Callbacks passed to the decorator form are bound to the instance.
It is instance-scoped, not unmount-scoped
The subscription stays active through unmount and mount cycles, for the whole life of the watching instance, which is why the call belongs in a field initializer. It is never released and dies with the element.
The initial sweep is deferred to a microtask, because the call is usually a field initializer; the announcement listeners attach at once, so nothing is missed. No global instance registry is added — unmounted instances announce from document, so one lazy, realm-shared listener serves every watcher and the document holds nothing but weak references.
$provide()
$provide<V>(key: ContextKey<V>, value: V): VProvides a value to the subtree and returns it, so the call is a field initializer. The value is provided as it is — nothing is wrapped.
class Slider extends Base {
static config = { name: 'Slider' };
api = this.$provide(SliderContext, {
state: signal({ index: 0 }),
goNext: () => {},
});
}Instance-scoped: never released, and it dies with the element. A component whose declaration is withdrawn keeps providing until its element goes.
$inject()
$inject<V>(key: ContextKey<V>): Promise<V>Resolves with the nearest provided value. When nothing provides, it never settles — a missing provider means "not yet", not "no".
async mounted() {
const api = await this.$inject(SliderContext);
return api.state.subscribe((state) => this.render(state));
}The pending request is unmount-scoped: $unmount() cancels it, and a new mount runs mounted() again and asks again.
$injectSync()
$injectSync<V>(key: ContextKey<V>): V | undefinedThe value, synchronously, or undefined. For the case where the answer is optional and the caller has a fallback.
$read() and $write()
$read<T>(fn: () => T): ScheduledTask<T>
$write<T>(fn: () => T): ScheduledTask<T>Schedules a layout read or a DOM write in the frame's matching phase, tied to this instance. $unmount() cancels the pending ones.
import { Base } from '@studiometa/js-toolkit';
class Reveal extends Base {
static config = { name: 'Reveal' };
async mounted() {
const box = await this.$read(() => this.$el.getBoundingClientRect()).promise;
this.$write(() => {
this.$el.style.setProperty('--height', `${box?.height}px`);
});
}
}interface ScheduledTask<T = unknown> {
promise: Promise<T | undefined>;
cancel(): void;
}Every read runs before every write, once, before paint. A read scheduled from a write runs in the next frame; a write scheduled from a read runs in the same frame. See The scheduler.
$warn() and $error()
$warn(code: ToolkitDiagnosticCode, message: string): void
$error(code: ToolkitDiagnosticCode, message: string, error: unknown): voidReport on the diagnostic channel with this component's name filled in.
this.$warn('carousel.no-slides', 'A Carousel with no slide does nothing.');What does not exist
| Not in v4 | Use instead |
|---|---|
$parent | $closest(name) |
$children | $watchChildren(name) or $query(name) |
$root | $closest(name) with the real name |
$update() | nothing — refs are live |
$terminate() | nothing — there is no permanent state |