@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,37 @@
|
|
|
1
|
+
# fromWebpackContext
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
fromWebpackContext(context: WebpackContextLike): ModuleRecord
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Adapts an `import.meta.webpackContext()` result for [`defineManifest()`](defineManifest.md).
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { defineManifest, fromWebpackContext, registerManifest } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
registerManifest(
|
|
15
|
+
defineManifest({
|
|
16
|
+
modules: fromWebpackContext(
|
|
17
|
+
import.meta.webpackContext('./components', {
|
|
18
|
+
recursive: true,
|
|
19
|
+
regExp: /\/index\.js$/,
|
|
20
|
+
mode: 'lazy',
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Parameters**
|
|
28
|
+
|
|
29
|
+
- `context` (`WebpackContextLike`) — anything with the `keys()` method and the callable shape of a webpack context.
|
|
30
|
+
|
|
31
|
+
**Return value**
|
|
32
|
+
|
|
33
|
+
- `ModuleRecord` — a map of path to a **deferred** importer, one per key.
|
|
34
|
+
|
|
35
|
+
The import of each key is deferred rather than performed, so nothing is downloaded when the manifest is built. Use `mode: 'lazy'` so webpack emits a chunk per module.
|
|
36
|
+
|
|
37
|
+
The adapter adds **no webpack dependency**. `WebpackContextLike` is a structural type, so the adapter is testable without a bundler and core names webpack nowhere.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# getInstance
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
getInstance<T extends Base = Base>(el: Element | null, name: string): T | undefined
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The instance of `name` on `el`, mounted or not.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Base, getInstance } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
class Dialog extends Base {
|
|
15
|
+
static config = { name: 'Dialog' };
|
|
16
|
+
|
|
17
|
+
close() {}
|
|
18
|
+
}
|
|
19
|
+
// `el` may be null — a querySelector() result passes straight through.
|
|
20
|
+
const dialog = getInstance<Dialog>(document.querySelector('[data-component="Dialog"]'), 'Dialog');
|
|
21
|
+
|
|
22
|
+
if (dialog?.$isMounted) dialog.close();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Parameters**
|
|
26
|
+
|
|
27
|
+
- `el` (`Element | null`) — the element to read.
|
|
28
|
+
- `name` (`string`) — the component name.
|
|
29
|
+
|
|
30
|
+
**Return value**
|
|
31
|
+
|
|
32
|
+
- `T | undefined`.
|
|
33
|
+
|
|
34
|
+
## Why it is a function of its own
|
|
35
|
+
|
|
36
|
+
One map read and no scan. The caller already holds the element and the name, so there is nothing left to search — which is what makes it different from a filter over [`getInstances()`](getInstances.md).
|
|
37
|
+
|
|
38
|
+
It is also the answer to "is this element's instance there yet", which every plural form loses by returning a list.
|
|
39
|
+
|
|
40
|
+
## Why `el` accepts `null` but the return is not
|
|
41
|
+
|
|
42
|
+
`el` accepts `null` so a `querySelector()` result can be passed straight through. The two ways of having no instance — no element, and an element without one — are the same answer to the caller, and `undefined` says it for both. Narrowing the parameter to `Element` would buy nothing back: the body reads an optional map either way, so the only thing a stricter type produces is a `!` at every call site asserting something the function never needed.
|
|
43
|
+
|
|
44
|
+
The **return** stays `T | undefined`, and that asymmetry is deliberate. An absent element is a fact the caller may reasonably not know; an absent instance is a fact the caller must handle.
|
|
45
|
+
|
|
46
|
+
## There is no `getMountedInstance`
|
|
47
|
+
|
|
48
|
+
The result is one object, so a caller who needs the live one reads `.$isMounted` on it. A second export would only hide that check behind an `undefined` that means two things.
|
|
49
|
+
|
|
50
|
+
## Detached elements
|
|
51
|
+
|
|
52
|
+
This form reads the element's instance map directly and never consults the DOM, so it answers for a detached element as readily as for a connected one.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# getInstances
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
getInstances<T extends Base = Base>(name: string, root?: ParentNode): T[]
|
|
5
|
+
getInstances<T extends Base = Base>(el: Element): T[]
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
**Every instance that exists** for a component name, mounted or not, in DOM order — or every instance on one element, in mount order.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { getInstances } from '@studiometa/js-toolkit';
|
|
14
|
+
|
|
15
|
+
const section = document.querySelector('section')!;
|
|
16
|
+
|
|
17
|
+
getInstances('Dialog'); // every Dialog in the document
|
|
18
|
+
getInstances('Dialog', section); // every Dialog built in a region
|
|
19
|
+
getInstances(section); // everything on one element
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Reach for it when the result is **counted or inspected**. When it is going to be _used_, prefer [`getMountedInstances()`](getMountedInstances.md) — every instance in that list has run `mounted()` and has not yet run `unmounted()`.
|
|
23
|
+
|
|
24
|
+
## The two overloads
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
(name: string, root?: ParentNode): T[]
|
|
28
|
+
(el: Element): T[]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **The name form** searches the descendants of `root`, which defaults to the document. `root` is a `ParentNode` and the call is `querySelectorAll`, so it never matches `root` itself.
|
|
32
|
+
- **The element form** reads the element's instance map directly and never consults the DOM, so it answers for a **detached** element as readily as for a connected one. The name form cannot: `document.querySelectorAll()` does not see a detached element. Pass the detached root as `root` when a name lookup has to reach inside it.
|
|
33
|
+
|
|
34
|
+
## How the narrowing works
|
|
35
|
+
|
|
36
|
+
`selectorFor(name)` over-matches on purpose — it lists the responsive spellings of `data-component` too — and **the instance-map read is what removes an inactive declaration**, because a breakpoint-withdrawn component is destroyed _and_ deleted from the map.
|
|
37
|
+
|
|
38
|
+
A matching element with no instance is skipped, and that is the whole narrowing. There is no mount filter, because it never did that work.
|
|
39
|
+
|
|
40
|
+
## Where the instances live
|
|
41
|
+
|
|
42
|
+
An element publishes its instances under `Symbol.for('@studiometa/js-toolkit/instances')`. That map is **not public API**; these four lookups are, and between them they express every read it answers. In a console:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
$0[Symbol.for('@studiometa/js-toolkit/instances')];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The element overload exists rather than a second export because both forms answer "which instances are there" and the argument picks the scope — and it keeps the map read in one place, which matters more now that the key is a symbol and no longer spellable as `el.__base__`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# getMountedInstances
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
getMountedInstances<T extends Base = Base>(name: string, root?: ParentNode): T[]
|
|
5
|
+
getMountedInstances<T extends Base = Base>(el: Element): T[]
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
**The live instances** of a component name, in DOM order — or the live ones on one element, in mount order.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { getMountedInstances } from '@studiometa/js-toolkit';
|
|
14
|
+
|
|
15
|
+
// This is the safe list to call a method on.
|
|
16
|
+
getMountedInstances('Dialog').forEach((dialog) => dialog.$unmount());
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**This is the safe list to call a method on**: every instance in it has run `mounted()` and has not yet run `unmounted()`. Prefer it over [`getInstances()`](getInstances.md) whenever the result is going to be _used_ rather than counted or inspected.
|
|
20
|
+
|
|
21
|
+
## The two overloads
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
(name: string, root?: ParentNode): T[]
|
|
25
|
+
(el: Element): T[]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **The name form** searches the descendants of `root`, which defaults to the document. `root` is a `ParentNode` and the call is `querySelectorAll`, so it never matches `root` itself.
|
|
29
|
+
- **The element form** reads the element's instance map directly and never consults the DOM, so it answers for a **detached** element as readily as for a connected one. The name form cannot: `document.querySelectorAll()` does not see a detached element. Pass the detached root as `root` when a name lookup has to reach inside it.
|
|
30
|
+
|
|
31
|
+
## How the narrowing works
|
|
32
|
+
|
|
33
|
+
`selectorFor(name)` over-matches on purpose — it lists the responsive spellings of `data-component` too — and **the instance-map read is what removes an inactive declaration**, because a breakpoint-withdrawn component is destroyed _and_ deleted from the map.
|
|
34
|
+
|
|
35
|
+
A matching element with no instance is skipped, and that is the whole narrowing. There is no mount filter, because it never did that work.
|
|
36
|
+
|
|
37
|
+
## There is no `getMountedInstance`
|
|
38
|
+
|
|
39
|
+
The singular [`getInstance()`](getInstance.md) returns one object, so a caller who needs the live one reads `.$isMounted` on it. A second export would only hide that check behind an `undefined` that means two things — no element, no instance, or an instance that is not mounted.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# getUnmountedInstances
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
getUnmountedInstances<T extends Base = Base>(name: string, root?: ParentNode): T[]
|
|
5
|
+
getUnmountedInstances<T extends Base = Base>(el: Element): T[]
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The instances of a component name that **were built and are not mounted**, in DOM order — or the same on one element, in mount order.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { getUnmountedInstances } from '@studiometa/js-toolkit';
|
|
14
|
+
|
|
15
|
+
getUnmountedInstances('Video').length;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What this population is
|
|
19
|
+
|
|
20
|
+
It is small and specific. It is what a **reversible** mount strategy leaves behind: `in-view` and `media:` unmount their instance when the condition stops holding and keep it for the crossing back, so the instance stays in the element's map with `$isMounted === false`.
|
|
21
|
+
|
|
22
|
+
A constructor that succeeded before a failing `mounted()` lands here too.
|
|
23
|
+
|
|
24
|
+
## What is _not_ here
|
|
25
|
+
|
|
26
|
+
- **A declaration whose class never arrived.** A lazy entry that has not loaded has no instance at all.
|
|
27
|
+
- **A declaration withdrawn by a breakpoint.** That instance is unmounted _and deleted_ from the map, so the name later declared again builds a new one.
|
|
28
|
+
- **A component still waiting on a mount strategy.** It has never been constructed.
|
|
29
|
+
|
|
30
|
+
Neither has an instance, so neither can be in a list of them.
|
|
31
|
+
|
|
32
|
+
## The two overloads
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
(name: string, root?: ParentNode): T[]
|
|
36
|
+
(el: Element): T[]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **The name form** searches the descendants of `root`, which defaults to the document. `root` is a `ParentNode` and the call is `querySelectorAll`, so it never matches `root` itself.
|
|
40
|
+
- **The element form** reads the element's instance map directly and never consults the DOM, so it answers for a **detached** element as readily as for a connected one. The name form cannot: `document.querySelectorAll()` does not see a detached element. Pass the detached root as `root` when a name lookup has to reach inside it.
|
|
41
|
+
|
|
42
|
+
## How the narrowing works
|
|
43
|
+
|
|
44
|
+
`selectorFor(name)` over-matches on purpose — it lists the responsive spellings of `data-component` too — and **the instance-map read is what removes an inactive declaration**, because a breakpoint-withdrawn component is destroyed _and_ deleted from the map.
|
|
45
|
+
|
|
46
|
+
A matching element with no instance is skipped, and that is the whole narrowing. There is no mount filter, because it never did that work.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Registry
|
|
2
|
+
|
|
3
|
+
The registry is the only code that constructs an instance. It holds one entry per name and one controller per element/component pair.
|
|
4
|
+
|
|
5
|
+
## Registering
|
|
6
|
+
|
|
7
|
+
- [`registerComponent(Ctor)`](registerComponent.md) — one eager entry, and its declared family
|
|
8
|
+
- [`registerComponents(...ctors)`](registerComponents.md) — several at once
|
|
9
|
+
- [`registerManifest(entries)`](registerManifest.md) — lazy entries in the same map
|
|
10
|
+
|
|
11
|
+
## Building a manifest
|
|
12
|
+
|
|
13
|
+
- [`defineManifest(options)`](defineManifest.md) — derive tokens from module paths
|
|
14
|
+
- [`fromMetaGlob(glob)`](fromMetaGlob.md) — a Vite `import.meta.glob` result
|
|
15
|
+
- [`fromWebpackContext(context)`](fromWebpackContext.md) — a webpack context
|
|
16
|
+
|
|
17
|
+
## Looking instances up
|
|
18
|
+
|
|
19
|
+
Four functions answer from the DOM. They keep **no registry of instances**.
|
|
20
|
+
|
|
21
|
+
| Function | Population |
|
|
22
|
+
| --------------------------------------------------------- | ------------------------------------------ |
|
|
23
|
+
| [`getInstances()`](getInstances.md) | every instance that exists, mounted or not |
|
|
24
|
+
| [`getMountedInstances()`](getMountedInstances.md) | the live ones — safe to call a method on |
|
|
25
|
+
| [`getUnmountedInstances()`](getUnmountedInstances.md) | built, then stood down |
|
|
26
|
+
| [`getInstance(el, name)`](getInstance.md) | the one on this element |
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
getMountedInstances('Dialog').forEach((dialog) => dialog.close());
|
|
30
|
+
getInstances('Dialog', section);
|
|
31
|
+
getUnmountedInstances('Dialog');
|
|
32
|
+
getInstance(el, 'Dialog');
|
|
33
|
+
getInstances(el);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The three plural forms take the same two overloads: a **name** with an optional `ParentNode` root, or an **element**.
|
|
37
|
+
|
|
38
|
+
- **A matching element with no instance is skipped, and that is the whole narrowing.** `selectorFor(name)` over-matches on purpose — it lists the responsive spellings of `data-component` too — and the instance-map read removes an inactive declaration, because a breakpoint-withdrawn component is destroyed _and_ deleted from the map.
|
|
39
|
+
- `root` is a `ParentNode` and the call is `querySelectorAll`, so an element root searches its **descendants** and never matches itself.
|
|
40
|
+
- **The element overload is the only form that reaches a detached element**, since `querySelectorAll()` does not see one. Pass the detached root as `root` when a name lookup has to reach inside it.
|
|
41
|
+
- **There is no `getMountedInstance`.** The singular returns one object, so a caller reads `.$isMounted` on it.
|
|
42
|
+
|
|
43
|
+
## In tests
|
|
44
|
+
|
|
45
|
+
`resetRegistry()` is re-exported from [`/test`](../test/index.md) and drops every registration. It is not part of a page's vocabulary.
|
|
46
|
+
|
|
47
|
+
## Read next
|
|
48
|
+
|
|
49
|
+
[The registry](../../guide/concepts/the-registry.md) explains the entry, the controller and the one scheduling algorithm that covers eager, lazy and conditional mounting.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# registerComponent
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
registerComponent(ComponentClass: BaseConstructor): void
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Registers a component and its merged family, then scans the document for matching elements.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
class Slider extends Base {
|
|
15
|
+
static config = { name: 'Slider' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
registerComponent(Slider);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Parameters**
|
|
22
|
+
|
|
23
|
+
- `ComponentClass` — a class extending `Base`, with a `config.name`.
|
|
24
|
+
|
|
25
|
+
**Return value**
|
|
26
|
+
|
|
27
|
+
- `void`. It is a registration, not an instantiation: the elements that match get their instances through the ordinary pipeline, and there is no list of instances to hand back. Use [`getInstances()`](getInstances.md) if you need them.
|
|
28
|
+
|
|
29
|
+
## It registers the family too
|
|
30
|
+
|
|
31
|
+
`config.components` is walked in one loop, so one call covers a whole tree:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
35
|
+
|
|
36
|
+
class AccordionItem extends Base {
|
|
37
|
+
static config = { name: 'AccordionItem' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Accordion extends Base {
|
|
41
|
+
static config = {
|
|
42
|
+
name: 'Accordion',
|
|
43
|
+
components: { AccordionItem },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Registers both.
|
|
48
|
+
registerComponent(Accordion);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A thunk in `config.components` is **deferred** rather than resolved, and becomes a lazy entry of the same registry under its key. See [Autoloading](../../guide/going-further/autoloading.md).
|
|
52
|
+
|
|
53
|
+
## The name comes from the merged config
|
|
54
|
+
|
|
55
|
+
Like the instance's `$id` and the instance-map key it publishes itself under. A subclass that extends a component with extra config and forgets to rename therefore **collides** with the name it inherited, rather than registering under `undefined`.
|
|
56
|
+
|
|
57
|
+
## One name, one entry
|
|
58
|
+
|
|
59
|
+
As with `customElements.define()`. A second registration under a name already taken gives a `registry.conflict` warning and is ignored.
|
|
60
|
+
|
|
61
|
+
The exception is a lazy child declared by several parents: that is the normal case, and it is **first wins, quietly**. A token two components genuinely claim is caught by the class-name check when the import lands.
|
|
62
|
+
|
|
63
|
+
## Registering a class you cannot edit
|
|
64
|
+
|
|
65
|
+
Do it in expression position:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
registerComponent(
|
|
69
|
+
class extends Vendor {
|
|
70
|
+
static config = { name: 'CompactVendor', options: { compact: Boolean } };
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What it does not do
|
|
76
|
+
|
|
77
|
+
- **It does not construct anything itself.** The registry does, when the DOM and the mount strategy agree.
|
|
78
|
+
- **It does not take a name or a selector.** `config.name` is the only name, and arbitrary selectors are removed.
|
|
79
|
+
- **It does not take a promise or a thunk.** Use [`registerManifest()`](registerManifest.md) for a lazy entry.
|
|
80
|
+
- **It has no inverse on a page.** `resetRegistry()` exists for tests only — see [`/test`](../test/index.md).
|
|
81
|
+
|
|
82
|
+
## `@component`
|
|
83
|
+
|
|
84
|
+
The decorator writes `static config` and calls this function in one step:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Base, component } from '@studiometa/js-toolkit';
|
|
88
|
+
|
|
89
|
+
@component({ name: 'Slider', refs: ['next'] })
|
|
90
|
+
class Slider extends Base {}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See [`@component`](../decorators/component.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# registerComponents
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
registerComponents(...classes: BaseConstructor[]): void
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
[`registerComponent()`](registerComponent.md) for several classes, in order.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Base, registerComponents } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
class Accordion extends Base {
|
|
15
|
+
static config = { name: 'Accordion' };
|
|
16
|
+
}
|
|
17
|
+
class Slider extends Base {
|
|
18
|
+
static config = { name: 'Slider' };
|
|
19
|
+
}
|
|
20
|
+
class TodoList extends Base {
|
|
21
|
+
static config = { name: 'TodoList' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
registerComponents(Accordion, Slider, TodoList);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This is the whole bootstrap of a page. There is no `createApp()` and no root component: each of these registers its own declared family, and every registered name auto-mounts through the single MutationObserver.
|
|
28
|
+
|
|
29
|
+
**Parameters**
|
|
30
|
+
|
|
31
|
+
- `...classes` — classes extending `Base`.
|
|
32
|
+
|
|
33
|
+
**Return value**
|
|
34
|
+
|
|
35
|
+
- `void`.
|
|
36
|
+
|
|
37
|
+
Order matters only for collisions: the first registration of a name wins and the second gives a `registry.conflict` warning.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# registerManifest
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
registerManifest(entries: ComponentManifest): void
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Registers lazy component entries in the same registry the eager classes use.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { registerManifest } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
registerManifest({
|
|
15
|
+
Accordion: () => import('./components/Accordion.js'),
|
|
16
|
+
Slider: () => import('./components/Slider.js'),
|
|
17
|
+
Map: { load: () => import('./components/Map.js'), mountStrategy: 'visible' },
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Types
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
type ComponentManifest = Record<string, ComponentImporter | ComponentManifestEntry>;
|
|
25
|
+
|
|
26
|
+
type ComponentImporter = () => Promise<unknown>;
|
|
27
|
+
|
|
28
|
+
interface ComponentManifestEntry {
|
|
29
|
+
load: ComponentImporter;
|
|
30
|
+
mountStrategy?: MountStrategy;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
An entry is either the importer itself, or an object with `load` and an optional `mountStrategy`.
|
|
35
|
+
|
|
36
|
+
## The contract
|
|
37
|
+
|
|
38
|
+
- **One name, one entry**, across eager classes and manifests alike. A token an eager class or an earlier manifest owns gives a `manifest.duplicate-token` warning and is ignored.
|
|
39
|
+
- **No dependency and no bundler knowledge.** The value is a function that returns a promise. `import.meta.glob`, `import.meta.webpackContext` and a generated manifest all produce that shape, and core names none of them.
|
|
40
|
+
- **The class is resolved out of the imported module for you**, so a hand-written entry is `Slider: () => import('./Slider.js')` with nothing to unwrap.
|
|
41
|
+
- **The registry stays the only constructor.** The import ends in [`registerComponent()`](registerComponent.md); autoload never touches `new`, the instance map or a mount hook.
|
|
42
|
+
- **One import per name, one failure report per name.** A failure emits one `component.load-failed` diagnostic and is never retried.
|
|
43
|
+
- **A class whose `config.name` differs from its token** gives a `registry.lazy-name-mismatch` warning.
|
|
44
|
+
- **An unloaded declaration is invisible** to `$query()`, `$closest()`, `$watchChildren()` and `getInstances()`, because nothing is constructed at discovery.
|
|
45
|
+
|
|
46
|
+
## The mount strategy chain
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
data-mount > manifest entry mountStrategy > 'eager'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The entry's strategy stands in the middle slot for the `config.mountStrategy` of a class that cannot be read yet. Once the class registers, the chain reads `config.mountStrategy` and the entry is deleted from the map.
|
|
53
|
+
|
|
54
|
+
There is **no `data-load`**: deferring the import and deferring the mount are one decision. See [Autoloading](../../guide/going-further/autoloading.md).
|
|
55
|
+
|
|
56
|
+
## `whenDOMSettled()` covers an eager lazy component
|
|
57
|
+
|
|
58
|
+
The import promise joins the lifecycle-work set for the **eager** trigger only, so [`swap()`](../dom/swap.md) waits for download, registration and mount — and still never waits on a viewport, an idle callback, an interaction or a media query.
|
|
59
|
+
|
|
60
|
+
## From a glob
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { defineManifest, fromMetaGlob, registerManifest } from '@studiometa/js-toolkit';
|
|
64
|
+
|
|
65
|
+
registerManifest(
|
|
66
|
+
defineManifest({
|
|
67
|
+
modules: fromMetaGlob(import.meta.glob('./components/*/index.js')),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See [`defineManifest()`](defineManifest.md).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# defaultScheduler
|
|
2
|
+
|
|
3
|
+
The one scheduler instance. Duplicate copies of the package reuse the canonical one through the shared runtime.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
class Scheduler {
|
|
7
|
+
get phase(): SchedulerPhase;
|
|
8
|
+
tick(callback: TickCallback): () => void;
|
|
9
|
+
read<T>(fn: () => T): ScheduledTask<T>;
|
|
10
|
+
write<T>(fn: () => T): ScheduledTask<T>;
|
|
11
|
+
whenIdle(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## `read()` and `write()`
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defaultScheduler } from '@studiometa/js-toolkit';
|
|
19
|
+
|
|
20
|
+
const el = document.body;
|
|
21
|
+
async function resize() {
|
|
22
|
+
const box = await defaultScheduler.read(() => el.getBoundingClientRect()).promise;
|
|
23
|
+
await defaultScheduler.write(() => {
|
|
24
|
+
el.style.height = `${box?.height}px`;
|
|
25
|
+
}).promise;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Both return a handle:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface ScheduledTask<T = unknown> {
|
|
33
|
+
promise: Promise<T | undefined>;
|
|
34
|
+
cancel(): void;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- The promise resolves with the **return value of the task**, so a read hands its measurement back.
|
|
39
|
+
- `cancel()` is idempotent, and a cancelled task's promise never resolves with a value.
|
|
40
|
+
- **Queued execution only.** There is no synchronous escape.
|
|
41
|
+
|
|
42
|
+
### The thrash rule
|
|
43
|
+
|
|
44
|
+
| Scheduled from | Runs in |
|
|
45
|
+
| -------------- | -------------- |
|
|
46
|
+
| a `read` | the same frame |
|
|
47
|
+
| a `write` | the next frame |
|
|
48
|
+
|
|
49
|
+
Each queue array is **swapped for an empty one when its phase starts**, so a task scheduled into the running phase lands in the next frame's batch. The `write` batch is taken after the reads run.
|
|
50
|
+
|
|
51
|
+
## `tick()`
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { defaultScheduler } from '@studiometa/js-toolkit';
|
|
55
|
+
|
|
56
|
+
const unsubscribe = defaultScheduler.tick(({ time, delta }) => {
|
|
57
|
+
console.log(time, delta);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
interface TickProps {
|
|
63
|
+
readonly time: DOMHighResTimeStamp;
|
|
64
|
+
readonly delta: number;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- Tick callbacks run at the **start** of the flush, before `read`, so what they schedule belongs to the same frame.
|
|
69
|
+
- **The subscription is the only handle, and it keeps the loop alive.** The scheduler requests the next frame when a queue inside the frame is not empty, or when a tick subscriber stays. **There is no permanent rAF loop.**
|
|
70
|
+
- Tick subscribers are **not queued work**, so `whenIdle()` ignores them.
|
|
71
|
+
- A tick callback that throws is reported as `callback.scheduler-tick-failed` and skipped, never unsubscribed.
|
|
72
|
+
- **`delta` is clamped to `[1, 40]` ms**, and the first tick after the loop wakes reports `1000/60`. `time` stays the raw rAF timestamp.
|
|
73
|
+
|
|
74
|
+
For a subscription that comes and goes within a cycle, use [`toggle()`](../services/toggle.md) rather than subscribing and unsubscribing by hand.
|
|
75
|
+
|
|
76
|
+
## `whenIdle()`
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
await defaultScheduler.whenIdle();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Resolves at the end of a flush **and** at the end of a background drain, because it counts background tasks too.
|
|
83
|
+
|
|
84
|
+
It is one half of the timing recipe the [`/test`](../test/index.md) helpers encode — the other half is the mutation observer's delivery latency, which is why `whenIdle()` alone is not enough to know a component has mounted.
|
|
85
|
+
|
|
86
|
+
## `phase`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
type SchedulerPhase = 'idle' | 'tick' | 'read' | 'write' | 'background';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Reading it is a diagnostic, not a control. Code that branches on the phase is usually code that should schedule instead.
|
|
93
|
+
|
|
94
|
+
## The background lane
|
|
95
|
+
|
|
96
|
+
It posts its own turns through `scheduler.postTask({ priority: 'background' })`, and falls back to a `MessageChannel` message. Each turn runs a **5 ms slice** measured from the start of the drain, then gives the thread back and posts the next turn.
|
|
97
|
+
|
|
98
|
+
**Background work alone never requests an animation frame.**
|
|
99
|
+
|
|
100
|
+
A failure to post is reported as `scheduler.background-post-failed`.
|
|
101
|
+
|
|
102
|
+
## Error isolation
|
|
103
|
+
|
|
104
|
+
One try/catch per task. A task that throws is reported as `callback.scheduled-task-failed`, **rejects its own promise with the same value**, and is dropped. The flush continues and the scheduler never deadlocks.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Scheduler
|
|
2
|
+
|
|
3
|
+
One frame-aligned scheduler is the clock of the framework. It replaces `domScheduler`, the `RafService` loop, `SmartQueue` and the view-transition scheduler of `@studiometa/ui`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
frame start (rAF)
|
|
7
|
+
1. tick — fan out to the subscribers of the clock
|
|
8
|
+
2. read — measure: layout reads only
|
|
9
|
+
3. write — mutate: DOM writes only
|
|
10
|
+
style / layout / paint
|
|
11
|
+
|
|
12
|
+
between frames, on its own turns
|
|
13
|
+
background — time-sliced lane: mount and update lifecycle work,
|
|
14
|
+
mutation-record processing, manifest loading
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The exports
|
|
18
|
+
|
|
19
|
+
- [`defaultScheduler`](defaultScheduler.md) — the instance, with `tick`, `read`, `write`, `whenIdle` and `phase`
|
|
20
|
+
- [`nextFrame()`](nextFrame.md) — await one frame
|
|
21
|
+
- [`viewTransition(update)`](viewTransition.md) — a batched native view transition
|
|
22
|
+
|
|
23
|
+
From a component, `$read()` and `$write()` tie tasks to the instance so unmount cancels them. See [Instance methods](../instance-methods.md#read-and-write).
|
|
24
|
+
|
|
25
|
+
## Read next
|
|
26
|
+
|
|
27
|
+
[The scheduler](../../guide/going-further/scheduling-work.md) explains the lanes, the thrash rule and why `afterWrite` is gone.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# nextFrame
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
nextFrame(): Promise<void>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Resolves on the next animation frame.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { nextFrame } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
async function afterOneFrame(el: HTMLElement) {
|
|
15
|
+
el.classList.add('is-entering');
|
|
16
|
+
await nextFrame();
|
|
17
|
+
el.classList.add('is-active');
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it is for
|
|
22
|
+
|
|
23
|
+
Letting the browser see a style before the next one is applied — the two-step every CSS transition needs. `transition()`, `enterTransition()` and `leaveTransition()` are built on exactly this.
|
|
24
|
+
|
|
25
|
+
## What it is not
|
|
26
|
+
|
|
27
|
+
It does **not** mean "after paint". rAF callbacks run **before** style, layout and paint, so no phase inside a frame can read post-layout geometry. `afterWrite` is removed for that reason.
|
|
28
|
+
|
|
29
|
+
To measure after a layout has happened, measure in the `read` phase of the next frame — [`defaultScheduler.read()`](defaultScheduler.md) or `$read()` — or use a `ResizeObserver`.
|
|
30
|
+
|
|
31
|
+
For a test that needs several frames, [`frames(count?)`](../test/index.md) is the same idea with a count.
|