@studiometa/js-toolkit 4.0.0-alpha.0 → 4.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/utils/index.d.ts +2 -2
- package/package.json +5 -2
- package/skills/studiometa-js-toolkit/SKILL.md +87 -0
- package/skills/studiometa-js-toolkit/references/api/configuration.md +169 -0
- package/skills/studiometa-js-toolkit/references/api/context/createContext.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/context/createGroup.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/context/index.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContext.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContextSync.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideContext.md +61 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideRootContext.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/context/signal.md +85 -0
- package/skills/studiometa-js-toolkit/references/api/context/subscribeContext.md +71 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/children.md +92 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/component.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/index.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/inject.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/on.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/provide.md +77 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/read-write.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/DIAGNOSTICS.md +137 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/EVENTS.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/index.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/reportDiagnostic.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/warn.md +54 -0
- package/skills/studiometa-js-toolkit/references/api/dom/breakpoints.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/dom/domUpdate.md +70 -0
- package/skills/studiometa-js-toolkit/references/api/dom/emitExtendable.md +68 -0
- package/skills/studiometa-js-toolkit/references/api/dom/index.md +26 -0
- package/skills/studiometa-js-toolkit/references/api/dom/swap.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributeNamespace.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributes.md +56 -0
- package/skills/studiometa-js-toolkit/references/api/dom/whenDOMSettled.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-component.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-mount.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-option.md +106 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-ref.md +63 -0
- package/skills/studiometa-js-toolkit/references/api/html/index.md +32 -0
- package/skills/studiometa-js-toolkit/references/api/index.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/instance-events.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/instance-methods.md +267 -0
- package/skills/studiometa-js-toolkit/references/api/instance-properties.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-events.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-lifecycle.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-options.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-services.md +164 -0
- package/skills/studiometa-js-toolkit/references/api/registry/defineManifest.md +57 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromMetaGlob.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromWebpackContext.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstance.md +52 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstances.md +48 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getMountedInstances.md +39 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getUnmountedInstances.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/registry/index.md +49 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponent.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponents.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerManifest.md +72 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/defaultScheduler.md +104 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/index.md +27 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/nextFrame.md +31 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/viewTransition.md +53 -0
- package/skills/studiometa-js-toolkit/references/api/services/createService.md +95 -0
- package/skills/studiometa-js-toolkit/references/api/services/createServiceMixin.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/services/index.md +94 -0
- package/skills/studiometa-js-toolkit/references/api/services/mixins.md +148 -0
- package/skills/studiometa-js-toolkit/references/api/services/perTarget.md +65 -0
- package/skills/studiometa-js-toolkit/references/api/services/toggle.md +80 -0
- package/skills/studiometa-js-toolkit/references/api/services/until.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/services/useBreakpoint.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useDrag.md +119 -0
- package/skills/studiometa-js-toolkit/references/api/services/useInView.md +64 -0
- package/skills/studiometa-js-toolkit/references/api/services/useKey.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMediaQuery.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMutation.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePointer.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePrefersReducedMotion.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useRaf.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/useResize.md +60 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScroll.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScrollProgress.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowScroll.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowSize.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/storage/createStorage.md +126 -0
- package/skills/studiometa-js-toolkit/references/api/storage/index.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/storage/presets.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/storage/providers.md +109 -0
- package/skills/studiometa-js-toolkit/references/api/test/index.md +214 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/attribute-grammar.md +93 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/mutation-engine.md +71 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/philosophy.md +78 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/the-registry.md +110 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/autoloading.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/handling-diagnostics.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/mount-strategies.md +95 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/scheduling-work.md +124 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/sharing-state.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/swapping-content.md +92 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/testing-components.md +137 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/typing-components.md +190 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-decorators.md +168 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-storage.md +125 -0
- package/skills/studiometa-js-toolkit/references/guide/index.md +163 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/installation.md +144 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/lifecycle-hooks.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-components.md +229 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-options.md +235 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-refs.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/using-services.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/working-with-events.md +251 -0
- package/skills/studiometa-js-toolkit/references/guide/migration/v3-to-v4.md +373 -0
- package/skills/studiometa-js-toolkit/references/utils/css.md +98 -0
- package/skills/studiometa-js-toolkit/references/utils/dom.md +54 -0
- package/skills/studiometa-js-toolkit/references/utils/easings.md +217 -0
- package/skills/studiometa-js-toolkit/references/utils/focus.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/history.md +69 -0
- package/skills/studiometa-js-toolkit/references/utils/index.md +53 -0
- package/skills/studiometa-js-toolkit/references/utils/is.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/load.md +59 -0
- package/skills/studiometa-js-toolkit/references/utils/math.md +129 -0
- package/skills/studiometa-js-toolkit/references/utils/motion.md +218 -0
- package/skills/studiometa-js-toolkit/references/utils/objects.md +68 -0
- package/skills/studiometa-js-toolkit/references/utils/scroll.md +128 -0
- package/skills/studiometa-js-toolkit/references/utils/strings.md +176 -0
- package/skills/studiometa-js-toolkit/references/utils/timing.md +111 -0
- package/skills/studiometa-js-toolkit/references/utils/transitions.md +122 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Storage
|
|
2
|
+
|
|
3
|
+
`createStorage()` is a typed, observable key-value store over a `StorageProvider`. **One seam, six adapters.**
|
|
4
|
+
|
|
5
|
+
## The store
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createLocalStorage } from '@studiometa/js-toolkit';
|
|
9
|
+
|
|
10
|
+
interface Prefs {
|
|
11
|
+
theme: 'light' | 'dark';
|
|
12
|
+
seen: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const prefs = createLocalStorage<Prefs>({ prefix: 'app:' });
|
|
16
|
+
|
|
17
|
+
prefs.set('theme', 'dark');
|
|
18
|
+
prefs.get('theme'); // 'light' | 'dark' | undefined
|
|
19
|
+
prefs.get('theme', 'light'); // 'light' | 'dark'
|
|
20
|
+
prefs.has('seen');
|
|
21
|
+
prefs.keys();
|
|
22
|
+
prefs.delete('seen');
|
|
23
|
+
prefs.clear();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`createStorage()` owns everything a consumer thinks of as storage: key namespacing through `prefix`, serialization in both directions, a `Signal` per key created on the first subscription, and the reference-counted wiring that keeps keys in sync. **A provider moves strings only.**
|
|
27
|
+
|
|
28
|
+
## Observing a key
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createLocalStorage } from '@studiometa/js-toolkit';
|
|
32
|
+
|
|
33
|
+
interface Prefs {
|
|
34
|
+
theme: 'light' | 'dark';
|
|
35
|
+
}
|
|
36
|
+
const prefs = createLocalStorage<Prefs>();
|
|
37
|
+
const unsubscribe = prefs.subscribe(
|
|
38
|
+
'theme',
|
|
39
|
+
(value) => {
|
|
40
|
+
document.documentElement.dataset.theme = value ?? 'light';
|
|
41
|
+
},
|
|
42
|
+
{ immediate: true },
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`destroy()` releases every subscription and the shared listeners at once.
|
|
47
|
+
|
|
48
|
+
In a component, hand the unsubscribe back from `mounted()`:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
mounted() {
|
|
52
|
+
return prefs.subscribe('theme', (value) => this.apply(value));
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The six adapters
|
|
57
|
+
|
|
58
|
+
| Adapter | Backed by |
|
|
59
|
+
| --------------------------------------------------------- | ------------------------------------------------------ |
|
|
60
|
+
| `localStorageProvider` | `localStorage` |
|
|
61
|
+
| `sessionStorageProvider` | `sessionStorage` |
|
|
62
|
+
| `memoryStorageProvider` / `createMemoryStorageProvider()` | a `Map` |
|
|
63
|
+
| `createFallbackProvider(...providers)` | reads from the first that holds the key, writes to all |
|
|
64
|
+
| `urlSearchParamsProvider` | `location.search` |
|
|
65
|
+
| `urlSearchParamsInHashProvider` | `location.hash`, read as search params |
|
|
66
|
+
|
|
67
|
+
The two URL adapters rebuild the whole location on each write, so a search write keeps the hash and a hash write keeps the query string. They take one option, `push`, which chooses `history.pushState` over the default `replaceState`.
|
|
68
|
+
|
|
69
|
+
Four presets remove an argument from every call site: `createLocalStorage()`, `createSessionStorage()`, `createUrlSearchParamsStorage()` and `createUrlSearchParamsInHashStorage()`.
|
|
70
|
+
|
|
71
|
+
**A factory exists only where its product has state**
|
|
72
|
+
|
|
73
|
+
`createMemoryStorageProvider()` holds a `Map`. `createFallbackProvider()` and the two URL factories take arguments. There is no `createLocalStorageProvider()` — the instance is enough.
|
|
74
|
+
|
|
75
|
+
## Failures are diagnostics, never throws
|
|
76
|
+
|
|
77
|
+
**A built-in provider reports its own failures and never throws.** A full quota or a refused storage area becomes a `storage.access-failed` diagnostic and the method returns its fallback: `null`, `false`, `[]` or `undefined`. It reports once per operation, not once per area.
|
|
78
|
+
|
|
79
|
+
The store's own failures have their own codes:
|
|
80
|
+
|
|
81
|
+
- `storage.serialize-failed` — nothing is written.
|
|
82
|
+
- `storage.deserialize-failed` — the default is returned.
|
|
83
|
+
|
|
84
|
+
`types.ts` states the same contract for a custom provider.
|
|
85
|
+
|
|
86
|
+
## Syncing with the outside
|
|
87
|
+
|
|
88
|
+
`syncEvents` is a list of window event names and nothing more. A provider declares how a change made outside this instance announces itself:
|
|
89
|
+
|
|
90
|
+
| Provider | `syncEvents` |
|
|
91
|
+
| ----------------- | ------------------------ |
|
|
92
|
+
| web storage | `storage` (another tab) |
|
|
93
|
+
| URL search params | `popstate` |
|
|
94
|
+
| URL hash | `popstate`, `hashchange` |
|
|
95
|
+
|
|
96
|
+
`createStorage()` subscribes **one shared, reference-counted listener per name** while at least one key is observed, and re-reads every observed key when it fires. The event carries no usable state, so the subscriber re-reads.
|
|
97
|
+
|
|
98
|
+
**A known gap**
|
|
99
|
+
|
|
100
|
+
A provider whose changes arrive on a `BroadcastChannel` or through an observer has no way to announce them yet.
|
|
101
|
+
|
|
102
|
+
## A custom backend
|
|
103
|
+
|
|
104
|
+
Six synchronous string methods, plus an optional `syncEvents`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { createStorage, type StorageProvider } from '@studiometa/js-toolkit';
|
|
108
|
+
|
|
109
|
+
const cookieProvider: StorageProvider = {
|
|
110
|
+
get: (key) => new URLSearchParams(document.cookie.replaceAll('; ', '&')).get(key),
|
|
111
|
+
set: (key, value) => {
|
|
112
|
+
document.cookie = `${key}=${value};path=/`;
|
|
113
|
+
},
|
|
114
|
+
remove: (key) => {
|
|
115
|
+
document.cookie = `${key}=;path=/;max-age=0`;
|
|
116
|
+
},
|
|
117
|
+
has: (key) => document.cookie.includes(`${key}=`),
|
|
118
|
+
keys: () => [...new URLSearchParams(document.cookie.replaceAll('; ', '&')).keys()],
|
|
119
|
+
clear: () => {},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const store = createStorage({ provider: cookieProvider });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
One storage instance runs in Node over the memory provider, which is what `test/package-node-consumer.js` exercises.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
`@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.
|
|
4
|
+
|
|
5
|
+
One sentence carries the whole design:
|
|
6
|
+
|
|
7
|
+
> **The registry is the framework. The DOM is the component tree.**
|
|
8
|
+
|
|
9
|
+
An instance exists because its element is in the document **and** its class is registered. Nothing else creates or unmounts an instance.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @studiometa/js-toolkit@next
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Hello world
|
|
18
|
+
|
|
19
|
+
Declare the component in the markup with `data-component`, and expose the elements it needs with `data-ref`:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<div data-component="Hello">
|
|
23
|
+
<button data-ref="btn">Say hello</button>
|
|
24
|
+
</div>
|
|
25
|
+
<script type="module" src="./main.js"></script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Write the class, and register it:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
32
|
+
|
|
33
|
+
class Hello extends Base {
|
|
34
|
+
static config = {
|
|
35
|
+
name: 'Hello',
|
|
36
|
+
refs: ['btn'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
onBtnClick() {
|
|
40
|
+
alert('Hello, world!');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
registerComponent(Hello);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
## The four things to know
|
|
50
|
+
|
|
51
|
+
### 1. Mount and unmount follow the element
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
There is no third state. A component never declares that its work is over.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
59
|
+
|
|
60
|
+
class Once extends Base {
|
|
61
|
+
static config = { name: 'Once' };
|
|
62
|
+
|
|
63
|
+
hasLoaded = false;
|
|
64
|
+
|
|
65
|
+
mounted() {
|
|
66
|
+
if (this.hasLoaded) return;
|
|
67
|
+
// … the work that must run once per element
|
|
68
|
+
this.hasLoaded = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Read more in [Lifecycle](introduction/lifecycle-hooks.md).
|
|
74
|
+
|
|
75
|
+
### 2. Refs are live, and handlers are delegated
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
81
|
+
|
|
82
|
+
class List extends Base {
|
|
83
|
+
static config = {
|
|
84
|
+
name: 'List',
|
|
85
|
+
refs: ['items[]'], // the `[]` is part of the attribute: data-ref="items[]"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
onItemsClick({ index }) {
|
|
89
|
+
console.log(`clicked item ${index}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Read more in [Refs](introduction/managing-refs.md).
|
|
95
|
+
|
|
96
|
+
### 3. Options are a read-only view over the attributes
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<div data-component="Grid" data-option-columns="1" data-option-columns:l="4"></div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
106
|
+
|
|
107
|
+
class Grid extends Base {
|
|
108
|
+
static config = {
|
|
109
|
+
name: 'Grid',
|
|
110
|
+
options: {
|
|
111
|
+
columns: { type: Number, default: 1 },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
mounted() {
|
|
116
|
+
console.log(this.$options.columns); // 1, or 4 from the `l` breakpoint up
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Every option is responsive with no flag to declare, and to change one you write the attribute. Read more in [Options](introduction/managing-options.md).
|
|
122
|
+
|
|
123
|
+
### 4. Children announce themselves, parents listen
|
|
124
|
+
|
|
125
|
+
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`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Base, type DelegatedEvent } from '@studiometa/js-toolkit';
|
|
129
|
+
|
|
130
|
+
class SliderItem extends Base<{ $emits: { select: { index: number } } }> {
|
|
131
|
+
static config = { name: 'SliderItem' };
|
|
132
|
+
|
|
133
|
+
onClick() {
|
|
134
|
+
this.$emit('select', { index: 0 });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class Slider extends Base {
|
|
139
|
+
static config = {
|
|
140
|
+
name: 'Slider',
|
|
141
|
+
components: { SliderItem },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
onSliderItemSelect({ payload }: DelegatedEvent<SliderItem, 'select'>) {
|
|
145
|
+
console.log(payload.index);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`$emit()` dispatches a real, bubbling `CustomEvent`, so a plain listener hears it too.
|
|
151
|
+
|
|
152
|
+
**A handler named by convention is not typed by convention**
|
|
153
|
+
|
|
154
|
+
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](../api/decorators/on.md) does not remove the annotation either — it checks it against a real target.
|
|
155
|
+
|
|
156
|
+
Read more in [Events](introduction/working-with-events.md).
|
|
157
|
+
|
|
158
|
+
## Where to go next
|
|
159
|
+
|
|
160
|
+
- [Installation](introduction/installation.md) — build tools, CDN and the subpath layout.
|
|
161
|
+
- [Components](introduction/managing-components.md) — declaring, registering and nesting components.
|
|
162
|
+
- [Philosophy](concepts/philosophy.md) — why one registry, and what was removed to get it.
|
|
163
|
+
- [Migrating from v3](migration/v3-to-v4.md) — every breaking change, with the replacement for each.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm install @studiometa/js-toolkit@next
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
**The `next` tag is not optional yet**
|
|
8
|
+
|
|
9
|
+
4.0 is still a prerelease, so it publishes to the `next` dist-tag. Plain `npm install @studiometa/js-toolkit` installs 3.x, which is a different framework — the tag is what selects this one. `next` becomes `latest` when 4.0.0 ships.
|
|
10
|
+
|
|
11
|
+
## Importing
|
|
12
|
+
|
|
13
|
+
The root barrel carries everything a page normally needs:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Utilities live on `/utils`:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { clamp, damp } from '@studiometa/js-toolkit/utils';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Test helpers live on `/test`:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { mount, settle } from '@studiometa/js-toolkit/test';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### One subpath per export
|
|
32
|
+
|
|
33
|
+
Every public export also has a subpath of its own — 197 of them. The barrel is convenient; the subpath is precise:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { Base } from '@studiometa/js-toolkit/Base';
|
|
37
|
+
import { useScroll } from '@studiometa/js-toolkit/useScroll';
|
|
38
|
+
import { clamp } from '@studiometa/js-toolkit/utils/clamp';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Which one to reach for depends on how the code is delivered:
|
|
42
|
+
|
|
43
|
+
- **With a bundler**, prefer the barrel. Tree-shaking removes what you do not import, and the barrel keeps the import lines short.
|
|
44
|
+
- **From an ESM CDN, with no build step**, prefer the subpath. There is no bundler to shake the graph, so a barrel import downloads the barrel's whole module graph before the first line of your code runs.
|
|
45
|
+
|
|
46
|
+
The subpath layout is what keeps a dependency contained. `morphdom` is reachable through [`swap()`](../../api/dom/swap.md) only, so a page that never swaps content never downloads it.
|
|
47
|
+
|
|
48
|
+
## No build step
|
|
49
|
+
|
|
50
|
+
v4 needs no compiler. Every decorator is sugar over a function that works without it, so a page loaded from an ESM CDN keeps the whole framework:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<script type="module">
|
|
54
|
+
import { Base } from 'https://esm.sh/@studiometa/js-toolkit@next/Base';
|
|
55
|
+
import { registerComponent } from 'https://esm.sh/@studiometa/js-toolkit@next/registerComponent';
|
|
56
|
+
|
|
57
|
+
class Counter extends Base {
|
|
58
|
+
static config = { name: 'Counter', refs: ['output'] };
|
|
59
|
+
|
|
60
|
+
count = 0;
|
|
61
|
+
|
|
62
|
+
onClick() {
|
|
63
|
+
this.count += 1;
|
|
64
|
+
this.$refs.output.textContent = String(this.count);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerComponent(Counter);
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## With a build step
|
|
73
|
+
|
|
74
|
+
### Vite
|
|
75
|
+
|
|
76
|
+
Nothing to configure for the framework itself. Vite 8 transforms TypeScript with Oxc, which passes decorators through untouched — so if you use the [decorators](../going-further/using-decorators.md), add a transform that compiles them:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// vite.config.js
|
|
80
|
+
import { defineConfig } from 'vite';
|
|
81
|
+
import swc from '@rollup/plugin-swc';
|
|
82
|
+
|
|
83
|
+
export default defineConfig({
|
|
84
|
+
plugins: [
|
|
85
|
+
{
|
|
86
|
+
...swc({ swc: { jsc: { target: 'es2022', transform: { decoratorVersion: '2023-11' } } } }),
|
|
87
|
+
// Compile only the files that contain a decorator.
|
|
88
|
+
enforce: 'pre',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### TypeScript
|
|
95
|
+
|
|
96
|
+
Stage-3 decorators need no flag on TypeScript 5. Turn `experimentalDecorators` **off**:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"compilerOptions": {
|
|
101
|
+
"target": "ES2022",
|
|
102
|
+
"module": "Preserve",
|
|
103
|
+
"moduleResolution": "bundler",
|
|
104
|
+
"experimentalDecorators": false,
|
|
105
|
+
"useDefineForClassFields": true,
|
|
106
|
+
"strict": true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
See [TypeScript](../going-further/typing-components.md) for how to type a component's refs, options and events.
|
|
112
|
+
|
|
113
|
+
## Autoloading
|
|
114
|
+
|
|
115
|
+
A page that declares its components in the markup does not have to import them all up front. Map each `data-component` token to a dynamic import and the registry downloads a chunk when an element needs it:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { registerManifest } from '@studiometa/js-toolkit';
|
|
119
|
+
|
|
120
|
+
registerManifest({
|
|
121
|
+
Accordion: () => import('./components/Accordion.js'),
|
|
122
|
+
Map: { load: () => import('./components/Map.js'), mountStrategy: 'visible' },
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See [Autoloading](../going-further/autoloading.md).
|
|
127
|
+
|
|
128
|
+
## Agent skills
|
|
129
|
+
|
|
130
|
+
The package ships the `studiometa-js-toolkit` [agent skill](https://agentskills.io). It teaches coding agents the v4 API and holds the documentation of the installed version, so an agent does not write v3 code. Link it into your project with the [`skills` CLI](https://github.com/vercel-labs/skills):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx skills experimental_sync
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The command links the skills of your dependencies from `node_modules` into the agent folders (`.agents/skills/`, `.claude/skills/`…). `@studiometa/ui` asks the CLI for this skill too, so a project that only lists `@studiometa/ui` also gets it.
|
|
137
|
+
|
|
138
|
+
Without npm, install the skills from this website. It publishes them at `/.well-known/agent-skills/index.json`, as the [Agent Skills Discovery](https://specification.website/spec/agent-readiness/agent-skills-discovery/) convention defines:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npx skills add https://js-toolkit-v4.studiometa.dev
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
These skills match the version that this website documents, which can differ from the version that your project installs. Prefer `npx skills experimental_sync` in a project that installs the package.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
**`mount` and `unmount` are the whole lifecycle.** There is no third, permanent notion: a component never declares that its work is over, and nothing marks an instance as never mountable again.
|
|
4
|
+
|
|
5
|
+
## The two notions
|
|
6
|
+
|
|
7
|
+
| Notion | What it is | Effect |
|
|
8
|
+
| ---------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| **disconnected** | The element left the document. | The registry calls `$unmount()`. The instance **stays on its element**, and a re-inserted element mounts it again. |
|
|
10
|
+
| **unmount** | The reversible opposite of mount. | Unbinds the cycle's listeners, runs the `mounted()` cleanups, cancels the scheduled tasks, calls `unmounted()`. |
|
|
11
|
+
|
|
12
|
+
A **move** gives one removal record and one addition record: the instance is unmounted and then mounted again, with the same identity, and the state of the cycle starts over. This is the behaviour of `disconnectedCallback` and `connectedCallback` for custom elements.
|
|
13
|
+
|
|
14
|
+
**Unmounting a parent does not unmount its children.** Each element answers for itself.
|
|
15
|
+
|
|
16
|
+
## `mounted()` and `unmounted()`
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
20
|
+
|
|
21
|
+
class Player extends Base {
|
|
22
|
+
static config = { name: 'Player' };
|
|
23
|
+
|
|
24
|
+
mounted() {
|
|
25
|
+
console.log('the element is in the document');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
unmounted() {
|
|
29
|
+
console.log('the element left, or the declaration was withdrawn');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## `mounted()` returns its cleanup
|
|
35
|
+
|
|
36
|
+
`mounted()` can return a function, or an array of functions, sync or async. They run on the next `$unmount()`:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { Base, useScroll } from '@studiometa/js-toolkit';
|
|
40
|
+
|
|
41
|
+
class Header extends Base {
|
|
42
|
+
static config = { name: 'Header' };
|
|
43
|
+
|
|
44
|
+
mounted() {
|
|
45
|
+
// `subscribe()` returns its own unsubscribe — hand it straight back.
|
|
46
|
+
return useScroll().subscribe(({ directionY }) => {
|
|
47
|
+
this.$el.classList.toggle('is-hidden', directionY > 0);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Several cleanups: return an array.
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { Base, useResize, useScroll } from '@studiometa/js-toolkit';
|
|
57
|
+
|
|
58
|
+
class Sticky extends Base {
|
|
59
|
+
static config = { name: 'Sticky' };
|
|
60
|
+
|
|
61
|
+
mounted() {
|
|
62
|
+
return [useScroll().subscribe(() => {}), useResize().subscribe(() => {})];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
An async `mounted()` works the same way:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Base, createContext, type Signal } from '@studiometa/js-toolkit';
|
|
71
|
+
|
|
72
|
+
const CountContext = createContext<Signal<number>>('count');
|
|
73
|
+
|
|
74
|
+
class TodoCount extends Base {
|
|
75
|
+
static config = { name: 'TodoCount' };
|
|
76
|
+
|
|
77
|
+
async mounted() {
|
|
78
|
+
const signal = await this.$inject(CountContext);
|
|
79
|
+
// Released on unmount, even if the await resolved after it.
|
|
80
|
+
return signal.subscribe((count) => {
|
|
81
|
+
this.$el.textContent = String(count);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If an async `mounted()` resolves **after** the unmount, the cleanup runs immediately.
|
|
88
|
+
|
|
89
|
+
`unmounted()` stays available for the cases the returned cleanup does not fit.
|
|
90
|
+
|
|
91
|
+
## What is unmount-scoped, and what is not
|
|
92
|
+
|
|
93
|
+
| Registered in | Scope | Released by |
|
|
94
|
+
| ----------------------------------------- | ---------------- | ---------------------------------- |
|
|
95
|
+
| a `mounted()` return value | the mount cycle | the next `$unmount()` |
|
|
96
|
+
| a pending `$inject()` request | the mount cycle | the next `$unmount()` |
|
|
97
|
+
| `on<X><Event>` handlers, service mixins | the mount cycle | the next `$unmount()` |
|
|
98
|
+
| `$read()` / `$write()` tasks | the mount cycle | the next `$unmount()` cancels them |
|
|
99
|
+
| `$provide()` in a field initializer | the **instance** | nothing — it dies with the element |
|
|
100
|
+
| `$watchChildren()` in a field initializer | the **instance** | nothing — it dies with the element |
|
|
101
|
+
|
|
102
|
+
A component whose declaration is withdrawn therefore keeps providing context until its element goes.
|
|
103
|
+
|
|
104
|
+
## "Do this once per element"
|
|
105
|
+
|
|
106
|
+
Because `$unmount()` leaves the instance on its element, **a plain field survives every move, re-insertion and `swap()` that preserves the element**. "Once per element" is instance state, not a lifecycle decision:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
110
|
+
|
|
111
|
+
class Reveal extends Base {
|
|
112
|
+
static config = { name: 'Reveal' };
|
|
113
|
+
|
|
114
|
+
hasRevealed = false;
|
|
115
|
+
|
|
116
|
+
mounted() {
|
|
117
|
+
if (this.hasRevealed) return;
|
|
118
|
+
this.$el.classList.add('is-revealed');
|
|
119
|
+
this.hasRevealed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
What a field does **not** survive is an element that is genuinely replaced — which is exactly when the work should run again.
|
|
125
|
+
|
|
126
|
+
## Withdrawing a declaration
|
|
127
|
+
|
|
128
|
+
When an element stops declaring a component — the token leaves `data-component`, or a responsive declaration stops matching — the registry unmounts the instance and **drops it from the element**, so declaring the name again builds a new one.
|
|
129
|
+
|
|
130
|
+
That is the registry rearranging its own bookkeeping. The instance only ever sees `$unmount()`.
|
|
131
|
+
|
|
132
|
+
## Announcements
|
|
133
|
+
|
|
134
|
+
Every instance dispatches a framework event on mount and on unmount, carrying itself in the payload:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { EVENTS, type LifecycleEventDetail } from '@studiometa/js-toolkit';
|
|
138
|
+
|
|
139
|
+
document.addEventListener(EVENTS.component.mounted, (event) => {
|
|
140
|
+
const { instance } = (event as CustomEvent<LifecycleEventDetail>).detail;
|
|
141
|
+
console.log(`${instance.$id} mounted`);
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- The **mount** event bubbles from the element, so any ancestor can follow its descendants with no declaration.
|
|
146
|
+
- The **unmount** event dispatches from `document`, because the element can already be detached.
|
|
147
|
+
- An instance that is scheduled but **not mounted** announces nothing.
|
|
148
|
+
|
|
149
|
+
See [`$watchChildren()`](../../api/instance-methods.md#watchchildren) for the component-level API built on this.
|
|
150
|
+
|
|
151
|
+
## Waiting for the DOM to settle
|
|
152
|
+
|
|
153
|
+
`whenDOMSettled()` is the completion boundary for morphing, fetch updates and breakpoint crossings. It drains the pending mutation records, follows the mutation chains of eager lifecycle work, and resolves after the eager mounts and the teardown:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { whenDOMSettled } from '@studiometa/js-toolkit';
|
|
157
|
+
|
|
158
|
+
async function replace(el: Element, html: string) {
|
|
159
|
+
el.innerHTML = html;
|
|
160
|
+
await whenDOMSettled();
|
|
161
|
+
// Every eager component in the new markup has mounted.
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It does **not** wait for visibility, interaction, idle or media conditions, and it does not await the promises returned by `mounted()`. [`swap()`](../../api/dom/swap.md) awaits it for you.
|
|
166
|
+
|
|
167
|
+
## Manual mounting
|
|
168
|
+
|
|
169
|
+
`$mount()` and `$unmount()` are public, and calling them is legitimate — but on a page the registry does it, and doing it yourself is nearly always a sign that a [mount strategy](../going-further/mount-strategies.md) is the answer instead.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
instance.$unmount(); // reversible, the instance stays on its element
|
|
173
|
+
instance.$mount(); // a new cycle, same identity
|
|
174
|
+
```
|