@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,229 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
A component is a class that extends `Base`, a name in the registry, and a `data-component` attribute in the markup. All three are needed, and nothing else is.
|
|
4
|
+
|
|
5
|
+
## Declaring a component
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
9
|
+
|
|
10
|
+
class Counter extends Base {
|
|
11
|
+
static config = {
|
|
12
|
+
name: 'Counter',
|
|
13
|
+
refs: ['output'],
|
|
14
|
+
options: { step: { type: Number, default: 1 } },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
count = 0;
|
|
18
|
+
|
|
19
|
+
onClick() {
|
|
20
|
+
this.count += this.$options.step;
|
|
21
|
+
this.$refs.output.textContent = String(this.count);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
registerComponent(Counter);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<button data-component="Counter" data-option-step="5">
|
|
30
|
+
clicked
|
|
31
|
+
<span data-ref="output">0</span>
|
|
32
|
+
times
|
|
33
|
+
</button>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`config.name` is the name the registry uses and the token the markup writes. It is also what gives each instance its [`$id`](../../api/instance-properties.md#id).
|
|
37
|
+
|
|
38
|
+
## Registering
|
|
39
|
+
|
|
40
|
+
`registerComponent(ComponentClass)` puts one name in the registry and scans the document for it. `registerComponents(...classes)` does the same for several:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { Base, registerComponents } from '@studiometa/js-toolkit';
|
|
44
|
+
|
|
45
|
+
class Accordion extends Base {
|
|
46
|
+
static config = { name: 'Accordion' };
|
|
47
|
+
}
|
|
48
|
+
class Slider extends Base {
|
|
49
|
+
static config = { name: 'Slider' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
registerComponents(Accordion, Slider);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**There is no `createApp()`**
|
|
56
|
+
|
|
57
|
+
v4 has no root component and no application object. A component is registered, not mounted by a parent, so nothing has to own the page. Register the components a page uses and the registry does the rest.
|
|
58
|
+
|
|
59
|
+
**One name gives one entry**, as with `customElements.define()`. A second registration under a name already taken gives a `registry.conflict` warning and is ignored.
|
|
60
|
+
|
|
61
|
+
## Several components on one element
|
|
62
|
+
|
|
63
|
+
`data-component` holds a whitespace-separated token list, and each token gets its own instance:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<a href="/next" data-component="Action Analytics Prefetch">Next</a>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The three instances are independent. They share the element and nothing else.
|
|
70
|
+
|
|
71
|
+
## Nesting
|
|
72
|
+
|
|
73
|
+
Children are not constructed by their parent. A nested `data-component` is discovered by the same registry, on its own:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<div data-component="Accordion">
|
|
77
|
+
<div data-component="AccordionItem">…</div>
|
|
78
|
+
<div data-component="AccordionItem">…</div>
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
What the parent declares in `config.components` is a **family**, and it does two jobs:
|
|
83
|
+
|
|
84
|
+
1. registering that family when the parent registers, so one `registerComponent()` call covers a whole tree;
|
|
85
|
+
2. giving the name set that [`on<Child><Event>`](working-with-events.md#child-events) resolution needs.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
89
|
+
|
|
90
|
+
class AccordionItem extends Base {
|
|
91
|
+
static config = { name: 'AccordionItem' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class Accordion extends Base {
|
|
95
|
+
static config = {
|
|
96
|
+
name: 'Accordion',
|
|
97
|
+
components: { AccordionItem },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Registers `Accordion` *and* `AccordionItem`.
|
|
102
|
+
registerComponent(Accordion);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A value can also be a thunk, which makes the child its own chunk:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
static config = {
|
|
109
|
+
name: 'Accordion',
|
|
110
|
+
components: {
|
|
111
|
+
AccordionItem: () => import('./AccordionItem.js'),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The key supplies the name, so the registry knows `AccordionItem` with nothing downloaded. See [Autoloading](../going-further/autoloading.md).
|
|
117
|
+
|
|
118
|
+
**`config.components` is not ownership**
|
|
119
|
+
|
|
120
|
+
Declaring a child does not make the parent construct it, does not make the parent's unmount unmount it, and does not restrict where the child may appear. Nesting is DOM ancestry, always.
|
|
121
|
+
|
|
122
|
+
## Finding other components
|
|
123
|
+
|
|
124
|
+
There is no `$parent` and no `$children`. A component that needs to reach another one has four channels, in order of preference:
|
|
125
|
+
|
|
126
|
+
| Direction | Use |
|
|
127
|
+
| ----------------------- | -------------------------------------------------------------------------------- |
|
|
128
|
+
| parent → children | [`$watchChildren()`](../../api/instance-methods.md#watchchildren) or `$query(name)` |
|
|
129
|
+
| child → parent | [`$emit()`](../../api/instance-methods.md#emit) and the parent's `on<Child><Event>` |
|
|
130
|
+
| ancestor lookup, ad hoc | [`$closest(name)`](../../api/instance-methods.md#closest) — guard the result |
|
|
131
|
+
| shared state either way | [provide/inject](../going-further/sharing-state.md) |
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
135
|
+
|
|
136
|
+
class Accordion extends Base {
|
|
137
|
+
static config = { name: 'Accordion', components: {} };
|
|
138
|
+
|
|
139
|
+
// A live collection, in document order, kept for the life of this instance.
|
|
140
|
+
items = this.$watchChildren('AccordionItem');
|
|
141
|
+
|
|
142
|
+
closeAll() {
|
|
143
|
+
for (const item of this.items) item.$el.removeAttribute('data-option-open');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Page-wide lookups
|
|
149
|
+
|
|
150
|
+
Outside a component, four functions answer from the DOM. They keep no registry of instances:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import {
|
|
154
|
+
getInstance,
|
|
155
|
+
getInstances,
|
|
156
|
+
getMountedInstances,
|
|
157
|
+
getUnmountedInstances,
|
|
158
|
+
} from '@studiometa/js-toolkit';
|
|
159
|
+
|
|
160
|
+
const section = document.querySelector('section')!;
|
|
161
|
+
|
|
162
|
+
getMountedInstances('Dialog'); // the live ones — safe to call a method on
|
|
163
|
+
getInstances('Dialog', section); // every one built in a region
|
|
164
|
+
getUnmountedInstances('Dialog'); // built, then stood down
|
|
165
|
+
getInstance(section, 'Dialog'); // the one on this element, mounted or not
|
|
166
|
+
getInstances(section); // everything on one element
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
See the [Registry reference](../../api/registry/index.md) for the full contract of each.
|
|
170
|
+
|
|
171
|
+
## Extending a component
|
|
172
|
+
|
|
173
|
+
There is no `withExtraConfig()`. To extend a component with a different config, declare a class. `$config` walks the prototype chain, so `refs`, `options` and `components` merge:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import { Base, registerComponent } from '@studiometa/js-toolkit';
|
|
177
|
+
|
|
178
|
+
class AbstractControl extends Base {
|
|
179
|
+
static config = {
|
|
180
|
+
name: 'AbstractControl',
|
|
181
|
+
refs: ['button'],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class NavigationControl extends AbstractControl {
|
|
186
|
+
static config = {
|
|
187
|
+
name: 'NavigationControl',
|
|
188
|
+
// `refs` merges: ['button', 'compass']
|
|
189
|
+
refs: ['compass'],
|
|
190
|
+
options: { showCompass: Boolean },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
registerComponent(NavigationControl);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
To extend a class you cannot edit, do it in expression position:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
registerComponent(
|
|
201
|
+
class extends Vendor {
|
|
202
|
+
static config = { name: 'CompactVendor', options: { compact: Boolean } };
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## When a component mounts
|
|
208
|
+
|
|
209
|
+
By default an instance mounts as soon as its element enters the document. `config.mountStrategy` and the `data-mount` attribute change that:
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<div data-component="Map" data-mount="visible"></div>
|
|
213
|
+
<div data-component="Chat" data-mount="interaction:page"></div>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A component that waits has **no instance**: it is invisible to `$query()`, `$closest()`, `$watchChildren()` and `getInstances()`, and it announces nothing. See [Mount strategies](../going-further/mount-strategies.md).
|
|
217
|
+
|
|
218
|
+
## Responsive declarations
|
|
219
|
+
|
|
220
|
+
`data-component` takes one breakpoint-scoped companion, so a component can exist at some widths and not others:
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<div
|
|
224
|
+
data-component="Action Analytics"
|
|
225
|
+
data-component:xxs="MobileMenu"
|
|
226
|
+
data-component:m="DesktopMenu"></div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The unconditional set is always active. The scoped set is resolved by walking from the widest active suffix down to the first attribute present, and it **replaces** rather than merges. A name that stops being declared is unmounted and dropped from the element; a crossing back builds a new instance. See [`data-component`](../../api/html/data-component.md).
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Options
|
|
2
|
+
|
|
3
|
+
An option is a value a component reads from its element. It is declared in `config.options` and written in the markup as `data-option-<name>`.
|
|
4
|
+
|
|
5
|
+
Two rules carry everything else on this page:
|
|
6
|
+
|
|
7
|
+
> **An option is an input, never a store.**
|
|
8
|
+
>
|
|
9
|
+
> **Every option is responsive. There is nothing to declare for it.**
|
|
10
|
+
|
|
11
|
+
## Declaring options
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
15
|
+
|
|
16
|
+
class Slider extends Base {
|
|
17
|
+
static config = {
|
|
18
|
+
name: 'Slider',
|
|
19
|
+
options: {
|
|
20
|
+
// The short form: just the type.
|
|
21
|
+
label: String,
|
|
22
|
+
// The long form: a type and a default.
|
|
23
|
+
speed: { type: Number, default: 1 },
|
|
24
|
+
loop: { type: Boolean, default: true },
|
|
25
|
+
// Array and Object defaults must be factory functions.
|
|
26
|
+
tween: { type: Object, default: () => ({ ease: 'linear' }) },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
mounted() {
|
|
31
|
+
console.log(this.$options.speed);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<div data-component="Slider" data-option-speed="2" data-option-label="Gallery"></div>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The attribute name is the option name in kebab case: `dragThreshold` is `data-option-drag-threshold`.
|
|
41
|
+
|
|
42
|
+
## `$options` is a read-only view
|
|
43
|
+
|
|
44
|
+
`buildOptions()` defines every property with a **getter and no setter**. The value is derived from the element and the viewport on each access, and nothing is written in. An assignment throws, because a module is strict code:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
this.$options.open = true;
|
|
48
|
+
// TypeError: Cannot set property open of #<Object> which has only a getter
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Two idioms replace the write, and which one you want depends on what the value is.
|
|
52
|
+
|
|
53
|
+
**The DOM is meant to change** — write the attribute. It is the same statement the markup makes:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
57
|
+
|
|
58
|
+
class Panel extends Base {
|
|
59
|
+
static config = { name: 'Panel', options: { disabled: Boolean } };
|
|
60
|
+
|
|
61
|
+
enable() {
|
|
62
|
+
this.$el.removeAttribute('data-option-disabled');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disable() {
|
|
66
|
+
this.$el.setAttribute('data-option-disabled', '');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The next read gives the new value, and [`option<Name>Changed()`](#live-effects) announces it to anyone listening.
|
|
72
|
+
|
|
73
|
+
**The value was never an input** — keep a private field seeded from the option. The attribute says where the component starts; the field carries where it has got to:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
77
|
+
|
|
78
|
+
class Carousel extends Base {
|
|
79
|
+
static config = { name: 'Carousel', options: { start: { type: Number, default: 0 } } };
|
|
80
|
+
|
|
81
|
+
#index = this.$options.start;
|
|
82
|
+
|
|
83
|
+
next() {
|
|
84
|
+
this.#index += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
An option a component only reads keeps neither: it reads `this.$options.x` where it needs it.
|
|
90
|
+
|
|
91
|
+
## Defaults
|
|
92
|
+
|
|
93
|
+
**A primitive can be a default. Every other data type needs a factory function.**
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
options: {
|
|
97
|
+
speed: { type: Number, default: 1 }, // fine
|
|
98
|
+
tween: { type: Object, default: () => ({ ease: 'linear' }) }, // required form
|
|
99
|
+
items: { type: Array, default: () => [] }, // required form
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `Function` is not an option type, so a `default` that is a function is always a factory.
|
|
104
|
+
- A default is built **once per instance** and then kept. Two instances never share one default.
|
|
105
|
+
- A factory is **lazy**. Nothing is built for an option nobody reads, and a component whose attribute is present never runs its factory.
|
|
106
|
+
- `Array` and `Object` with no declared default get an empty value per instance.
|
|
107
|
+
- A literal object or array default gives one `option.literal-default` warning, naming the component, the option and the correction. The value is then used as declared and **shared** between instances.
|
|
108
|
+
|
|
109
|
+
A default is the one option value that lives on the instance, so mutating the object a factory built sticks — `this.$options.tween.ease = 'ease-out'` is kept, for that instance. Replacing the option itself does not: `this.$options.tween = {}` throws, like every other write to the view.
|
|
110
|
+
|
|
111
|
+
## Boolean options read presence
|
|
112
|
+
|
|
113
|
+
`data-option-open` is on, an absent attribute is the declared default, and the string the attribute carries is **never read** — the way `disabled` and `checked` work on the platform:
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<div data-component="Panel" data-option-open></div>
|
|
117
|
+
<!-- true -->
|
|
118
|
+
<div data-component="Panel" data-option-open="false"></div>
|
|
119
|
+
<!-- true — the value says nothing -->
|
|
120
|
+
<div data-component="Panel"></div>
|
|
121
|
+
<!-- the declared default -->
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Update one from code by adding or removing the attribute:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
flag ? el.setAttribute('data-option-open', '') : el.removeAttribute('data-option-open');
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A template must therefore write the attribute conditionally rather than interpolate a boolean into it:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
{# Correct #}
|
|
134
|
+
<div data-component="Panel" {% if isOpen %}data-option-open{% endif %}></div>
|
|
135
|
+
|
|
136
|
+
{# Wrong — always true #}
|
|
137
|
+
<div data-component="Panel" data-option-open="{{ isOpen }}"></div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Turning one off — `data-option-no-<name>`
|
|
141
|
+
|
|
142
|
+
A boolean option is turned off by an attribute that only has to be there:
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<div data-component="Dialog" data-option-no-trap-focus></div>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- It is how an option declared `default: true` is turned off, since removing an attribute that is not there says nothing.
|
|
149
|
+
- **Only an option that can hold `false` has one** — a declared `Boolean`, or a union containing it. A `String` option has nothing to turn off, so `data-option-no-label` is not an attribute and the observer never watches for it.
|
|
150
|
+
- **Presence is the whole statement.** `data-option-no-x="false"` is not a double negative; it is the same flag.
|
|
151
|
+
- It is responsive like every other spelling.
|
|
152
|
+
- An option whose own name starts with `no` negates independently: `noSort` owns `data-option-no-sort`, and its off spelling doubles the prefix, `data-option-no-no-sort`.
|
|
153
|
+
|
|
154
|
+
## Several types
|
|
155
|
+
|
|
156
|
+
An option can accept several types. Declare the constructors in order:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
160
|
+
|
|
161
|
+
class Sticky extends Base {
|
|
162
|
+
static config = {
|
|
163
|
+
name: 'Sticky',
|
|
164
|
+
options: {
|
|
165
|
+
// "10" reads as a number; "[10, 20]" reads as an array.
|
|
166
|
+
offset: [Number, Array],
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each parser must give a value of its declared type before the next parser runs. An absent union option uses its declared default, or the empty value of its first type.
|
|
173
|
+
|
|
174
|
+
## Responsive options
|
|
175
|
+
|
|
176
|
+
Every option takes a breakpoint suffix, with no flag to declare:
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<div
|
|
180
|
+
data-component="Grid"
|
|
181
|
+
data-option-columns="1"
|
|
182
|
+
data-option-columns:s="2"
|
|
183
|
+
data-option-columns:l="4"></div>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **The suffix names one breakpoint and it cascades upwards.** `$options.columns` walks from the active breakpoint down to the base value and gives the first attribute present. v3 spelled a set (`:xs:s`); v4 does not.
|
|
187
|
+
- **The separator is a colon**, because an option name in kebab case can contain a dash.
|
|
188
|
+
- **The value is derived on read.** Nothing is stored and nothing is written.
|
|
189
|
+
- A suffix naming no configured breakpoint gives one `responsive.unknown-breakpoint` warning per mount.
|
|
190
|
+
- A `matchMedia` subscription opens **only** for a component that declares `option<Name>Changed()`. A page that only reads options holds no listener.
|
|
191
|
+
|
|
192
|
+
See [`setBreakpoints()`](../../api/dom/breakpoints.md) to replace the named set.
|
|
193
|
+
|
|
194
|
+
## Live effects {#live-effects}
|
|
195
|
+
|
|
196
|
+
A declared method named `option<Name>Changed()` makes that option a live effect. It is how an option that chooses a resource stays correct:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
200
|
+
|
|
201
|
+
function connect(url) {
|
|
202
|
+
return { dispose() {} };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class Feed extends Base {
|
|
206
|
+
static config = { name: 'Feed', options: { source: String } };
|
|
207
|
+
|
|
208
|
+
optionSourceChanged({ value, previousValue, initial }) {
|
|
209
|
+
const connection = connect(value);
|
|
210
|
+
// The returned function is the cleanup for *this* value.
|
|
211
|
+
return () => connection.dispose();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- The hook runs **before `mounted()`** on each mount cycle, with `initial: true`.
|
|
217
|
+
- Several writes in one mutation batch give **one** change, from the first old raw value to the final DOM value. A write that ends where it started is not a change.
|
|
218
|
+
- The previous cleanup runs before an update. Every active cleanup runs on `$unmount()`.
|
|
219
|
+
- Removal of the attribute applies the declared default.
|
|
220
|
+
- A breakpoint crossing reports through the same hook. A crossing to the same resolved value announces nothing.
|
|
221
|
+
- A component without the convention pays no setup cost.
|
|
222
|
+
|
|
223
|
+
The payload is an [`OptionChange`](../../api/methods-hooks-options.md):
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { Base, type OptionChange } from '@studiometa/js-toolkit';
|
|
227
|
+
|
|
228
|
+
class Feed extends Base {
|
|
229
|
+
static config = { name: 'Feed', options: { source: String } };
|
|
230
|
+
|
|
231
|
+
optionSourceChanged({ value, previousValue, rawValue, initial }: OptionChange<string>) {
|
|
232
|
+
console.log(value, previousValue, rawValue, initial);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Refs
|
|
2
|
+
|
|
3
|
+
A ref is an element of a component's own markup, named in the HTML with `data-ref` and declared in `config.refs`.
|
|
4
|
+
|
|
5
|
+
**Refs are live.** Each `$refs` property reads the DOM on access, so markup put into a component is found with no refresh, and no detached element stays in a list. There is no `$update()`.
|
|
6
|
+
|
|
7
|
+
## A single ref
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-component="Dialog">
|
|
11
|
+
<button data-ref="close">Close</button>
|
|
12
|
+
</div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
17
|
+
|
|
18
|
+
class Dialog extends Base {
|
|
19
|
+
static config = {
|
|
20
|
+
name: 'Dialog',
|
|
21
|
+
refs: ['close'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
mounted() {
|
|
25
|
+
this.$refs.close.focus();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onCloseClick() {
|
|
29
|
+
this.$el.removeAttribute('data-option-open');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A plain declaration selects `[data-ref="close"]` and gives the **first** match.
|
|
35
|
+
|
|
36
|
+
## A list of refs
|
|
37
|
+
|
|
38
|
+
**A list ref keeps the `[]` in the attribute.** The suffix is part of the name, in the config and in the markup:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<ul data-component="Tabs">
|
|
42
|
+
<li><button data-ref="tabs[]">One</button></li>
|
|
43
|
+
<li><button data-ref="tabs[]">Two</button></li>
|
|
44
|
+
</ul>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
49
|
+
|
|
50
|
+
class Tabs extends Base {
|
|
51
|
+
static config = {
|
|
52
|
+
name: 'Tabs',
|
|
53
|
+
refs: ['tabs[]'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
onTabsClick({ index }) {
|
|
57
|
+
console.log(`tab ${index} was clicked`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`$refs.tabs` is an array. The property name never carries the suffix — `tabs[]` in the declaration, `$refs.tabs` everywhere else.
|
|
63
|
+
|
|
64
|
+
**The two spellings must agree**
|
|
65
|
+
|
|
66
|
+
`config.refs: ['tabs[]']` matches `data-ref="tabs[]"` and nothing else. The opposite mistake — the suffix missing from the attribute — gives one `ref.mismatch` warning per instance and per ref, naming the component and both spellings.
|
|
67
|
+
|
|
68
|
+
## Ref boundaries
|
|
69
|
+
|
|
70
|
+
By default a ref belongs to the **nearest enclosing component**. A ref inside a nested component is that component's, not yours:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<div data-component="Slider">
|
|
74
|
+
<button data-ref="next">Slider's own</button>
|
|
75
|
+
<div data-component="SliderItem">
|
|
76
|
+
<button data-ref="next">SliderItem's, not Slider's</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Naming the owner
|
|
82
|
+
|
|
83
|
+
A ref can name the component it belongs to, and then it crosses boundaries:
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<div data-component="Slider">
|
|
87
|
+
<div data-component="SliderItem">
|
|
88
|
+
<button data-ref="Slider.next">Slider's, from inside a child</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`Slider.next` passes every boundary **except** another `Slider`, so the nearest `Slider` wins and a nested `Slider` shadows its parent.
|
|
94
|
+
|
|
95
|
+
The namespace is written in the markup only, never in `config.refs`, and the name elsewhere never carries it:
|
|
96
|
+
|
|
97
|
+
| markup | `config.refs` | property | handler | decorator |
|
|
98
|
+
| -------------------------- | ------------- | ------------ | ------------- | ------------------ |
|
|
99
|
+
| `data-ref="next"` | `'next'` | `$refs.next` | `onNextClick` | `@on('next', …)` |
|
|
100
|
+
| `data-ref="Slider.next"` | `'next'` | `$refs.next` | `onNextClick` | `@on('next', …)` |
|
|
101
|
+
| `data-ref="dots[]"` | `'dots[]'` | `$refs.dots` | `onDotsClick` | `@on('dots[]', …)` |
|
|
102
|
+
| `data-ref="Slider.dots[]"` | `'dots[]'` | `$refs.dots` | `onDotsClick` | `@on('dots[]', …)` |
|
|
103
|
+
|
|
104
|
+
The namespace goes **before** the suffix: `Component.name[]`.
|
|
105
|
+
|
|
106
|
+
## Event handlers
|
|
107
|
+
|
|
108
|
+
`on<Ref><Event>` handlers are **delegated from the root element**, so a ref that appears later needs no new binding:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
112
|
+
|
|
113
|
+
class Todo extends Base {
|
|
114
|
+
static config = {
|
|
115
|
+
name: 'Todo',
|
|
116
|
+
refs: ['items[]', 'input'],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Fires for any `items[]` ref, including the ones added after mount.
|
|
120
|
+
onItemsClick({ event, target, index }) {
|
|
121
|
+
console.log(index, target);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onInputInput({ target }) {
|
|
125
|
+
console.log(target.value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The payload is `{ event, target, index }`:
|
|
131
|
+
|
|
132
|
+
- `event` — the DOM event.
|
|
133
|
+
- `target` — the ref element the handler matched, **not** `event.target`.
|
|
134
|
+
- `index` — the position in the list, or `0` for a single ref.
|
|
135
|
+
|
|
136
|
+
Events that do not bubble — `focus`, `blur`, `scroll`, `mouseenter`, `mouseleave` — are delegated from the **capture** phase, so they are heard all the same.
|
|
137
|
+
|
|
138
|
+
In TypeScript, annotate the payload with `RefEvent`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Base, type RefEvent } from '@studiometa/js-toolkit';
|
|
142
|
+
|
|
143
|
+
class Todo extends Base {
|
|
144
|
+
static config = { name: 'Todo', refs: ['input'] };
|
|
145
|
+
|
|
146
|
+
onInputInput({ target }: RefEvent<HTMLInputElement>) {
|
|
147
|
+
console.log(target.value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## How the live read works
|
|
153
|
+
|
|
154
|
+
Ref lookups are cached, and the cache is invalidated by a counter that the framework's own MutationObserver increases. Reading that counter drains the pending records with `takeRecords()`, so the read is current even inside the same task as the mutation. Detached elements are never cached.
|
|
155
|
+
|
|
156
|
+
The practical consequence: you never refresh anything.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { Base } from '@studiometa/js-toolkit';
|
|
160
|
+
|
|
161
|
+
class Todo extends Base {
|
|
162
|
+
static config = { name: 'Todo', refs: ['list', 'items[]'] };
|
|
163
|
+
|
|
164
|
+
add(title) {
|
|
165
|
+
const li = document.createElement('li');
|
|
166
|
+
li.dataset.ref = 'items[]';
|
|
167
|
+
li.textContent = title;
|
|
168
|
+
this.$refs.list.append(li);
|
|
169
|
+
|
|
170
|
+
// Already there. No `$update()`, no re-query.
|
|
171
|
+
console.log(this.$refs.items.length);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|