microtags 0.0.0 → 0.0.3
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 +343 -46
- package/dist/attributes.d.ts +10 -0
- package/dist/attributes.d.ts.map +1 -0
- package/dist/builders.d.ts +43 -4
- package/dist/builders.d.ts.map +1 -1
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +7 -0
- package/dist/chunk-CCYWRJW6.js +98 -0
- package/dist/chunk-CCYWRJW6.js.map +7 -0
- package/dist/chunk-E7ZGOHL3.js +51 -0
- package/dist/chunk-E7ZGOHL3.js.map +7 -0
- package/dist/context.cjs +10 -4
- package/dist/context.cjs.map +3 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -63
- package/dist/context.js.map +3 -3
- package/dist/context.min.js.map +2 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/index.cjs +416 -5
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +345 -2
- package/dist/index.js.map +3 -3
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +4 -4
- package/dist/meta.json +277 -27
- package/dist/reactivity.d.ts.map +1 -1
- package/dist/render.cjs +6 -4
- package/dist/render.cjs.map +2 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +9 -6
- package/dist/render.js.map +2 -2
- package/dist/render.min.js.map +1 -1
- package/dist/setup-context.d.ts.map +1 -1
- package/dist/types.d.ts +45 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/util.cjs +68 -0
- package/dist/util.cjs.map +7 -0
- package/dist/{utils.d.ts → util.d.ts} +1 -1
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +16 -0
- package/dist/util.js.map +7 -0
- package/dist/util.min.js +2 -0
- package/dist/util.min.js.map +7 -0
- package/package.json +38 -19
- package/dist/utils.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,70 +1,164 @@
|
|
|
1
1
|
# microtags
|
|
2
2
|
|
|
3
|
-
](https://github.com/nichoth/microtags/actions/workflows/nodejs.yml)
|
|
4
4
|
[](README.md)
|
|
5
5
|
[](README.md)
|
|
6
6
|
[](https://semver.org/)
|
|
7
7
|
[](./CHANGELOG.md)
|
|
8
|
+
[](https://packagephobia.com/result?p=microtags)
|
|
9
|
+
[](https://bundlephobia.com/package/microtags)
|
|
8
10
|
[](LICENSE)
|
|
9
11
|
|
|
10
12
|
Web component helper with reactive props, automatic cleanup,
|
|
11
13
|
and no Shadow DOM. Reactive via
|
|
12
14
|
[alien-signals](https://github.com/stackblitz/alien-signals).
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
**Size**: 3.41 kB with all dependencies, minified and brotlied
|
|
17
|
+
|
|
18
|
+
Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
19
|
+
**alien-signals** instead of nanostores atoms.
|
|
18
20
|
|
|
19
21
|
<details><summary><h2>Contents</h2></summary>
|
|
20
22
|
|
|
23
|
+
<!-- toc -->
|
|
24
|
+
|
|
21
25
|
- [Install](#install)
|
|
22
|
-
|
|
26
|
+
* [Import map](#import-map)
|
|
27
|
+
* [Pre-bundled](#pre-bundled)
|
|
28
|
+
- [Example](#example)
|
|
29
|
+
* [Client-side validation](#client-side-validation)
|
|
30
|
+
- [Serverside Example](#serverside-example)
|
|
31
|
+
* [`.TAG`](#tag)
|
|
32
|
+
* [`.refs`](#refs)
|
|
23
33
|
- [API](#api)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
* [Props](#props)
|
|
35
|
+
+ [`withProps`](#withprops)
|
|
36
|
+
* [Refs](#refs)
|
|
37
|
+
+ [`withRefs`](#withrefs)
|
|
38
|
+
+ [Element types](#element-types)
|
|
39
|
+
* [define](#define)
|
|
40
|
+
* [withContexts](#withcontexts)
|
|
41
|
+
* [setup and ctx](#setup-and-ctx)
|
|
42
|
+
+ [peek and untracked](#peek-and-untracked)
|
|
29
43
|
- [Subpaths](#subpaths)
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
* [microtags/context](#microtagscontext)
|
|
45
|
+
* [microtags/render](#microtagsrender)
|
|
32
46
|
- [Divergence from nanotags](#divergence-from-nanotags)
|
|
33
47
|
|
|
48
|
+
<!-- tocstop -->
|
|
49
|
+
|
|
34
50
|
</details>
|
|
35
51
|
|
|
52
|
+
----------------------------------------------------------------------
|
|
36
53
|
## Install
|
|
54
|
+
----------------------------------------------------------------------
|
|
37
55
|
|
|
38
56
|
```sh
|
|
39
57
|
npm i -S microtags
|
|
40
58
|
```
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
### Import map
|
|
61
|
+
|
|
62
|
+
No build step needed. Resolve `microtags` with an
|
|
63
|
+
[import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap)
|
|
64
|
+
and `esm.sh`.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<script type="importmap">
|
|
69
|
+
{
|
|
70
|
+
"imports": {
|
|
71
|
+
"microtags": "https://esm.sh/microtags"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<script type="module">
|
|
77
|
+
import { define } from 'microtags'
|
|
78
|
+
</script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Subpaths follow the same URL pattern: `microtags/context`, `microtags/render`.
|
|
82
|
+
|
|
83
|
+
### Pre-bundled
|
|
84
|
+
|
|
85
|
+
For raw-file CDNs (jsDelivr, unpkg) or local files, where dependencies are
|
|
86
|
+
not resolved for you, use the pre-bundled `microtags/min` build. It is a
|
|
87
|
+
single self-contained file with `alien-signals` inlined, so no other
|
|
88
|
+
mappings are needed:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<script type="importmap">
|
|
92
|
+
{
|
|
93
|
+
"imports": {
|
|
94
|
+
"microtags": "https://cdn.jsdelivr.net/npm/microtags/dist/index.min.js"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
----------------------------------------------------------------------
|
|
102
|
+
## Example
|
|
103
|
+
----------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
This example depends on having the full HTML already in the DOM. If you want
|
|
106
|
+
to do client-side-rendered components, that is possible. You would update the
|
|
107
|
+
DOM inside `.setup`.
|
|
108
|
+
|
|
109
|
+
Since the full HTML exists already, defining your components in the
|
|
110
|
+
client-side JS is about "hydrating" the page (adding behavior).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
`withProps`, `withRefs`, and `withContexts` are optional and can appear in
|
|
115
|
+
any order. `.setup` ends the chain: it calls `customElements.define` under the
|
|
116
|
+
hood and returns a typed constructor.
|
|
43
117
|
|
|
44
118
|
```ts
|
|
45
119
|
import { define } from 'microtags'
|
|
46
120
|
import { signal } from 'alien-signals'
|
|
47
121
|
|
|
48
|
-
|
|
122
|
+
/**
|
|
123
|
+
* - attribute-backed prop (`start`)
|
|
124
|
+
* - data-ref elements (the <count-button> and <copy-btn> children)
|
|
125
|
+
* - ctx.effect, ctx.on
|
|
126
|
+
*/
|
|
127
|
+
export const myCounter = define('my-counter')
|
|
49
128
|
.withProps(p => ({
|
|
50
|
-
start: p.number(),
|
|
129
|
+
start: p.number(),
|
|
51
130
|
}))
|
|
52
|
-
.withRefs(r => ({
|
|
53
|
-
display: r.one,
|
|
54
|
-
inc: r.one,
|
|
55
|
-
dec: r.one,
|
|
131
|
+
.withRefs(r => ({ // the keys in this object are the ref names
|
|
132
|
+
display: r.one<HTMLDivElement>(), // typed as HTMLDivElement
|
|
133
|
+
inc: r.one(), // defaults to HTMLElement
|
|
134
|
+
dec: r.one(),
|
|
135
|
+
copy: r.one(),
|
|
56
136
|
}))
|
|
57
137
|
.setup(ctx => {
|
|
58
|
-
//
|
|
138
|
+
// all event <-> state logic here
|
|
59
139
|
const count = signal(ctx.props.start())
|
|
140
|
+
const display = ctx.refs.display
|
|
141
|
+
|
|
142
|
+
ctx.effect(() => {
|
|
143
|
+
display.textContent = `Count: ${count()}`
|
|
144
|
+
display.style.color = count() < 0 ? 'red' : 'inherit'
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Reflect the live count onto the copy button's `value` attribute.
|
|
148
|
+
// Because `value` is attribute-backed, this flows into <copy-btn>'s
|
|
149
|
+
// own signal, so it always copies the current count.
|
|
150
|
+
ctx.effect(() => {
|
|
151
|
+
const copy = ctx.refs.copy
|
|
152
|
+
copy.setAttribute('value', String(count()))
|
|
153
|
+
})
|
|
60
154
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
155
|
+
ctx.on(ctx.refs.inc, 'click', () => {
|
|
156
|
+
count(count() + 1)
|
|
157
|
+
})
|
|
64
158
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
159
|
+
ctx.on(ctx.refs.dec, 'click', () => {
|
|
160
|
+
count(count() - 1)
|
|
161
|
+
})
|
|
68
162
|
})
|
|
69
163
|
```
|
|
70
164
|
|
|
@@ -76,29 +170,164 @@ define('my-counter')
|
|
|
76
170
|
</my-counter>
|
|
77
171
|
```
|
|
78
172
|
|
|
79
|
-
|
|
173
|
+
### Client-side validation
|
|
80
174
|
|
|
81
|
-
|
|
175
|
+
See [example/subscribe-form.ts](./example/subscribe-form.ts).
|
|
176
|
+
|
|
177
|
+
There are two separate things people call "validation", and `microtags`
|
|
178
|
+
handles them in different places:
|
|
179
|
+
|
|
180
|
+
1. **Prop validation** checks the attributes passed *into* a component.
|
|
181
|
+
Pass a [Standard Schema](https://standardschema.dev) (Zod, Valibot,
|
|
182
|
+
ArkType) to [`p.schema`](#withprops); it coerces the attribute value
|
|
183
|
+
and falls back to `undefined` when the value doesn't match.
|
|
184
|
+
2. **Form input validation** checks live user input in a form, for example a
|
|
185
|
+
value that changes on every keystroke and needs to report *why* it is invalid.
|
|
186
|
+
There is no dedicated API for this. Validate the schema directly
|
|
187
|
+
inside `.setup()` and hold the result in a signal:
|
|
188
|
+
|
|
189
|
+
The example below is the second kind -- form input (client-side) validation.
|
|
82
190
|
|
|
83
191
|
```ts
|
|
192
|
+
import { signal, startBatch, endBatch } from 'alien-signals'
|
|
193
|
+
import { z } from 'zod'
|
|
84
194
|
import { define } from 'microtags'
|
|
85
195
|
|
|
86
|
-
|
|
196
|
+
const Email = z.email('Please enter a valid email address.')
|
|
197
|
+
|
|
198
|
+
define('subscribe-form')
|
|
199
|
+
.withRefs(r => ({
|
|
200
|
+
form: r.one('form'),
|
|
201
|
+
input: r.one('input'),
|
|
202
|
+
submit: r.one('button'),
|
|
203
|
+
error: r.one('.error'),
|
|
204
|
+
status: r.one('.status'),
|
|
205
|
+
}))
|
|
206
|
+
.setup(ctx => {
|
|
207
|
+
const value = signal('')
|
|
208
|
+
const touched = signal(false)
|
|
209
|
+
|
|
210
|
+
// capture the live value (two-way; clears the field on reset)
|
|
211
|
+
ctx.bind(value, ctx.refs.input)
|
|
212
|
+
// only reveal errors after the first blur
|
|
213
|
+
ctx.on(ctx.refs.input, 'blur', () => touched(true))
|
|
214
|
+
|
|
215
|
+
ctx.effect(() => {
|
|
216
|
+
const result = Email.safeParse(value())
|
|
217
|
+
const message = result.success ?
|
|
218
|
+
'' :
|
|
219
|
+
result.error.issues[0].message
|
|
220
|
+
const showError = touched() && !result.success
|
|
221
|
+
|
|
222
|
+
ctx.refs.submit.disabled = !result.success
|
|
223
|
+
ctx.refs.input.setAttribute('aria-invalid', String(showError))
|
|
224
|
+
ctx.refs.error.textContent = showError ? message : ''
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
ctx.on(ctx.refs.form, 'submit', ev => {
|
|
228
|
+
ev.preventDefault()
|
|
229
|
+
if (!Email.safeParse(value()).success) return
|
|
230
|
+
ctx.refs.status.textContent = 'Thanks for subscribing!'
|
|
231
|
+
startBatch()
|
|
232
|
+
value('')
|
|
233
|
+
touched(false)
|
|
234
|
+
endBatch()
|
|
235
|
+
})
|
|
236
|
+
})
|
|
87
237
|
```
|
|
88
238
|
|
|
89
|
-
|
|
90
|
-
|
|
239
|
+
The error stays hidden until the field is blurred once, then updates live;
|
|
240
|
+
the submit button is gated on validity. See `example/subscribe-form.ts`
|
|
241
|
+
for the full working component.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
----------------------------------------------------------------------
|
|
245
|
+
## Serverside Example
|
|
246
|
+
----------------------------------------------------------------------
|
|
91
247
|
|
|
92
|
-
###
|
|
248
|
+
### `.TAG`
|
|
249
|
+
|
|
250
|
+
Each tag name that you pass to `define` is exposed as a property `.TAG`
|
|
251
|
+
on the return value. It is possible to import your web components in Node,
|
|
252
|
+
even though Node does not have browser APIs.
|
|
253
|
+
|
|
254
|
+
### `.refs`
|
|
255
|
+
|
|
256
|
+
All refs are defined per-component, and the ref names are exposed at `.refs`,
|
|
257
|
+
so you can import your web components on the server and read the ref names
|
|
258
|
+
as strings.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { MyCounter, CountBtn, CopyBtn } from './index.js'
|
|
262
|
+
|
|
263
|
+
// this runs in Node
|
|
264
|
+
|
|
265
|
+
export function render ():string {
|
|
266
|
+
return `<div>
|
|
267
|
+
<${MyCounter.TAG} start="${START}">
|
|
268
|
+
<div data-ref="${MyCounter.refs.display}"
|
|
269
|
+
role="status"
|
|
270
|
+
class="count"
|
|
271
|
+
>${START}</div>
|
|
272
|
+
|
|
273
|
+
<div class="controls">
|
|
274
|
+
<${CountBtn.TAG}
|
|
275
|
+
class="dec"
|
|
276
|
+
data-ref="${MyCounter.refs.dec}"
|
|
277
|
+
aria-label="Decrement"
|
|
278
|
+
>
|
|
279
|
+
<button>-</button>
|
|
280
|
+
</${CountBtn.TAG}>
|
|
281
|
+
|
|
282
|
+
<${CopyBtn.TAG}
|
|
283
|
+
class="copy"
|
|
284
|
+
data-ref="${MyCounter.refs.copy}"
|
|
285
|
+
></${CopyBtn.TAG}>
|
|
286
|
+
|
|
287
|
+
<${CountBtn.TAG}
|
|
288
|
+
class="inc"
|
|
289
|
+
data-ref="${MyCounter.refs.inc}"
|
|
290
|
+
aria-label="Increment"
|
|
291
|
+
>
|
|
292
|
+
<button>+</button>
|
|
293
|
+
</${CountBtn.TAG}>
|
|
294
|
+
</div>
|
|
295
|
+
</${MyCounter.TAG}>
|
|
296
|
+
</div>`
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
----------------------------------------------------------------------
|
|
301
|
+
## API
|
|
302
|
+
----------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
### Props
|
|
305
|
+
|
|
306
|
+
Declare reactive attributes via `withProps`. Each prop becomes:
|
|
307
|
+
|
|
308
|
+
* An observed HTML attribute (auto-synced via `attributeChangedCallback`)
|
|
309
|
+
* A signal at `ctx.props.propName`
|
|
310
|
+
* A typed getter/setter on the element instance
|
|
311
|
+
* Four built-in validators coerce raw attribute strings to typed values:
|
|
312
|
+
|
|
313
|
+
| Validator | Coercion | `null` attr |
|
|
314
|
+
|-----------|----------|-------------|
|
|
315
|
+
| `p.string()` | `String(val)` | `""` |
|
|
316
|
+
| `p.number()` | `Number(val)` | `0` |
|
|
317
|
+
| `p.boolean()` | `"true"` / `""` → `true`, `"false"` → `false` | `false` |
|
|
318
|
+
| `p.oneOf(opts)` | Picklist enum, throws on invalid | throws |
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
#### `withProps`
|
|
93
322
|
|
|
94
323
|
```ts
|
|
95
324
|
.withProps(p => ({
|
|
96
|
-
count: p.number(),
|
|
97
|
-
label: p.string(),
|
|
98
|
-
active: p.boolean(),
|
|
99
|
-
data: p.json<MyType>(),
|
|
100
|
-
schema: p.schema(zodSchema), // Standard Schema (Zod, Valibot, ArkType
|
|
101
|
-
value: p.prop(0),
|
|
325
|
+
count: p.number(), // attribute -> number (NaN when absent)
|
|
326
|
+
label: p.string(), // attribute -> string ('' when absent)
|
|
327
|
+
active: p.boolean(), // attribute -> boolean (true if present)
|
|
328
|
+
data: p.json<MyType>(), // attribute -> JSON.parse or undefined
|
|
329
|
+
schema: p.schema(zodSchema), // Standard Schema (Zod, Valibot, ArkType)
|
|
330
|
+
value: p.prop(0), // property-only, not an observed attribute
|
|
102
331
|
}))
|
|
103
332
|
```
|
|
104
333
|
|
|
@@ -109,22 +338,86 @@ ctx.props.count() // read
|
|
|
109
338
|
ctx.props.count(42) // write
|
|
110
339
|
```
|
|
111
340
|
|
|
112
|
-
|
|
341
|
+
|
|
342
|
+
### Refs
|
|
343
|
+
|
|
344
|
+
The `data-ref` attribute is a fallback. It's used when you don't pass a
|
|
345
|
+
selector to `r.one()`. If you pass explicit CSS selectors to `withRefs`,
|
|
346
|
+
e.g. `{ copy: r.one('button') }`, then the HTML does not require a `data-ref`
|
|
347
|
+
attribute.
|
|
348
|
+
|
|
349
|
+
#### `withRefs`
|
|
350
|
+
|
|
351
|
+
Call `r.one()` / `r.all()` with no argument to match `[data-ref="<key>"]`,
|
|
352
|
+
where `key` is the key name you pass in, or pass a CSS selector to query by
|
|
353
|
+
it instead. Queries are resolved relative to the host element, during
|
|
354
|
+
[the `connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks).
|
|
355
|
+
|
|
113
356
|
|
|
114
357
|
```ts
|
|
115
358
|
.withRefs(r => ({
|
|
116
|
-
button: r.one, // resolves [data-ref="button"] — throws if missing
|
|
117
|
-
items: r.all, // resolves all [data-ref="items"] — empty array if none
|
|
359
|
+
button: r.one(), // resolves [data-ref="button"] — throws if missing
|
|
360
|
+
items: r.all(), // resolves all [data-ref="items"] — empty array if none
|
|
118
361
|
}))
|
|
119
362
|
```
|
|
120
363
|
|
|
121
|
-
|
|
364
|
+
If there are no `data-ref` attribtues in the HTML, use CSS query selectors.
|
|
122
365
|
|
|
123
366
|
```ts
|
|
124
|
-
|
|
125
|
-
|
|
367
|
+
.withRefs(r => ({
|
|
368
|
+
code: r.one('code'), // host.querySelector('code')
|
|
369
|
+
items: r.all('li'), // host.querySelectorAll('li')
|
|
370
|
+
}))
|
|
126
371
|
```
|
|
127
372
|
|
|
373
|
+
See [#refs in serverside-rendering](#refs). Each component created with
|
|
374
|
+
`micortags` exposes a `.refs` property that is an object map
|
|
375
|
+
of ref names, eg `MyComponent.refs.copyButton`.
|
|
376
|
+
|
|
377
|
+
#### Element types
|
|
378
|
+
|
|
379
|
+
An un-annotated `r.one()` is typed as `HTMLElement`, so `.style`,
|
|
380
|
+
`.textContent`, and `.dataset` work without a cast. There are two ways to
|
|
381
|
+
get a more specific element type:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
.withRefs(r => ({
|
|
385
|
+
// Generic argument: sets the type, keeps the [data-ref] selector.
|
|
386
|
+
display: r.one<HTMLDivElement>(),
|
|
387
|
+
|
|
388
|
+
// Tag string: infers the type AND queries by that tag, exactly like
|
|
389
|
+
// document.querySelector('button'). It does NOT keep [data-ref].
|
|
390
|
+
save: r.one('button'), // SingleRefDef<HTMLButtonElement>
|
|
391
|
+
items: r.all('li'), // MultipleRefDef<HTMLLIElement>
|
|
392
|
+
button: r.one(), // HTMLElement (un-annotated default)
|
|
393
|
+
}))
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
These flow straight through to `ctx.refs`:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
ctx.refs.display // HTMLDivElement
|
|
400
|
+
ctx.refs.save // HTMLButtonElement
|
|
401
|
+
ctx.refs.items // HTMLLIElement[]
|
|
402
|
+
ctx.refs.button // HTMLElement (the un-annotated default)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Non-HTML refs (SVG / MathML) are out of scope: the type parameter is
|
|
406
|
+
constrained to `HTMLElement`, so cast at the use site if you need one
|
|
407
|
+
(e.g. `ctx.refs.icon as unknown as SVGSVGElement`).
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
### define
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { define } from 'microtags'
|
|
414
|
+
|
|
415
|
+
define(tagName: string): ComponentBuilder
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Returns a fluent builder. Call `.setup()` to register the custom element.
|
|
419
|
+
All builder methods are chainable and fully type-inferred.
|
|
420
|
+
|
|
128
421
|
### withContexts
|
|
129
422
|
|
|
130
423
|
```ts
|
|
@@ -161,7 +454,7 @@ define('my-child')
|
|
|
161
454
|
})
|
|
162
455
|
|
|
163
456
|
// Bind a DOM property to a signal expression; auto-cleaned.
|
|
164
|
-
ctx.bind(
|
|
457
|
+
ctx.bind(() => String(ctx.props.count()), el, { prop: 'textContent' })
|
|
165
458
|
|
|
166
459
|
// Add an event listener; auto-removed on disconnect.
|
|
167
460
|
ctx.on(el, 'click', handler)
|
|
@@ -244,3 +537,7 @@ renderList(container, template, {
|
|
|
244
537
|
|
|
245
538
|
Props declared with `withProps` are native alien-signals callable signals.
|
|
246
539
|
There is no `$` prefix convention — all prop names are plain identifiers.
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
/ed3d-plan-and-execute:execute-implementation-plan /Users/nick/code/_microtags/docs/implementation-plans/2026-06-28-zod-validation-example/ /Users/nick/code/_microtags/
|
|
543
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Attrs = Record<string, undefined | null | string | number | boolean | (string | number)[]>;
|
|
2
|
+
/**
|
|
3
|
+
* Transform an object into an HTML attributes string. The object should be
|
|
4
|
+
* like `{ attributeName: value }`.
|
|
5
|
+
*
|
|
6
|
+
* @param attrs An object for the attributes.
|
|
7
|
+
* @returns {string} A string suitable for use as HTML attributes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toAttributes(attrs: Attrs): string;
|
|
10
|
+
//# sourceMappingURL=attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../src/attributes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,GAAC,IAAI,GAAC,MAAM,GAAC,MAAM,GAAC,OAAO,GAAC,CAAC,MAAM,GAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAE1F;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAE,KAAK,EAAC,KAAK,GAAE,MAAM,CAgBhD"}
|
package/dist/builders.d.ts
CHANGED
|
@@ -12,11 +12,20 @@ export type PropDSL = {
|
|
|
12
12
|
export declare const propDSL: PropDSL;
|
|
13
13
|
/**
|
|
14
14
|
* DSL object passed to the withRefs callback.
|
|
15
|
-
*
|
|
15
|
+
* Call `r.one()` for a single element or `r.all()` for multiple. Pass a
|
|
16
|
+
* CSS selector to query by it, or omit it to match `[data-ref="<key>"]`.
|
|
17
|
+
*
|
|
18
|
+
* Element typing:
|
|
19
|
+
* - `r.one()` defaults to `HTMLElement`.
|
|
20
|
+
* - `r.one<T>()` sets the type and keeps the `[data-ref]` selector.
|
|
21
|
+
* - `r.one('button')` infers the type from the tag AND queries by that
|
|
22
|
+
* tag, exactly like `document.querySelector('button')`.
|
|
16
23
|
*/
|
|
17
24
|
export type RefsDSL = {
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
one<K extends keyof HTMLElementTagNameMap>(selector: K): SingleRefDef<HTMLElementTagNameMap[K]>;
|
|
26
|
+
one<T extends HTMLElement = HTMLElement>(selector?: string): SingleRefDef<T>;
|
|
27
|
+
all<K extends keyof HTMLElementTagNameMap>(selector: K): MultipleRefDef<HTMLElementTagNameMap[K]>;
|
|
28
|
+
all<T extends HTMLElement = HTMLElement>(selector?: string): MultipleRefDef<T>;
|
|
20
29
|
};
|
|
21
30
|
export declare const refsDSL: RefsDSL;
|
|
22
31
|
/**
|
|
@@ -25,6 +34,30 @@ export declare const refsDSL: RefsDSL;
|
|
|
25
34
|
* context tokens directly in the returned record.
|
|
26
35
|
*/
|
|
27
36
|
export type ContextDSL = Record<never, never>;
|
|
37
|
+
/**
|
|
38
|
+
* Maps each declared ref key to its own name as a string literal. Exposed
|
|
39
|
+
* as the static `refs` on a registered element so server-rendered templates
|
|
40
|
+
* can fill in `data-ref` values without hardcoding the names, e.g.
|
|
41
|
+
* `data-ref="${myCounter.refs.dec}"`.
|
|
42
|
+
*/
|
|
43
|
+
export type RefNames<RefDefs extends Record<string, RefDef>> = {
|
|
44
|
+
readonly [K in keyof RefDefs]: K & string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A registered custom element class that also exposes, as static members,
|
|
48
|
+
* the tag name it was registered under (`TAG`) and the names of its
|
|
49
|
+
* declared refs (`refs`).
|
|
50
|
+
*/
|
|
51
|
+
export type MicrotagElementClass<RefDefs extends Record<string, RefDef> = Record<never, never>> = typeof HTMLElement & {
|
|
52
|
+
/** The custom element tag name, e.g. 'my-counter'. */
|
|
53
|
+
readonly TAG: string;
|
|
54
|
+
/**
|
|
55
|
+
* The declared ref names, each mapping to its own `data-ref` string.
|
|
56
|
+
* Lets server-rendered markup reference refs by key instead of a magic
|
|
57
|
+
* string: `myCounter.refs.dec`.
|
|
58
|
+
*/
|
|
59
|
+
readonly refs: RefNames<RefDefs>;
|
|
60
|
+
};
|
|
28
61
|
/**
|
|
29
62
|
* Fluent builder that accumulates props/refs/contexts config
|
|
30
63
|
* before registering a custom element via `.setup()`.
|
|
@@ -36,10 +69,16 @@ export declare class ComponentBuilder<Props extends Record<string, PropDef> = Re
|
|
|
36
69
|
private readonly _refDefs;
|
|
37
70
|
private readonly _ctxDefs;
|
|
38
71
|
constructor(tagName: string, coercers?: Record<string, Coercer<unknown>>, observed?: string[], refDefs?: Record<string, RefDef>, ctxDefs?: Record<string, ContextToken<unknown>>);
|
|
72
|
+
/**
|
|
73
|
+
* The custom element tag name, e.g. 'my-counter'. Available at every
|
|
74
|
+
* point in the fluent chain since each builder method returns a new
|
|
75
|
+
* builder carrying the same tag name.
|
|
76
|
+
*/
|
|
77
|
+
get TAG(): string;
|
|
39
78
|
withProps<P extends Record<string, PropDef>>(fn: (p: PropDSL) => P): ComponentBuilder<P, RefDefs, CtxDefs>;
|
|
40
79
|
withRefs<R extends Record<string, RefDef>>(fn: (r: RefsDSL) => R): ComponentBuilder<Props, R, CtxDefs>;
|
|
41
80
|
withContexts<C extends Record<string, ContextToken<unknown>>>(fn: (c: ContextDSL) => C): ComponentBuilder<Props, RefDefs, C>;
|
|
42
|
-
setup(fn: (ctx: SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>) => void):
|
|
81
|
+
setup(fn: (ctx: SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>) => void): MicrotagElementClass<RefDefs>;
|
|
43
82
|
}
|
|
44
83
|
/** Create a new component builder for the given tag name. */
|
|
45
84
|
export declare function define(tagName: string): ComponentBuilder;
|
package/dist/builders.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EACR,OAAO,EACP,gBAAgB,EAChB,YAAY,EACZ,OAAO,EACP,MAAM,EACN,YAAY,EACZ,cAAc,EACd,OAAO,EACP,YAAY,EACZ,WAAW,EACd,MAAM,YAAY,CAAA;AAiDnB,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG;IAClB,MAAM,
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"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"}
|