microtags 0.0.8 → 0.0.11
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 +465 -168
- package/dist/builders.d.ts.map +1 -1
- package/dist/{chunk-P4QHFKTJ.js → chunk-FZKCMDSS.js} +4 -4
- package/dist/{chunk-P4QHFKTJ.js.map → chunk-FZKCMDSS.js.map} +2 -2
- package/dist/index.cjs +31 -31
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -31
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/meta.json +18 -18
- package/dist/setup-context.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util.cjs +3 -3
- package/dist/util.cjs.map +2 -2
- package/dist/util.d.ts +2 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +3 -3
- package/dist/util.min.js +1 -1
- package/dist/util.min.js.map +3 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://bundlephobia.com/package/microtags)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
Web component helper with reactive props, automatic cleanup, and
|
|
13
|
-
[Shadow DOM](https://gomakethings.com/the-shadow-dom-is-an-antipattern/).
|
|
12
|
+
Web component helper with reactive props, automatic cleanup, and
|
|
13
|
+
[no Shadow DOM](https://gomakethings.com/the-shadow-dom-is-an-antipattern/).
|
|
14
14
|
Reactive via [alien-signals](https://github.com/stackblitz/alien-signals).
|
|
15
15
|
|
|
16
|
-
**Size: 3.
|
|
16
|
+
**Size: <!-- size -->3.48 KB<!-- /size -->** with all dependencies, minified and brotlied
|
|
17
17
|
|
|
18
18
|
Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
19
19
|
**alien-signals** instead of nanostores atoms.
|
|
@@ -25,21 +25,31 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
25
25
|
- [Install](#install)
|
|
26
26
|
* [Import map](#import-map)
|
|
27
27
|
* [Pre-bundled](#pre-bundled)
|
|
28
|
-
- [
|
|
28
|
+
- [Get Started](#get-started)
|
|
29
|
+
* [1. Call `.define`](#1-call-define)
|
|
30
|
+
* [2. Call the `.with*` Methods](#2-call-the-with-methods)
|
|
31
|
+
* [3. Call `.setup`](#3-call-setup)
|
|
32
|
+
- [Full Example](#full-example)
|
|
33
|
+
- [Validation](#validation)
|
|
34
|
+
* [Prop validation](#prop-validation)
|
|
35
|
+
+ [`p.schema` vs Standard Schema](#pschema-vs-standard-schema)
|
|
29
36
|
* [Client-side validation](#client-side-validation)
|
|
30
37
|
- [Serverside Example](#serverside-example)
|
|
31
38
|
* [`.TAG`](#tag)
|
|
32
|
-
* [`.refs`](#refs)
|
|
33
39
|
- [API](#api)
|
|
34
|
-
* [Props](#props)
|
|
35
|
-
+ [`withProps`](#withprops)
|
|
36
|
-
* [Refs](#refs)
|
|
37
|
-
+ [`withRefs`](#withrefs)
|
|
38
|
-
+ [Element types](#element-types)
|
|
39
40
|
* [`define`](#define)
|
|
40
|
-
|
|
41
|
-
* [`
|
|
42
|
-
+ [
|
|
41
|
+
+ [`ComponentBuilder`](#componentbuilder)
|
|
42
|
+
* [`builder.withProps`](#builderwithprops)
|
|
43
|
+
+ [`p.json`](#pjson)
|
|
44
|
+
+ [`p.schema`](#pschema)
|
|
45
|
+
* [`builder.withRefs(fn)`](#builderwithrefsfn)
|
|
46
|
+
* [`builder.withContexts`](#builderwithcontexts)
|
|
47
|
+
+ [`.withContexts` Example](#withcontexts-example)
|
|
48
|
+
* [`builder.setup(ctx)`](#buildersetupctx)
|
|
49
|
+
+ [`ctx`](#ctx)
|
|
50
|
+
- [Missing refs](#missing-refs)
|
|
51
|
+
* [Checking refs in tests](#checking-refs-in-tests)
|
|
52
|
+
- [Typed Events](#typed-events)
|
|
43
53
|
- [Templates](#templates)
|
|
44
54
|
- [Subpath Exports](#subpath-exports)
|
|
45
55
|
* [`microtags/context`](#microtagscontext)
|
|
@@ -48,14 +58,14 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
48
58
|
+ [`render`](#render)
|
|
49
59
|
* [`microtags/util`](#microtagsutil)
|
|
50
60
|
+ [Coercion](#coercion)
|
|
51
|
-
+ [`
|
|
61
|
+
+ [`runAll`](#runall)
|
|
52
62
|
+ [`toAttributes`](#toattributes)
|
|
53
|
-
- [
|
|
63
|
+
- [Differences from `nanotags`](#differences-from-nanotags)
|
|
54
64
|
* [Reactivity primitive](#reactivity-primitive)
|
|
55
65
|
* [Effects + Subscriptions](#effects--subscriptions)
|
|
56
66
|
* [Refs: `r.all` instead of `r.many`](#refs-rall-instead-of-rmany)
|
|
57
67
|
* [Context API](#context-api)
|
|
58
|
-
* [Props](#props
|
|
68
|
+
* [Props](#props)
|
|
59
69
|
* [Not ported](#not-ported)
|
|
60
70
|
* [Additions / New Features](#additions--new-features)
|
|
61
71
|
|
|
@@ -63,9 +73,7 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
63
73
|
|
|
64
74
|
</details>
|
|
65
75
|
|
|
66
|
-
----------------------------------------------------------------------
|
|
67
76
|
## Install
|
|
68
|
-
----------------------------------------------------------------------
|
|
69
77
|
|
|
70
78
|
```sh
|
|
71
79
|
npm i -S microtags
|
|
@@ -96,10 +104,8 @@ Subpaths follow the same URL pattern: `microtags/context`, `microtags/render`.
|
|
|
96
104
|
|
|
97
105
|
### Pre-bundled
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
single self-contained file with `alien-signals` inlined, so no other
|
|
102
|
-
mappings are needed:
|
|
107
|
+
This package exposes `microtags/min`, which is a single self-contained file
|
|
108
|
+
with `alien-signals` inlined.
|
|
103
109
|
|
|
104
110
|
```html
|
|
105
111
|
<script type="importmap">
|
|
@@ -111,23 +117,89 @@ mappings are needed:
|
|
|
111
117
|
</script>
|
|
112
118
|
```
|
|
113
119
|
|
|
120
|
+
## Get Started
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
## Example
|
|
117
|
-
----------------------------------------------------------------------
|
|
122
|
+
### 1. Call `.define`
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
DOM inside `.setup`.
|
|
124
|
+
```ts
|
|
125
|
+
import { define } from 'microtags'
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
const myCounter = define('my-counter')
|
|
128
|
+
```
|
|
125
129
|
|
|
126
130
|
---
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
### 2. Call the `.with*` Methods
|
|
133
|
+
|
|
134
|
+
Methods `withProps`, `withRefs`, and `withContexts` are optional and can appear
|
|
135
|
+
in any order. `.setup` ends the chain. It calls `customElements.define` under
|
|
136
|
+
the hood and returns a typed constructor.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
myCounter
|
|
140
|
+
.withProps(p => ({
|
|
141
|
+
start: p.number(), // the starting count (an attribute)
|
|
142
|
+
}))
|
|
143
|
+
.withRefs(r => ({ // keys in this object are the `data-ref` names in HTML
|
|
144
|
+
display: r.one<HTMLDivElement>(), // typed as HTMLDivElement,
|
|
145
|
+
inc: r.one(), // depends on a child in HTML with `data-ref="inc"`
|
|
146
|
+
dec: r.one(), // depends on a child in HTML with `data-ref="dec"`
|
|
147
|
+
copy: r.one(), // the <copy-btn> element, via `data-ref="copy"`
|
|
148
|
+
}))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. Call `.setup`
|
|
152
|
+
|
|
153
|
+
Method `.setup` takes a function that gets called with `context` object.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
myCounter
|
|
157
|
+
.setup(ctx => {
|
|
158
|
+
const count = signal(ctx.props.start())
|
|
159
|
+
// `ctx.refs` are expected to be HTML elements with a matching `data-ref`
|
|
160
|
+
const display = ctx.refs.display
|
|
161
|
+
|
|
162
|
+
// whenever `count` changes
|
|
163
|
+
ctx.effect(() => {
|
|
164
|
+
display.textContent = `Count: ${count()}`
|
|
165
|
+
display.style.color = count() < 0 ? 'red' : 'inherit'
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Reflect the live count onto the copy button's `value` attribute.
|
|
169
|
+
// Because `value` is attribute-backed, this flows into <copy-btn>'s
|
|
170
|
+
// own signal, so it always copies the current count.
|
|
171
|
+
ctx.effect(() => {
|
|
172
|
+
const copy = ctx.refs.copy
|
|
173
|
+
copy.setAttribute('value', String(count()))
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Listen for clicks.
|
|
178
|
+
*/
|
|
179
|
+
ctx.on(ctx.refs.inc, 'click', () => {
|
|
180
|
+
count(count() + 1)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
ctx.on(ctx.refs.dec, 'click', () => {
|
|
184
|
+
count(count() - 1)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Full Example
|
|
190
|
+
|
|
191
|
+
This example expects the full HTML to already be in the DOM.
|
|
192
|
+
Defining the components is about "hydrating" the page, or adding behavior.
|
|
193
|
+
|
|
194
|
+
If you want to client-side render the components, that is possible too.
|
|
195
|
+
You can update the DOM inside `.setup`.
|
|
196
|
+
|
|
197
|
+
>
|
|
198
|
+
> [!NOTE]
|
|
199
|
+
> The server should know how to create the markup that components depend
|
|
200
|
+
> on here. See [the serverside example](#serverside-example).
|
|
201
|
+
>
|
|
202
|
+
|
|
131
203
|
|
|
132
204
|
```ts
|
|
133
205
|
import { define } from 'microtags'
|
|
@@ -135,7 +207,7 @@ import { signal } from 'alien-signals'
|
|
|
135
207
|
|
|
136
208
|
/**
|
|
137
209
|
* - attribute-backed prop (`start`)
|
|
138
|
-
* - data-ref elements
|
|
210
|
+
* - data-ref elements -- each needs a data-ref attribute, eg data-ref="inc"
|
|
139
211
|
* - ctx.effect, ctx.on
|
|
140
212
|
*/
|
|
141
213
|
export const myCounter = define('my-counter')
|
|
@@ -184,25 +256,102 @@ export const myCounter = define('my-counter')
|
|
|
184
256
|
</my-counter>
|
|
185
257
|
```
|
|
186
258
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
See [example/subscribe-form.ts](./example/subscribe-form.ts).
|
|
259
|
+
## Validation
|
|
190
260
|
|
|
191
|
-
There are two separate things people call "validation"
|
|
192
|
-
handles them in different places:
|
|
261
|
+
There are two separate things people call "validation":
|
|
193
262
|
|
|
194
|
-
1. **Prop validation** checks the attributes passed
|
|
263
|
+
1. **Prop validation** checks the attributes passed into a component.
|
|
195
264
|
Pass a [Standard Schema](https://standardschema.dev) (Zod, Valibot,
|
|
196
265
|
ArkType) to [`p.schema`](#withprops); it coerces the attribute value
|
|
197
|
-
and falls back to `undefined` when the value doesn't
|
|
198
|
-
2. **Form input validation** checks live user input
|
|
199
|
-
value that changes on every keystroke
|
|
266
|
+
and falls back to `undefined` when the value doesn't coerce.
|
|
267
|
+
2. **Form input validation** -- client-side validation -- checks live user input
|
|
268
|
+
in a form, for example a value that changes on every keystroke.
|
|
200
269
|
There is no dedicated API for this. Validate the schema directly
|
|
201
|
-
inside `.setup()` and hold the result
|
|
270
|
+
inside `.setup()` and hold the result as a signal.
|
|
271
|
+
|
|
272
|
+
### Prop validation
|
|
202
273
|
|
|
203
|
-
|
|
274
|
+
Pass a [Standard Schema](https://standardschema.dev) validator to `p.schema`.
|
|
275
|
+
The validator runs against the raw attribute and its output becomes the
|
|
276
|
+
signal's value; anything it rejects falls back to `undefined`.
|
|
204
277
|
|
|
205
278
|
```ts
|
|
279
|
+
import { z } from 'zod'
|
|
280
|
+
import { define } from 'microtags'
|
|
281
|
+
|
|
282
|
+
// Any Standard Schema validator works (Zod, Valibot, ArkType). microtags
|
|
283
|
+
// only sees the shared `~standard` interface, so it has no validation
|
|
284
|
+
// library of its own and no dependency on the one you pick.
|
|
285
|
+
const Variant = z.enum(['info', 'success', 'warning', 'danger'])
|
|
286
|
+
|
|
287
|
+
define('status-badge')
|
|
288
|
+
.withProps(p => ({
|
|
289
|
+
// Validates the raw `variant` attribute. A value outside the
|
|
290
|
+
// four options falls back to `undefined`.
|
|
291
|
+
variant: p.schema(Variant),
|
|
292
|
+
}))
|
|
293
|
+
.setup(ctx => {
|
|
294
|
+
ctx.effect(() => {
|
|
295
|
+
const variant = ctx.props.variant() ?? 'info'
|
|
296
|
+
ctx.host.className = `badge badge-${variant}`
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<status-badge variant="success">Saved</status-badge>
|
|
303
|
+
|
|
304
|
+
<!-- "purple" is not in the enum, so ctx.props.variant() is undefined -->
|
|
305
|
+
<status-badge variant="purple">Unknown</status-badge>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `p.schema` vs Standard Schema
|
|
309
|
+
|
|
310
|
+
[Standard Schema](https://standardschema.dev) is a small shared interface
|
|
311
|
+
that Zod, Valibot and ArkType all implement. `p.schema` validates through that
|
|
312
|
+
interface, so microtags ships no validator of its own.
|
|
313
|
+
|
|
314
|
+
A schema prop is attribute-backed and observed, exactly like `p.string()` or
|
|
315
|
+
`p.number()`: editing the attribute re-runs the validator and pushes the result
|
|
316
|
+
to `ctx.props`.
|
|
317
|
+
|
|
318
|
+
The validator's input is the raw attribute string, or `null` when the attribute
|
|
319
|
+
is absent. Because the input is always a string, schemas for non-string values
|
|
320
|
+
must coerce. Use `z.coerce.number()`, not `z.number()`:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
.withProps(p => ({
|
|
324
|
+
// "5" -> 5; "0" and the absent attribute both fall back to undefined
|
|
325
|
+
quantity: p.schema(z.coerce.number().int().min(1)),
|
|
326
|
+
}))
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**On success the signal holds the validator's output value**; on failure it
|
|
330
|
+
falls back to `undefined`. The inferred signal type is the schema's output,
|
|
331
|
+
so guard for the `undefined` case, or fold a default into the schema so it
|
|
332
|
+
never fails:
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
{
|
|
337
|
+
// always 'info' | 'success' | 'warning' | 'danger', never undefined
|
|
338
|
+
variant: p.schema(Variant.catch('info')),
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Only synchronous schemas are supported; an async schema throws when the
|
|
343
|
+
attribute is validated.
|
|
344
|
+
|
|
345
|
+
-------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
### Client-side validation
|
|
348
|
+
|
|
349
|
+
There is no special API here. Validate input inside `.setup`,
|
|
350
|
+
|
|
351
|
+
See [example/subscribe-form.ts](./example/subscribe-form.ts).
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// form input (client-side) validation
|
|
206
355
|
import { signal, startBatch, endBatch } from 'alien-signals'
|
|
207
356
|
import { z } from 'zod'
|
|
208
357
|
import { define } from 'microtags'
|
|
@@ -219,18 +368,21 @@ define('subscribe-form')
|
|
|
219
368
|
}))
|
|
220
369
|
.setup(ctx => {
|
|
221
370
|
const value = signal('')
|
|
222
|
-
const touched = signal(false)
|
|
371
|
+
const touched = signal<boolean>(false)
|
|
223
372
|
|
|
224
373
|
// capture the live value (two-way; clears the field on reset)
|
|
225
374
|
ctx.bind(value, ctx.refs.input)
|
|
375
|
+
|
|
226
376
|
// only reveal errors after the first blur
|
|
227
377
|
ctx.on(ctx.refs.input, 'blur', () => touched(true))
|
|
228
378
|
|
|
379
|
+
// ctx.effect is alien-signals/effect with automatic/correct disposal
|
|
229
380
|
ctx.effect(() => {
|
|
381
|
+
// subscribe to `value` and `touched`
|
|
230
382
|
const result = Email.safeParse(value())
|
|
231
|
-
const message = result.success ?
|
|
383
|
+
const message = (result.success ?
|
|
232
384
|
'' :
|
|
233
|
-
result.error.issues[0].message
|
|
385
|
+
result.error.issues[0].message)
|
|
234
386
|
const showError = touched() && !result.success
|
|
235
387
|
|
|
236
388
|
ctx.refs.submit.disabled = !result.success
|
|
@@ -239,6 +391,7 @@ define('subscribe-form')
|
|
|
239
391
|
})
|
|
240
392
|
|
|
241
393
|
ctx.on(ctx.refs.form, 'submit', ev => {
|
|
394
|
+
// subscribe to DOM events
|
|
242
395
|
ev.preventDefault()
|
|
243
396
|
if (!Email.safeParse(value()).success) return
|
|
244
397
|
ctx.refs.status.textContent = 'Thanks for subscribing!'
|
|
@@ -250,14 +403,16 @@ define('subscribe-form')
|
|
|
250
403
|
})
|
|
251
404
|
```
|
|
252
405
|
|
|
253
|
-
The error stays hidden until the field is blurred once, then updates live
|
|
254
|
-
the submit button is
|
|
255
|
-
|
|
406
|
+
The error stays hidden until the field is blurred once, then updates live,
|
|
407
|
+
and the submit button is disabled until valid. See `example/subscribe-form.ts`.
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
---------------------------------------------------------------
|
|
256
411
|
|
|
257
412
|
|
|
258
|
-
----------------------------------------------------------------------
|
|
259
413
|
## Serverside Example
|
|
260
|
-
|
|
414
|
+
|
|
415
|
+
See [./example/ssr.ts](./example/ssr.ts).
|
|
261
416
|
|
|
262
417
|
### `.TAG`
|
|
263
418
|
|
|
@@ -265,11 +420,11 @@ Each tag name that you pass to `define` is exposed as a property `.TAG`
|
|
|
265
420
|
on the return value. It is possible to import your web components in Node,
|
|
266
421
|
even though Node does not have browser APIs.
|
|
267
422
|
|
|
268
|
-
|
|
423
|
+
The `data-ref` attribute is a fallback. It's used when you don't pass a
|
|
424
|
+
selector to `r.one()`. If you pass explicit CSS selectors to `withRefs`,
|
|
425
|
+
e.g. `{ copy: r.one('button') }`, then the HTML does not require a `data-ref`
|
|
426
|
+
attribute.
|
|
269
427
|
|
|
270
|
-
All refs are defined per-component, and the ref names are exposed at `.refs`,
|
|
271
|
-
so you can import your web components on the server and read the ref names
|
|
272
|
-
as strings.
|
|
273
428
|
|
|
274
429
|
```ts
|
|
275
430
|
import { MyCounter, CountBtn, CopyBtn } from './index.js'
|
|
@@ -311,11 +466,39 @@ export function render ():string {
|
|
|
311
466
|
}
|
|
312
467
|
```
|
|
313
468
|
|
|
314
|
-
|
|
469
|
+
|
|
470
|
+
---------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
315
473
|
## API
|
|
316
|
-
----------------------------------------------------------------------
|
|
317
474
|
|
|
318
|
-
###
|
|
475
|
+
### `define`
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
function define (tagName:string):ComponentBuilder
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Function `define(tagName)` returns a [`ComponentBuilder`](#componentbuilder).
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
#### `ComponentBuilder`
|
|
485
|
+
|
|
486
|
+
The object returned by [`define`](#define). Includes methods:
|
|
487
|
+
|
|
488
|
+
| Method | Returns | Description |
|
|
489
|
+
|--------|---------|-------------|
|
|
490
|
+
| `.TAG` | `string` | The tag name passed to `define`. See [`.TAG`](#tag). |
|
|
491
|
+
| `.withProps(fn)` | `ComponentBuilder` | Declare reactive attributes. See [Props](#props). |
|
|
492
|
+
| `.withRefs(fn)` | `ComponentBuilder` | Declare DOM refs resolved once the element connects. See [Refs](#refs-1). |
|
|
493
|
+
| `.withContexts(fn)` | `ComponentBuilder` | Consume context values published by an ancestor. See [`withContexts`](#withcontexts). |
|
|
494
|
+
| `.setup(fn)` | `MicrotagElementClass` | Registers the custom element and returns its class. See [`setup`](#setup) below. |
|
|
495
|
+
|
|
496
|
+
Methods `withProps`, `withRefs`, and `withContexts` each return a new builder,
|
|
497
|
+
so they chain in any order; `setup` ends the chain by registering the element.
|
|
498
|
+
|
|
499
|
+
---------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
### `builder.withProps`
|
|
319
502
|
|
|
320
503
|
Declare reactive attributes via `withProps`. Each prop becomes:
|
|
321
504
|
|
|
@@ -331,8 +514,6 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
331
514
|
| `p.boolean()` | `"true"` / `""` → `true`, `"false"` → `false` | `false` |
|
|
332
515
|
| `p.oneOf(opts)` | Picklist enum, throws on invalid | throws |
|
|
333
516
|
|
|
334
|
-
#### `withProps`
|
|
335
|
-
|
|
336
517
|
```ts
|
|
337
518
|
.withProps(p => ({
|
|
338
519
|
count: p.number(), // attribute -> number (NaN when absent)
|
|
@@ -344,7 +525,15 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
344
525
|
}))
|
|
345
526
|
```
|
|
346
527
|
|
|
347
|
-
|
|
528
|
+
Inside `.setup()`, every prop is a callable signal:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
ctx.props.count() // read
|
|
532
|
+
ctx.props.count(42) // write
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
#### `p.json`
|
|
348
537
|
|
|
349
538
|
`p.json<T>()` -- a prop whose attribute value is parsed with
|
|
350
539
|
`JSON.parse`. The type parameter `T` annotates the parsed result; it is a
|
|
@@ -367,6 +556,7 @@ observed, so editing the attribute re-parses and pushes the new value to
|
|
|
367
556
|
attribute is absent, and when the value is not valid JSON. Parse errors are
|
|
368
557
|
swallowed rather than thrown.
|
|
369
558
|
|
|
559
|
+
|
|
370
560
|
```ts
|
|
371
561
|
// check for `undefined` case
|
|
372
562
|
ctx.effect(() => {
|
|
@@ -376,32 +566,46 @@ ctx.effect(() => {
|
|
|
376
566
|
})
|
|
377
567
|
```
|
|
378
568
|
|
|
569
|
+
#### `p.schema`
|
|
570
|
+
|
|
379
571
|
For input you want validated rather than only parsed, pass a Standard Schema
|
|
380
572
|
to [`p.schema`](#withprops) instead.
|
|
381
573
|
|
|
382
|
-
|
|
574
|
+
|
|
575
|
+
---------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
### `builder.withRefs(fn)`
|
|
579
|
+
|
|
580
|
+
Takes a function that gets called with a helpful query selector, `r`.
|
|
581
|
+
|
|
582
|
+
Call `r.one()` / `r.all()` with no argument to match `[data-ref="<key>"]`,
|
|
583
|
+
where `key` is the key name you passed in. Or pass a CSS selector to query by
|
|
584
|
+
it instead. Queries are resolved relative to the host element, during
|
|
585
|
+
[the `connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks).
|
|
586
|
+
|
|
587
|
+
All refs returned from this callback are typed as much as possible, and exposed
|
|
588
|
+
on the `context` object passed to the `.setup` function.
|
|
589
|
+
|
|
383
590
|
|
|
384
591
|
```ts
|
|
385
|
-
|
|
386
|
-
|
|
592
|
+
class ComponentBuilder {
|
|
593
|
+
withRefs<R extends Record<string, RefDef>> (
|
|
594
|
+
fn:(r:RefsDSL) => R
|
|
595
|
+
):ComponentBuilder<Props, R, CtxDefs>
|
|
596
|
+
}
|
|
387
597
|
```
|
|
388
598
|
|
|
389
|
-
---
|
|
390
599
|
|
|
391
|
-
|
|
600
|
+
Takes a function that gets called with a [`RefsDSL`](./src/builders.ts#L54)
|
|
601
|
+
object. It should return an object of child elements that you want to keep
|
|
602
|
+
references to.
|
|
392
603
|
|
|
393
604
|
The `data-ref` attribute is a fallback. It's used when you don't pass a
|
|
394
605
|
selector to `r.one()`. If you pass explicit CSS selectors to `withRefs`,
|
|
395
606
|
e.g. `{ copy: r.one('button') }`, then the HTML does not require a `data-ref`
|
|
396
607
|
attribute.
|
|
397
608
|
|
|
398
|
-
#### `withRefs`
|
|
399
|
-
|
|
400
|
-
Call `r.one()` / `r.all()` with no argument to match `[data-ref="<key>"]`,
|
|
401
|
-
where `key` is the key name you pass in, or pass a CSS selector to query by
|
|
402
|
-
it instead. Queries are resolved relative to the host element, during
|
|
403
|
-
[the `connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks).
|
|
404
|
-
|
|
405
609
|
|
|
406
610
|
```ts
|
|
407
611
|
.withRefs(r => ({
|
|
@@ -419,67 +623,41 @@ If there are no `data-ref` attribtues in the HTML, use CSS query selectors.
|
|
|
419
623
|
}))
|
|
420
624
|
```
|
|
421
625
|
|
|
422
|
-
See [#refs in serverside-rendering](#refs). Each component created with
|
|
423
|
-
`micortags` exposes a `.refs` property that is an object map
|
|
424
|
-
of ref names, eg `MyComponent.refs.copyButton`.
|
|
425
626
|
|
|
426
|
-
|
|
627
|
+
---------------------------------------------------------------------
|
|
427
628
|
|
|
428
|
-
An un-annotated `r.one()` is typed as `HTMLElement`, so `.style`,
|
|
429
|
-
`.textContent`, and `.dataset` work without a cast. There are two ways to
|
|
430
|
-
get a more specific element type:
|
|
431
629
|
|
|
432
|
-
|
|
433
|
-
.withRefs(r => ({
|
|
434
|
-
// Generic argument: sets the type, keeps the [data-ref] selector.
|
|
435
|
-
display: r.one<HTMLDivElement>(),
|
|
436
|
-
|
|
437
|
-
// Tag string: infers the type AND queries by that tag, exactly like
|
|
438
|
-
// document.querySelector('button'). It does NOT keep [data-ref].
|
|
439
|
-
save: r.one('button'), // SingleRefDef<HTMLButtonElement>
|
|
440
|
-
items: r.all('li'), // MultipleRefDef<HTMLLIElement>
|
|
441
|
-
button: r.one(), // HTMLElement (un-annotated default)
|
|
442
|
-
}))
|
|
443
|
-
```
|
|
630
|
+
### `builder.withContexts`
|
|
444
631
|
|
|
445
|
-
|
|
632
|
+
Context is for the case where a parent component defines some state, e.g. a
|
|
633
|
+
`theme`, and the top-level (application) knows nothing about the theme, but the
|
|
634
|
+
application *does* determine what children the parent renders
|
|
635
|
+
(the app passes in children), and the children need to know the theme.
|
|
446
636
|
|
|
447
637
|
```ts
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
638
|
+
class ComponentBuilder {
|
|
639
|
+
withContexts<C extends Record<string, ContextToken<unknown>>> (
|
|
640
|
+
fn:(c:ContextDSL) => C
|
|
641
|
+
):ComponentBuilder<Props, RefDefs, C>
|
|
642
|
+
}
|
|
452
643
|
```
|
|
453
644
|
|
|
454
|
-
|
|
455
|
-
constrained to `HTMLElement`, so cast at the use site if you need one
|
|
456
|
-
(e.g. `ctx.refs.icon as unknown as SVGSVGElement`).
|
|
457
|
-
|
|
645
|
+
#### `.withContexts` Example
|
|
458
646
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
647
|
+
```html
|
|
648
|
+
<!-- The application composes this markup. -->
|
|
649
|
+
<theme-provider>
|
|
650
|
+
<some-child></some-child>
|
|
651
|
+
<another-child></another-child>
|
|
652
|
+
</theme-provider>
|
|
465
653
|
```
|
|
466
654
|
|
|
467
|
-
Returns a fluent builder. Call `.setup()` to register the custom element.
|
|
468
|
-
All builder methods are chainable and fully type-inferred.
|
|
469
|
-
|
|
470
|
-
### `withContexts`
|
|
471
|
-
|
|
472
|
-
Context is for the case where a parent component defines some state, e.g. a
|
|
473
|
-
`theme`, and the top-level (application) knows nothing about the theme. But the
|
|
474
|
-
top level application does determine what children the parent renders
|
|
475
|
-
(it passes in children), and the children need to know the theme.
|
|
476
|
-
|
|
477
655
|
In that case, there is no way for the "parent" component to pass the theme to
|
|
478
|
-
the children
|
|
656
|
+
the children. The root level (application) doesn't know what the theme is.
|
|
479
657
|
|
|
480
|
-
Context gives us a
|
|
481
|
-
publishes the value, and any descendants opt in
|
|
482
|
-
`withContexts
|
|
658
|
+
Context gives us a way to model this. The context-provider
|
|
659
|
+
publishes the value, and any descendants opt in with
|
|
660
|
+
`withContexts`.
|
|
483
661
|
|
|
484
662
|
>
|
|
485
663
|
> [!IMPORTANT]
|
|
@@ -487,7 +665,6 @@ publishes the value, and any descendants opt in (or pull values) with
|
|
|
487
665
|
> Context gives you nothing for first paint or the no-JS case.
|
|
488
666
|
>
|
|
489
667
|
|
|
490
|
-
|
|
491
668
|
```ts
|
|
492
669
|
import { define } from 'microtags'
|
|
493
670
|
import { createContext, provide } from 'microtags/context'
|
|
@@ -498,7 +675,8 @@ const ThemeToken = createContext<string>()
|
|
|
498
675
|
// not reference the children below. The application composes those in.
|
|
499
676
|
define('theme-provider')
|
|
500
677
|
.setup(ctx => {
|
|
501
|
-
|
|
678
|
+
const rmContext = provide(ctx.host, ThemeToken, 'dark')
|
|
679
|
+
ctx.onCleanup(rmContext)
|
|
502
680
|
})
|
|
503
681
|
|
|
504
682
|
// Child: opts in to the theme and reads whichever provider it is
|
|
@@ -515,65 +693,180 @@ define('themed-card')
|
|
|
515
693
|
```
|
|
516
694
|
|
|
517
695
|
```html
|
|
518
|
-
<!-- The application writes this markup. It nests <themed-card> inside
|
|
519
|
-
<theme-provider> but never sets the theme itself. -->
|
|
520
696
|
<theme-provider>
|
|
521
697
|
<themed-card>Card content</themed-card>
|
|
522
698
|
</theme-provider>
|
|
523
699
|
```
|
|
524
700
|
|
|
525
|
-
`setup()` is deferred until all required contexts resolve. See
|
|
526
|
-
[`microtags/context`](#microtagscontext) for the provider API.
|
|
527
701
|
|
|
528
|
-
|
|
702
|
+
---------------------------------------------------------
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
### `builder.setup(ctx)`
|
|
706
|
+
|
|
707
|
+
Registers the custom element via `customElements.define` and returns the
|
|
708
|
+
element class. The returned class exposes `.TAG` and `.refs` as static
|
|
709
|
+
members, for use in [serverside rendering](#serverside-example).
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
class ComponentBuilder {
|
|
713
|
+
setup (
|
|
714
|
+
fn:(ctx:SetupContext<
|
|
715
|
+
Props,
|
|
716
|
+
RefsMap<RefDefs>,
|
|
717
|
+
ContextsMap<CtxDefs>
|
|
718
|
+
>) => void
|
|
719
|
+
):MicrotagElementClass<RefDefs> {
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
The callback function runs once per element instance, and is called with the
|
|
724
|
+
following `context` object.
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
#### `ctx`
|
|
728
|
+
|
|
729
|
+
The argument passed to the callback given to `setup`.
|
|
529
730
|
|
|
530
731
|
```ts
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
// ctx.props callable signals for each declared prop
|
|
534
|
-
// ctx.refs resolved DOM refs
|
|
535
|
-
// ctx.contexts consumed context values (reactive)
|
|
732
|
+
ctx:SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>
|
|
733
|
+
```
|
|
536
734
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
735
|
+
##### `ctx` Methods
|
|
736
|
+
|
|
737
|
+
| Method | Returns | Description |
|
|
738
|
+
|--------|---------|-------------|
|
|
739
|
+
| `.peek(signal)` | `T` | Read a signal's current value without subscribing, even inside `ctx.effect`. |
|
|
740
|
+
| `.on(target, type, handler, options?)` | `void` | Add a DOM event listener, auto-removed on disconnect. |
|
|
741
|
+
| `.emit(event)` / `.emit(name, detail?, options?)` | `boolean` | Dispatch a pre-built `Event` as-is, or construct and dispatch a bubbling `CustomEvent` from a name. Returns `dispatchEvent`'s result: `false` if a cancelable event had `preventDefault()` called. |
|
|
742
|
+
| `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on disconnect. |
|
|
743
|
+
| `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on disconnect. |
|
|
744
|
+
| `.onCleanup(fn)` | `void` | Register custom teardown to run on disconnect. |
|
|
745
|
+
| `.consume(token)` | `T \| undefined` | Read an optional context value published by an ancestor. See [`withContexts`](#withcontexts). |
|
|
746
|
+
|
|
747
|
+
See [Typed Events](#typed-events) for more about `.on` and `.emit`.
|
|
541
748
|
|
|
542
|
-
|
|
543
|
-
ctx.bind(() => String(ctx.props.count()), el, { prop: 'textContent' })
|
|
749
|
+
##### `ctx.bind`
|
|
544
750
|
|
|
545
|
-
|
|
546
|
-
|
|
751
|
+
The `.bind` method treats the signal as the source of truth. The element
|
|
752
|
+
property is set from the signal.
|
|
753
|
+
With no `options`, the control's element property and write-back event are
|
|
754
|
+
auto-detected from its type:
|
|
547
755
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
756
|
+
```ts
|
|
757
|
+
{
|
|
758
|
+
bind<T> (
|
|
759
|
+
source:Signal<T>,
|
|
760
|
+
control:Element,
|
|
761
|
+
opts?:BindOptions
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
| Control | `prop` | write-back `event` |
|
|
767
|
+
|---------|--------|---------------------|
|
|
768
|
+
| `<input type="checkbox">` | `checked` | `change` |
|
|
769
|
+
| `<input type="number">` / `type="range"` | `valueAsNumber` | `input` |
|
|
770
|
+
| Other `<input>` types / `<textarea>` | `value` | `input` |
|
|
771
|
+
| Anything else, e.g. `<select>` | `value` | `change` |
|
|
772
|
+
|
|
773
|
+
Passing `options` overrides the auto-detected `prop`, and replaces the
|
|
774
|
+
write-back `event` -- omit `options.event` for a one-way binding. A read-only
|
|
775
|
+
getter (`() => T`) is also accepted as the `signal` argument, for binding a
|
|
776
|
+
derived value one-way (pass it with `options` and omit `event`).
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
// two-way, auto-detected (checkbox -> `checked` / `change`)
|
|
780
|
+
ctx.bind(checked, ctx.refs.toggle)
|
|
781
|
+
|
|
782
|
+
// one-way: push a derived value into a read-only display
|
|
783
|
+
ctx.bind(() => `${count()} items`, ctx.refs.label, { prop: 'textContent' })
|
|
784
|
+
```
|
|
552
785
|
|
|
553
|
-
|
|
554
|
-
const snap = ctx.peek(ctx.props.count)
|
|
786
|
+
---
|
|
555
787
|
|
|
556
|
-
// Register arbitrary teardown logic.
|
|
557
|
-
ctx.onCleanup(() => { /* ... */ })
|
|
558
788
|
|
|
559
|
-
|
|
560
|
-
|
|
789
|
+
## Missing refs
|
|
790
|
+
|
|
791
|
+
Any calls to `r.one` in `.withRefs` will throw if there is not a matching
|
|
792
|
+
child element.
|
|
793
|
+
|
|
794
|
+
```
|
|
795
|
+
Error: MY-COMPONENT: missing refs: button, save
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
The tag name is upper case (it comes from `host.tagName`), and the
|
|
799
|
+
message lists every required ref that was not found.
|
|
800
|
+
|
|
801
|
+
Call to `r.all()` are **never reported**. A missing match resolves to an
|
|
802
|
+
empty array, not an error.
|
|
803
|
+
|
|
804
|
+
This check happens right before the custom element runs your `.setup` function.
|
|
805
|
+
|
|
806
|
+
For a component with no contexts, refs are resolved at the moment the element
|
|
807
|
+
connects to the DOM. For a component that declares
|
|
808
|
+
[`withContexts`](#withcontexts), ref collection is deferred along
|
|
809
|
+
with `setup` until every required context resolves.
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
### Checking refs in tests
|
|
813
|
+
|
|
814
|
+
A missing `r.one` ref throws while the component connects. When the browser
|
|
815
|
+
runs a lifecycle callback (which is what `appendChild` triggers), it reports
|
|
816
|
+
the exception to the global `error` event rather than propagating it to the
|
|
817
|
+
caller, so a try / catch around `appendChild` sees nothing.
|
|
818
|
+
|
|
819
|
+
You need to listen for the global `error` event. When every
|
|
820
|
+
`data-ref` the component declares is present, nothing is reported.
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
import { test } from '@substrate-system/tapzero'
|
|
824
|
+
import './my-counter.js' // the component from the example above
|
|
825
|
+
|
|
826
|
+
test('all refs resolve', t => {
|
|
827
|
+
let err:Error|null = null
|
|
828
|
+
const onError = (ev:ErrorEvent) => { err = ev.error }
|
|
829
|
+
window.addEventListener('error', onError)
|
|
830
|
+
|
|
831
|
+
const el = document.createElement('my-counter')
|
|
832
|
+
el.innerHTML = `
|
|
833
|
+
<div data-ref="display"></div>
|
|
834
|
+
<button data-ref="dec">-</button>
|
|
835
|
+
<button data-ref="inc">+</button>`
|
|
836
|
+
document.body.appendChild(el)
|
|
837
|
+
|
|
838
|
+
t.ok(err === null, 'no missing refs')
|
|
839
|
+
|
|
840
|
+
window.removeEventListener('error', onError)
|
|
841
|
+
document.body.removeChild(el)
|
|
561
842
|
})
|
|
562
843
|
```
|
|
563
844
|
|
|
564
|
-
#### peek and untracked
|
|
565
845
|
|
|
566
|
-
|
|
567
|
-
|
|
846
|
+
## Typed Events
|
|
847
|
+
|
|
848
|
+
`TypedEvent<Target, Detail>` is a type-only helper that narrows
|
|
849
|
+
`CustomEvent` to a specific `target` and `detail`. Combine it with
|
|
850
|
+
`HTMLElementEventMap` augmentation for app-wide type-safe events.
|
|
568
851
|
|
|
569
852
|
```ts
|
|
570
|
-
import {
|
|
853
|
+
import type { TypedEvent } from 'microtags'
|
|
571
854
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
855
|
+
type TabsChangedEvent = TypedEvent<InstanceType<typeof XTabs>, { index:number }>
|
|
856
|
+
|
|
857
|
+
declare global {
|
|
858
|
+
interface HTMLElementEventMap {
|
|
859
|
+
'tabs:changed':TabsChangedEvent
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Emitting from setup:
|
|
864
|
+
ctx.emit('tabs:changed', { index: 2 })
|
|
865
|
+
|
|
866
|
+
// Listening, fully typed:
|
|
867
|
+
ctx.on(tabsEl, 'tabs:changed', ev => {
|
|
868
|
+
ev.target // the XTabs instance
|
|
869
|
+
ev.detail // { index:number }
|
|
577
870
|
})
|
|
578
871
|
```
|
|
579
872
|
|
|
@@ -649,6 +942,7 @@ callback function mutates the DOM through properties like `textContent`.
|
|
|
649
942
|
Nothing concatenates strings into markup, so there is no string-injection
|
|
650
943
|
XSS vector.
|
|
651
944
|
|
|
945
|
+
|
|
652
946
|
## Subpath Exports
|
|
653
947
|
|
|
654
948
|
### `microtags/context`
|
|
@@ -719,6 +1013,10 @@ signal imperatively.
|
|
|
719
1013
|
|
|
720
1014
|
### `microtags/render`
|
|
721
1015
|
|
|
1016
|
+
```ts
|
|
1017
|
+
import { render, renderList } from 'microtags/render'
|
|
1018
|
+
```
|
|
1019
|
+
|
|
722
1020
|
A keyed reconciler for rendering lists and single items from a
|
|
723
1021
|
`<template>`, with no virtual DOM and no diffing library.
|
|
724
1022
|
|
|
@@ -846,7 +1144,8 @@ import {
|
|
|
846
1144
|
coerceString,
|
|
847
1145
|
coerceBoolean,
|
|
848
1146
|
coerceJson,
|
|
849
|
-
|
|
1147
|
+
runAll,
|
|
1148
|
+
toAttributes
|
|
850
1149
|
} from 'microtags/util'
|
|
851
1150
|
```
|
|
852
1151
|
|
|
@@ -872,14 +1171,14 @@ function coerceJson<T = unknown> (raw:string | null):T | undefined
|
|
|
872
1171
|
- `coerceJson` -- `JSON.parse(raw)`, or `undefined` when the attribute is
|
|
873
1172
|
absent or not valid JSON. Pass a type parameter to annotate the result.
|
|
874
1173
|
|
|
875
|
-
#### `
|
|
1174
|
+
#### `runAll`
|
|
876
1175
|
|
|
877
1176
|
Runs every function in the array, even if some throw. After all have run
|
|
878
1177
|
it re-throws the first error encountered, so one failing teardown cannot
|
|
879
1178
|
skip the rest. A `Cleanup` is a no-argument teardown function, `() => void`.
|
|
880
1179
|
|
|
881
1180
|
```ts
|
|
882
|
-
function
|
|
1181
|
+
function runAll (fns:Cleanup[]):void
|
|
883
1182
|
```
|
|
884
1183
|
|
|
885
1184
|
#### `toAttributes`
|
|
@@ -907,7 +1206,7 @@ function toAttributes (attrs:Attrs):string
|
|
|
907
1206
|
|
|
908
1207
|
---
|
|
909
1208
|
|
|
910
|
-
##
|
|
1209
|
+
## Differences from `nanotags`
|
|
911
1210
|
|
|
912
1211
|
### Reactivity primitive
|
|
913
1212
|
|
|
@@ -985,8 +1284,6 @@ These nanotags APIs have no microtags equivalent:
|
|
|
985
1284
|
|
|
986
1285
|
- `ctx.getElement` / `ctx.getElements` -- declare refs with `withRefs`, or
|
|
987
1286
|
call `ctx.host.querySelector` directly.
|
|
988
|
-
- The `setup` mixin return value -- `setup` returns `void`; it does not
|
|
989
|
-
assign members back onto the element.
|
|
990
1287
|
- The `define(name, setupFn)` two-argument shorthand -- always use the
|
|
991
1288
|
builder chain ending in `.setup()`.
|
|
992
1289
|
|