@studiometa/js-toolkit 4.0.0-alpha.1 → 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/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,42 @@
|
|
|
1
|
+
# provideRootContext
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
provideRootContext<T>(key: ContextKey<T>, create: () => T): T
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Provides a value on `document.documentElement`, making the page-wide case the **outermost scope of the same mechanism**.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createContext, injectContextSync, provideRootContext } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
const DataChannels = createContext<Map<string, unknown>>('channels');
|
|
15
|
+
const el = document.body;
|
|
16
|
+
// Scoped or page-wide, resolved the same way, nearest first.
|
|
17
|
+
const channels =
|
|
18
|
+
injectContextSync(el, DataChannels) ?? provideRootContext(DataChannels, () => new Map());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Parameters**
|
|
22
|
+
|
|
23
|
+
- `key` — a [`ContextKey`](createContext.md).
|
|
24
|
+
- `create` — a factory, run **at most once per key**.
|
|
25
|
+
|
|
26
|
+
**Return value**
|
|
27
|
+
|
|
28
|
+
- `T` — the value, created or already there.
|
|
29
|
+
|
|
30
|
+
## Why it is the same mechanism
|
|
31
|
+
|
|
32
|
+
Because the value sits on `document.documentElement`, a request from anywhere reaches it by bubbling, and **a nearer provider still wins** through `stopPropagation`. There is no separate global registry and no second lookup path.
|
|
33
|
+
|
|
34
|
+
## Lazy, and once
|
|
35
|
+
|
|
36
|
+
`create` runs at most once per key, and **nothing is created at import time**. A page that never asks for a key never builds its value.
|
|
37
|
+
|
|
38
|
+
## It cannot be disposed
|
|
39
|
+
|
|
40
|
+
A root provider outlives the instance that asked for it first, because it is page state. That is the point: the first component to need a page-wide channel should not own its lifetime.
|
|
41
|
+
|
|
42
|
+
This is what replaces `withGroup`, which is not ported. For a scoped set of peers, use [`createGroup()`](createGroup.md) with an ordinary [`provideContext()`](provideContext.md).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# signal
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
signal<T>(initialValue: T): Signal<T>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A reactive value. The accessor is `.value`.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface Signal<T = unknown> {
|
|
11
|
+
value: T;
|
|
12
|
+
subscribe(callback: (value: T) => void, options?: { immediate?: boolean }): () => void;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { signal } from '@studiometa/js-toolkit';
|
|
20
|
+
|
|
21
|
+
const count = signal(0);
|
|
22
|
+
|
|
23
|
+
const unsubscribe = count.subscribe((value) => console.log(value), { immediate: true });
|
|
24
|
+
|
|
25
|
+
count.value = 1;
|
|
26
|
+
count.value += 1;
|
|
27
|
+
|
|
28
|
+
unsubscribe();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Parameters**
|
|
32
|
+
|
|
33
|
+
- `initialValue` (`T`).
|
|
34
|
+
|
|
35
|
+
**Return value**
|
|
36
|
+
|
|
37
|
+
- `Signal<T>`. `subscribe()` returns the unsubscribe function.
|
|
38
|
+
|
|
39
|
+
## A write settles synchronously, and the newest value wins
|
|
40
|
+
|
|
41
|
+
The delivery loop re-reads the value after each callback. If the value moved, the loop **abandons the round and starts again on the new value**, so a subscriber that was not reached yet skips the old value entirely.
|
|
42
|
+
|
|
43
|
+
Delivery stays in the same task. There is no batching and no microtask.
|
|
44
|
+
|
|
45
|
+
**A subscriber that writes on every delivery live-locks the loop**
|
|
46
|
+
|
|
47
|
+
Each write restarts the round, so the loop never finishes. Guard the write, or move it out of the subscriber.
|
|
48
|
+
|
|
49
|
+
## It is a factory over a closure
|
|
50
|
+
|
|
51
|
+
There is no class and no proxy. `signal()` closes over the value and a subscriber set, which is why it is the right thing to _provide_: the value crossing a context boundary is the signal itself, and the type of the key says so.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Base, createContext, signal, type Signal } from '@studiometa/js-toolkit';
|
|
55
|
+
|
|
56
|
+
const CountContext = createContext<Signal<number>>('count');
|
|
57
|
+
class Counter extends Base {
|
|
58
|
+
static config = { name: 'Counter' };
|
|
59
|
+
|
|
60
|
+
count = this.$provide(CountContext, signal(0));
|
|
61
|
+
|
|
62
|
+
increment() {
|
|
63
|
+
this.count.value += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class CounterOutput extends Base {
|
|
68
|
+
static config = { name: 'CounterOutput' };
|
|
69
|
+
|
|
70
|
+
async mounted() {
|
|
71
|
+
const count = await this.$inject(CountContext);
|
|
72
|
+
return count.subscribe((value) => {
|
|
73
|
+
this.$el.textContent = String(value);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Failures
|
|
80
|
+
|
|
81
|
+
A subscriber that throws is isolated and reported as `callback.signal-failed`, so one subscriber cannot stop the others.
|
|
82
|
+
|
|
83
|
+
## `toggle()` works on it
|
|
84
|
+
|
|
85
|
+
[`toggle(subscribe)`](../services/toggle.md) takes anything that returns its own unsubscribe function, a `Signal` included.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# subscribeContext
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
subscribeContext<T>(el: Element, key: ContextKey<T>, onProvide: ContextCallback<T>): () => void
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The subscription behaviour of the WICG context protocol: **every** answer, as providers come and go.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Base, createContext, subscribeContext } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
interface GroupApi {
|
|
15
|
+
join(peer: Base): () => void;
|
|
16
|
+
}
|
|
17
|
+
const DisclosureGroupContext = createContext<GroupApi>('disclosure-group');
|
|
18
|
+
class Disclosure extends Base {
|
|
19
|
+
static config = { name: 'Disclosure' };
|
|
20
|
+
|
|
21
|
+
group?: GroupApi;
|
|
22
|
+
|
|
23
|
+
mounted() {
|
|
24
|
+
return subscribeContext(this.$el, DisclosureGroupContext, (group) => {
|
|
25
|
+
this.group = group;
|
|
26
|
+
const leave = group.join(this);
|
|
27
|
+
|
|
28
|
+
// The teardown for *this* value.
|
|
29
|
+
return () => {
|
|
30
|
+
leave();
|
|
31
|
+
this.group = undefined;
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Parameters**
|
|
39
|
+
|
|
40
|
+
- `onProvide` — **required**. It runs synchronously for each answer and receives the value and the same unsubscribe function the helper returns.
|
|
41
|
+
|
|
42
|
+
**Return value**
|
|
43
|
+
|
|
44
|
+
- the unsubscribe function. Call it from `mounted()`'s return value to get an unmount-scoped lifetime.
|
|
45
|
+
|
|
46
|
+
## A new answer replaces; it never accumulates
|
|
47
|
+
|
|
48
|
+
The callback can return a teardown for the value it received. That teardown runs:
|
|
49
|
+
|
|
50
|
+
- before the next **different** value;
|
|
51
|
+
- on unsubscribe.
|
|
52
|
+
|
|
53
|
+
**An identical value is not an answer**, so nothing runs and nothing is torn down.
|
|
54
|
+
|
|
55
|
+
That is what lets a member move between groups correctly: `join()` returns its own `leave`, the teardown calls it, and a member that moves to a nearer group leaves the old one first.
|
|
56
|
+
|
|
57
|
+
## When it fires
|
|
58
|
+
|
|
59
|
+
**The trigger is the mount announcement**, not a broadcast from the provider, and it runs after `mounted()`.
|
|
60
|
+
|
|
61
|
+
The optional `context-subscription` module keeps **one** listener on the document, attached on the first subscription and never at import time.
|
|
62
|
+
|
|
63
|
+
Two `contains()` calls bound the cost per mount: the new provider must contain the consumer, and it must sit inside the provider that answers it now. **A mount that changes nothing checks nothing.**
|
|
64
|
+
|
|
65
|
+
## What it holds
|
|
66
|
+
|
|
67
|
+
**The registry holds nothing.** A subscription is anchored on its consumer element through a `WeakMap`, and the iterable index holds `WeakRef`s that the sweep prunes.
|
|
68
|
+
|
|
69
|
+
Callback and teardown failures are isolated — `callback.context-subscription-failed` and `callback.context-teardown-failed` — so one consumer cannot stop the shared sweep.
|
|
70
|
+
|
|
71
|
+
`context.ts` and the `Base` graph import **none** of this optional state, so a page that never subscribes pays for none of it.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @children
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
children<T extends Base>(name: string, callbacks?: WatchChildrenCallbacks<T>): ValueDecorator<ChildrenCollection<T>>
|
|
5
|
+
children<T extends BaseConstructor>(ComponentClass: T, callbacks?: WatchChildrenCallbacks<InstanceType<T>>): ValueDecorator<ChildrenCollection<InstanceType<T>>>
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Field sugar over [`$watchChildren()`](../instance-methods.md#watchchildren).
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Base, children, component, type ChildrenCollection } from '@studiometa/js-toolkit';
|
|
14
|
+
|
|
15
|
+
class SliderItem extends Base {
|
|
16
|
+
static config = { name: 'SliderItem' };
|
|
17
|
+
}
|
|
18
|
+
@component({ name: 'Slider', components: { SliderItem } })
|
|
19
|
+
class Slider extends Base {
|
|
20
|
+
// Exact `config.name`.
|
|
21
|
+
@children('SliderItem')
|
|
22
|
+
items!: ChildrenCollection<Base>;
|
|
23
|
+
|
|
24
|
+
// Also every named subclass, through `instanceof`.
|
|
25
|
+
@children(SliderItem)
|
|
26
|
+
allItems!: ChildrenCollection<SliderItem>;
|
|
27
|
+
|
|
28
|
+
reset() {
|
|
29
|
+
for (const item of this.items) {
|
|
30
|
+
item.$el.removeAttribute('data-option-active');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The collection is **live**, in document order whatever the mount order is.
|
|
37
|
+
|
|
38
|
+
## Callbacks
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Base, children, component, type ChildrenCollection } from '@studiometa/js-toolkit';
|
|
42
|
+
|
|
43
|
+
class SliderItem extends Base {
|
|
44
|
+
static config = { name: 'SliderItem' };
|
|
45
|
+
}
|
|
46
|
+
@component({ name: 'Slider', components: { SliderItem } })
|
|
47
|
+
class Slider extends Base {
|
|
48
|
+
@children(SliderItem, {
|
|
49
|
+
added(item) {
|
|
50
|
+
// `this` is the Slider instance.
|
|
51
|
+
this.reindex();
|
|
52
|
+
},
|
|
53
|
+
removed(item) {
|
|
54
|
+
this.reindex();
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
items!: ChildrenCollection<SliderItem>;
|
|
58
|
+
|
|
59
|
+
reindex() {}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**The callbacks are bound to the instance**, which is the one thing this form gives over the function form.
|
|
64
|
+
|
|
65
|
+
## It is instance-scoped
|
|
66
|
+
|
|
67
|
+
The subscription stays active through unmount and mount cycles, for the whole life of the watching instance. It is never released and dies with the element.
|
|
68
|
+
|
|
69
|
+
The initial sweep is deferred to a microtask, because a field initializer runs before the element is in place; the announcement listeners attach at once, so nothing is missed.
|
|
70
|
+
|
|
71
|
+
## The function form
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
75
|
+
|
|
76
|
+
class SliderItem extends Base {
|
|
77
|
+
static config = { name: 'SliderItem' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class Slider extends Base {
|
|
81
|
+
static config = { name: 'Slider', components: { SliderItem } };
|
|
82
|
+
|
|
83
|
+
items = this.$watchChildren(SliderItem, {
|
|
84
|
+
added: (item) => this.reindex(),
|
|
85
|
+
removed: (item) => this.reindex(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
reindex() {}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Identical behaviour, no build step. Note the arrow functions: the function form does not bind the callbacks for you.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @component
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
component(config: BaseConfig): (value, context) => void
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Writes `static config` and calls [`registerComponent()`](../registry/registerComponent.md), in one step.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Base, component } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
@component({
|
|
15
|
+
name: 'Slider',
|
|
16
|
+
refs: ['next', 'items[]'],
|
|
17
|
+
options: { speed: { type: Number, default: 1 } },
|
|
18
|
+
})
|
|
19
|
+
class Slider extends Base {
|
|
20
|
+
onNextClick() {}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The class is registered **as soon as it is defined**, so there is no separate registration line to forget.
|
|
25
|
+
|
|
26
|
+
## It merges with `static config`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Base, component } from '@studiometa/js-toolkit';
|
|
30
|
+
|
|
31
|
+
@component({ name: 'Slider', refs: ['next'] })
|
|
32
|
+
class Slider extends Base {
|
|
33
|
+
static config = { name: 'Slider', options: { speed: Number } };
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
They merge in a **class initializer**, which runs after the fields and inside the class definition, so `registerComponent()` on the next line reads the finished config.
|
|
38
|
+
|
|
39
|
+
The rules are the rules of `$config`:
|
|
40
|
+
|
|
41
|
+
| Key | Merge rule |
|
|
42
|
+
| ----------------------- | -------------- |
|
|
43
|
+
| `refs` | union |
|
|
44
|
+
| `options`, `components` | entry by entry |
|
|
45
|
+
| a declared scalar | overrides |
|
|
46
|
+
|
|
47
|
+
A key both sides declare **differently** is reported as `component.config-conflict`.
|
|
48
|
+
|
|
49
|
+
## It is applied last
|
|
50
|
+
|
|
51
|
+
Among stacked class decorators, `@component` is applied last, so it registers a finished class. That is what lets a service mixin decorator sit under it:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Base, component, withScroll } from '@studiometa/js-toolkit';
|
|
55
|
+
|
|
56
|
+
@component({ name: 'Header' })
|
|
57
|
+
@withScroll()
|
|
58
|
+
class Header extends Base {
|
|
59
|
+
scrolled() {}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## The function form
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
67
|
+
|
|
68
|
+
class Slider extends Base {
|
|
69
|
+
static config = { name: 'Slider', refs: ['next'] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registerComponent(Slider);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Identical behaviour, no build step.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Decorators
|
|
2
|
+
|
|
3
|
+
**Sugar, never a requirement.** No engine ships stage-3 decorators, so every decorator here is a thin wrapper over a function API that works without it.
|
|
4
|
+
|
|
5
|
+
| Decorator | Wraps |
|
|
6
|
+
| ------------------------------------------------ | --------------------------------------- |
|
|
7
|
+
| [`@component(config)`](component.md) | `static config` + `registerComponent()` |
|
|
8
|
+
| [`@on(target, type)`](on.md) | the `on<Child><Event>` names |
|
|
9
|
+
| [`@provide(key)`](provide.md) | `$provide()` |
|
|
10
|
+
| [`@inject(key)`](inject.md) | `$inject()` |
|
|
11
|
+
| [`@children(nameOrClass, cb?)`](children.md) | `$watchChildren()` |
|
|
12
|
+
| [`@read` / `@write`](read-write.md) | `$read()` / `$write()` |
|
|
13
|
+
|
|
14
|
+
The service mixins have a decorator form too — see [Mixins](../services/mixins.md).
|
|
15
|
+
|
|
16
|
+
## A page with no build step keeps everything
|
|
17
|
+
|
|
18
|
+
`registerComponent`, `$provide`, `$watchChildren`, `$read`, `$write` and the `on<Child><Event>` method names are all there is. A page loaded from an ESM CDN uses the same framework with none of this file's contents.
|
|
19
|
+
|
|
20
|
+
## Each value decorator works on a plain field or an `accessor`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Base, component, inject, createContext } from '@studiometa/js-toolkit';
|
|
24
|
+
|
|
25
|
+
const Key = createContext<number>('key');
|
|
26
|
+
@component({ name: 'Demo' })
|
|
27
|
+
class Demo extends Base {
|
|
28
|
+
@inject(Key) a?: number;
|
|
29
|
+
@inject(Key) accessor b: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Build setup
|
|
34
|
+
|
|
35
|
+
Vite 8 transforms TypeScript with Oxc, which passes decorators through untouched, so a transform has to compile them. TypeScript 5 needs `experimentalDecorators: false`. See [Installation](../../guide/introduction/installation.md#with-a-build-step).
|
|
36
|
+
|
|
37
|
+
The package itself compiles them with `@rollup/plugin-swc` and `decoratorVersion: '2023-11'`, filtered to the files that contain a decorator.
|
|
38
|
+
|
|
39
|
+
## Read next
|
|
40
|
+
|
|
41
|
+
[Decorators](../../guide/going-further/using-decorators.md) walks the whole set, including how they stack and why the phase decorators are leaf-method sugar.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @inject
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
inject<T>(key: ContextKey<T>): ValueObserver<T | undefined>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Field sugar over [`$inject()`](../instance-methods.md#inject). **It asks once, at construction.**
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Base, component, createContext, inject, type Signal } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
const CountContext = createContext<Signal<number>>('count');
|
|
15
|
+
@component({ name: 'Output' })
|
|
16
|
+
class Output extends Base {
|
|
17
|
+
@inject(CountContext)
|
|
18
|
+
count?: Signal<number>;
|
|
19
|
+
|
|
20
|
+
mounted() {
|
|
21
|
+
// The request may not have been answered yet.
|
|
22
|
+
return this.count?.subscribe((value) => {
|
|
23
|
+
this.$el.textContent = String(value);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The field type includes `undefined`, because the request is asynchronous and the value lands when a provider answers.
|
|
30
|
+
|
|
31
|
+
## When to use it, and when not
|
|
32
|
+
|
|
33
|
+
| Situation | Reach for |
|
|
34
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
35
|
+
| the value is there for the whole life of the instance | `@inject` |
|
|
36
|
+
| the consumer must be able to wait through several mount cycles | `$inject()` from `mounted()` |
|
|
37
|
+
| the answer is optional and there is a fallback | `$injectSync()` |
|
|
38
|
+
| the provider comes and goes | [`subscribeContext()`](../context/subscribeContext.md) |
|
|
39
|
+
|
|
40
|
+
**`@inject` asks once, at construction.** `$inject()` from `mounted()` is unmount-scoped: `$unmount()` cancels the pending request, and a new mount asks again. That difference is the whole reason both exist.
|
|
41
|
+
|
|
42
|
+
## A plain field or an `accessor`
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Base, component, createContext, inject, type Signal } from '@studiometa/js-toolkit';
|
|
46
|
+
|
|
47
|
+
const CountContext = createContext<Signal<number>>('count');
|
|
48
|
+
@component({ name: 'Output' })
|
|
49
|
+
class Output extends Base {
|
|
50
|
+
@inject(CountContext) a?: Signal<number>;
|
|
51
|
+
@inject(CountContext) accessor b: Signal<number> | undefined;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## The function form
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { Base, createContext, type Signal } from '@studiometa/js-toolkit';
|
|
59
|
+
|
|
60
|
+
const CountContext = createContext<Signal<number>>('count');
|
|
61
|
+
class Output extends Base {
|
|
62
|
+
static config = { name: 'Output' };
|
|
63
|
+
|
|
64
|
+
async mounted() {
|
|
65
|
+
const count = await this.$inject(CountContext);
|
|
66
|
+
return count.subscribe((value) => {
|
|
67
|
+
this.$el.textContent = String(value);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `await` form has no `undefined` to handle, which is usually the better trade.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @on
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
on(type: string)
|
|
5
|
+
on(target: typeof window | typeof document, type: string)
|
|
6
|
+
on(child: BaseConstructor | string, type: string)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Binds a method to an event source, in place of the `on<X><Event>` naming convention.
|
|
10
|
+
|
|
11
|
+
## Every form
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
Base,
|
|
16
|
+
component,
|
|
17
|
+
on,
|
|
18
|
+
type DelegatedEvent,
|
|
19
|
+
type GlobalEvent,
|
|
20
|
+
type RefEvent,
|
|
21
|
+
} from '@studiometa/js-toolkit';
|
|
22
|
+
|
|
23
|
+
class AccordionItem extends Base<{ $emits: { open: { height: number } } }> {
|
|
24
|
+
static config = { name: 'AccordionItem' };
|
|
25
|
+
}
|
|
26
|
+
@component({ name: 'Demo', components: { AccordionItem }, refs: ['dots[]'] })
|
|
27
|
+
class Demo extends Base {
|
|
28
|
+
// The component's own element, typed from HTMLElementEventMap.
|
|
29
|
+
@on('click') a(event: MouseEvent) {}
|
|
30
|
+
@on('submit') b(event: SubmitEvent) {}
|
|
31
|
+
|
|
32
|
+
// A ref, named as it is declared.
|
|
33
|
+
@on('dots[]', 'click') c({ index }: RefEvent) {}
|
|
34
|
+
|
|
35
|
+
// A child, by name — imports nothing, and the payload stays `unknown`.
|
|
36
|
+
@on('AccordionItem', 'open') d({ payload }: DelegatedEvent<AccordionItem>) {}
|
|
37
|
+
|
|
38
|
+
// A child, by class — the class is the type, so the payload is typed.
|
|
39
|
+
@on(AccordionItem, 'open') e({ payload }: DelegatedEvent<AccordionItem, 'open'>) {}
|
|
40
|
+
|
|
41
|
+
// A global.
|
|
42
|
+
@on(window, 'resize') f({ event }: GlobalEvent<UIEvent>) {}
|
|
43
|
+
@on(document, 'click') g({ event }: GlobalEvent<MouseEvent>) {}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## The one-argument form
|
|
48
|
+
|
|
49
|
+
It types its event from `HTMLElementEventMap`, so `@on('click')` hands over a `MouseEvent` and `@on('submit')` a `SubmitEvent`.
|
|
50
|
+
|
|
51
|
+
A name **outside** that map is a component event, whose detail only its emitter knows, so the handler declares the type it expects:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Base, component, on } from '@studiometa/js-toolkit';
|
|
55
|
+
@component({ name: 'Slot' })
|
|
56
|
+
class Slot extends Base {
|
|
57
|
+
@on('content')
|
|
58
|
+
inject(event: CustomEvent<{ content: string }>) {
|
|
59
|
+
this.$el.innerHTML = event.detail.content;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## A name is a child or a ref
|
|
65
|
+
|
|
66
|
+
Resolved **children-first**, so the handler is typed as `DelegatedEvent` or `RefEvent`.
|
|
67
|
+
|
|
68
|
+
A **ref is named as it is declared**: `@on('dots[]', 'click')` for `config.refs: ['dots[]']`. The rule is one rule — the declaration spelling refers to the entry, and the property spelling is used where a name is derived from it.
|
|
69
|
+
|
|
70
|
+
A mismatched `@on('dots', 'click')` gives a warning at bind time when the other spelling is declared. A name that matches nothing stays **silent**.
|
|
71
|
+
|
|
72
|
+
## A class resolves to its merged `config.name`
|
|
73
|
+
|
|
74
|
+
It lands on the same delegated entry as the string form, so the two behave identically **at runtime**. They differ in what they can type:
|
|
75
|
+
|
|
76
|
+
| Form | `target` | `payload` |
|
|
77
|
+
| ------------------------------ | --------------- | ----------------------- |
|
|
78
|
+
| `@on(AccordionItem, 'open')` | `AccordionItem` | typed from its `$emits` |
|
|
79
|
+
| `@on('AccordionItem', 'open')` | the annotation | `unknown` |
|
|
80
|
+
|
|
81
|
+
**Only the class form carries the payload type**, because only it has the class to read `$emits` from. The string form's event name is a plain `string` as far as the type system is concerned, so annotate it as `DelegatedEvent<AccordionItem>` and narrow the payload yourself:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { Base, component, on, type DelegatedEvent } from '@studiometa/js-toolkit';
|
|
85
|
+
|
|
86
|
+
class AccordionItem extends Base<{ $emits: { open: { height: number } } }> {
|
|
87
|
+
static config = { name: 'AccordionItem' };
|
|
88
|
+
}
|
|
89
|
+
@component({ name: 'Accordion', components: { AccordionItem } })
|
|
90
|
+
class Accordion extends Base {
|
|
91
|
+
@on('AccordionItem', 'open')
|
|
92
|
+
byName({ payload }: DelegatedEvent<AccordionItem>) {
|
|
93
|
+
(payload as { height: number }).height;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@on(AccordionItem, 'open')
|
|
97
|
+
byClass({ payload }: DelegatedEvent<AccordionItem, 'open'>) {
|
|
98
|
+
payload.height; // number, with no cast
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**A lazy child needs the string form**
|
|
104
|
+
|
|
105
|
+
`@on('Child', 'open')` imports nothing. **A thunk is not a target**, and both the overloads and the runtime refuse it — so a lazy child is exactly the case that trades the typed payload for the deferred import.
|
|
106
|
+
|
|
107
|
+
## Globals
|
|
108
|
+
|
|
109
|
+
`@on(window, 'click')` types the event from `WindowEventMap` and falls back to `Event`. It goes through the same binding `onWindow<Event>` uses: **bubble phase, one listener per mount cycle, removed by `$unmount()`**.
|
|
110
|
+
|
|
111
|
+
**Nothing is reserved in its string space.** `@on('Window', 'resize')` means the child named `Window`; `@on(window, 'resize')` means the global. That is the escape from the reserved `onWindow` prefix.
|
|
112
|
+
|
|
113
|
+
## Any other `EventTarget` is refused
|
|
114
|
+
|
|
115
|
+
By the overloads, and by a `TypeError` at runtime. A decorator is evaluated once, at class definition, so an arbitrary target could only ever be a module-scope value — and binding a component's lifetime to one would be a leak with no way to see it.
|
|
116
|
+
|
|
117
|
+
## It checks the annotation; it does not replace it
|
|
118
|
+
|
|
119
|
+
A decorator cannot infer a method's own parameters: the method signature is checked **against** what the decorator expects. So annotate the payload either way — with `@on`, the annotation is verified against a real target instead of being taken on trust.
|
|
120
|
+
|
|
121
|
+
## Stacking
|
|
122
|
+
|
|
123
|
+
The skip is keyed by the **method name**, so `@on` stacks with itself and with `@read`/`@write` in either order:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { Base, component, on, write } from '@studiometa/js-toolkit';
|
|
127
|
+
|
|
128
|
+
class Child extends Base {
|
|
129
|
+
static config = { name: 'Child' };
|
|
130
|
+
}
|
|
131
|
+
@component({ name: 'Demo', components: { Child } })
|
|
132
|
+
class Demo extends Base {
|
|
133
|
+
// One method, two events.
|
|
134
|
+
@on('Child', 'open')
|
|
135
|
+
@on('Child', 'close')
|
|
136
|
+
track() {}
|
|
137
|
+
|
|
138
|
+
// A phase decorator nearest the method schedules the handler's body.
|
|
139
|
+
@on('click')
|
|
140
|
+
@write
|
|
141
|
+
paint() {}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
See [`@read` / `@write`](read-write.md).
|
|
146
|
+
|
|
147
|
+
## The function form
|
|
148
|
+
|
|
149
|
+
The `on<X><Event>` method names. Identical behaviour, no build step. See [Events hooks](../methods-hooks-events.md).
|