Skip to content

Instance methods

$mount()

ts
$mount(): this

Starts 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()

ts
$unmount(): this

The 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()

ts
$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.

js
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 detail at the platform value null.
  • 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()

ts
$on(type: string, listener: EventListener, options?: AddEventListenerOptions): () => void

Adds a listener on $el and returns its own remover, which makes it a mounted() cleanup as it is:

js
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()

ts
$off(type: string, listener: EventListener, options?: AddEventListenerOptions): void

$query()

ts
$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.

js
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()

ts
$closest<T extends Base = Base>(name: string): T | null

The 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.

js
this.$closest('Slider')?.goNext();

A child that reaches for its parent is usually a child that should $emit() instead.

$watchChildren()

ts
$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.

js
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
);
}
ts
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()

ts
$provide<V>(key: ContextKey<V>, value: V): V

Provides 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.

js
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()

ts
$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".

js
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()

ts
$injectSync<V>(key: ContextKey<V>): V | undefined

The value, synchronously, or undefined. For the case where the answer is optional and the caller has a fallback.

$read() and $write()

ts
$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.

js
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`);
}); } }
ts
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()

ts
$warn(code: ToolkitDiagnosticCode, message: string): void
$error(code: ToolkitDiagnosticCode, message: string, error: unknown): void

Report on the diagnostic channel with this component's name filled in.

js
this.$warn('carousel.no-slides', 'A Carousel with no slide does nothing.');

What does not exist

Not in v4Use 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

MIT Licensed