Getting Started
@studiometa/js-toolkit v4 is a data-attributes driven micro-framework. You write classes, you add data-* attributes to your HTML, and one registry mounts the two together.
One sentence carries the whole design:
The registry is the framework. The DOM is the component tree.
An instance exists because its element is in the document and its class is registered. Nothing else creates or unmounts an instance.
Installation
npm install @studiometa/js-toolkit@nextHello world
Declare the component in the markup with data-component, and expose the elements it needs with data-ref:
<div data-component="Hello">
<button data-ref="btn">Say hello</button>
</div>
<script type="module" src="./main.js"></script>Write the class, and register it:
import { Base, registerComponent } from '@studiometa/js-toolkit';
class Hello extends Base {
static config = {
name: 'Hello',
refs: ['btn'],
};
onBtnClick() {
alert('Hello, world!');
}
}
registerComponent(Hello);That is the whole setup. registerComponent() puts the name in the registry, and every [data-component="Hello"] element in the document gets an instance — including the elements added to the page afterwards, by a template, a fetch or another component.
The four things to know
1. Mount and unmount follow the element
The element enters the document, the instance mounts. The element leaves, the instance unmounts and stays on its element, ready for a re-insertion. A move is one unmount and one mount of the same instance, exactly like disconnectedCallback and connectedCallback on a custom element.
There is no third state. A component never declares that its work is over.
import { Base } from '@studiometa/js-toolkit';
class Once extends Base {
static config = { name: 'Once' };
hasLoaded = false;
mounted() {
if (this.hasLoaded) return;
// … the work that must run once per element
this.hasLoaded = true;
}
}Read more in Lifecycle.
2. Refs are live, and handlers are delegated
Each $refs property reads the DOM on access, so markup that arrives later is found with no refresh and no $update(). on<Ref><Event> handlers are delegated from the root element, so a ref that appears later needs no new binding.
import { Base } from '@studiometa/js-toolkit';
class List extends Base {
static config = {
name: 'List',
refs: ['items[]'], // the `[]` is part of the attribute: data-ref="items[]"
};
onItemsClick({ index }) {
console.log(`clicked item ${index}`);
}
}Read more in Refs.
3. Options are a read-only view over the attributes
An option is an input, never a store. Every property of $options is a getter that derives its value from the element and the viewport on each access.
<div data-component="Grid" data-option-columns="1" data-option-columns:l="4"></div>import { Base } from '@studiometa/js-toolkit';
class Grid extends Base {
static config = {
name: 'Grid',
options: {
columns: { type: Number, default: 1 },
},
};
mounted() {
console.log(this.$options.columns); // 1, or 4 from the `l` breakpoint up
}
}Every option is responsive with no flag to declare, and to change one you write the attribute. Read more in Options.
4. Children announce themselves, parents listen
A child never reaches for its parent. It emits, and the parent hears it through the on<Child><Event> convention, resolved against the names in config.components:
import { Base, type DelegatedEvent } from '@studiometa/js-toolkit';
class SliderItem extends Base<{ $emits: { select: { index: number } } }> {
static config = { name: 'SliderItem' };
onClick() {
this.$emit('select', { index: 0 });
}
}
class Slider extends Base {
static config = {
name: 'Slider',
components: { SliderItem },
};
onSliderItemSelect({ payload }: DelegatedEvent<SliderItem, 'select'>) {
console.log(payload.index);
}
}$emit() dispatches a real, bubbling CustomEvent, so a plain listener hears it too.
A handler named by convention is not typed by convention
The on<Child><Event> name is resolved at runtime, so TypeScript cannot infer the parameter for you. Annotate it with DelegatedEvent, RefEvent or GlobalEvent. The @on decorator does not remove the annotation either — it checks it against a real target.
Read more in Events.
Where to go next
- Installation — build tools, CDN and the subpath layout.
- Components — declaring, registering and nesting components.
- Philosophy — why one registry, and what was removed to get it.
- Migrating from v3 — every breaking change, with the replacement for each.