Skip to content

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

bash
npm install @studiometa/js-toolkit@next

Hello world

Declare the component in the markup with data-component, and expose the elements it needs with data-ref:

html
<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:

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

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

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

html
<div data-component="Grid" data-option-columns="1" data-option-columns:l="4"></div>
js
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:

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

MIT Licensed