-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs.html
More file actions
651 lines (610 loc) · 35.4 KB
/
Copy pathdocs.html
File metadata and controls
651 lines (610 loc) · 35.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta
name="description"
content="Tan Compose Documentation — API reference, lifecycle, reactivity, and migration guide"
/>
<title>Tan Compose — Documentation</title>
<link rel="canonical" href="https://ra9.github.io/tan-compose/docs.html" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Documentation — Tan Compose" />
<meta property="og:description" content="API reference, lifecycle, reactivity, and migration guide for @ra9/tan-compose." />
<meta property="og:url" content="https://ra9.github.io/tan-compose/docs.html" />
<meta property="og:site_name" content="Tan Compose" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Documentation — Tan Compose" />
<meta name="twitter:description" content="API reference, lifecycle, reactivity, and migration guide for @ra9/tan-compose." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<script type="module" src="./dist/site.js"></script>
<style>
/* The kit ships theme tokens on :root, components carry their own
shadow CSS, and <site-nav>/<site-footer> handle chrome. All this
page needs is layout for the prose body, the API tables, and a
tiny set of inline-code styles. ~80 lines vs the old ~400. */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--tc-font-sans, "Inter", system-ui, sans-serif);
line-height: 1.65;
color: var(--tc-color-ink, #14171f);
background: var(--tc-color-bg, #faf8f3);
-webkit-font-smoothing: antialiased;
}
.layout {
max-width: 1100px;
margin: 0 auto;
padding: 56px 24px 80px;
display: grid;
grid-template-columns: minmax(0, 1fr) 220px;
gap: 56px;
align-items: start;
}
.doc-hero { margin-bottom: 32px; }
.doc-hero h1 {
font-size: 2.5rem; font-weight: 800;
letter-spacing: -0.02em; margin-bottom: 12px;
}
.doc-hero p {
font-size: 1.1rem; color: var(--tc-color-ink-soft, #4a5061);
max-width: 680px;
}
section {
scroll-margin-top: 80px;
padding: 36px 0;
border-top: 1px solid var(--tc-color-rule, #ece5d3);
}
section:first-of-type { border-top: none; padding-top: 0; }
section h2 {
font-size: 1.6rem; font-weight: 700;
margin-bottom: 14px; letter-spacing: -0.01em;
}
section h3 {
font-size: 1.1rem; font-weight: 600;
margin: 28px 0 10px;
color: var(--tc-color-ink, #14171f);
}
section p, section li {
color: var(--tc-color-ink-soft, #4a5061);
margin-bottom: 12px;
}
section ul, section ol { margin: 8px 0 16px 22px; }
section li { margin-bottom: 6px; }
tc-code { display: block; margin: 12px 0 16px; }
tc-callout { display: block; margin: 16px 0; }
code {
font-family: var(--tc-font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.88em;
}
p code, li code, td code, h3 code {
background: var(--tc-color-accent-soft, #efe2cf);
color: var(--tc-color-accent-hover, #8a572d);
padding: 1px 6px; border-radius: 4px; font-size: 0.85em;
}
.signature {
display: inline-block;
font-family: var(--tc-font-mono, monospace);
font-size: 0.92rem; font-weight: 500;
background: var(--tc-color-accent-soft, #efe2cf);
color: var(--tc-color-accent-hover, #8a572d);
padding: 6px 12px; border-radius: 6px;
margin: 6px 0 12px;
}
table.api {
width: 100%; border-collapse: collapse;
margin: 14px 0 8px; font-size: 0.92rem;
}
table.api th, table.api td {
text-align: left; padding: 10px 12px;
border-bottom: 1px solid var(--tc-color-rule, #ece5d3);
vertical-align: top;
}
table.api th {
font-weight: 600; background: var(--tc-color-bg, #faf8f3);
font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
color: var(--tc-color-ink, #14171f);
}
table.api td { color: var(--tc-color-ink-soft, #4a5061); }
table.api td:first-child {
white-space: nowrap;
font-family: var(--tc-font-mono, monospace);
color: var(--tc-color-ink, #14171f); font-weight: 500;
}
aside.toc-rail { padding-top: 8px; }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; gap: 24px; padding: 32px 20px 64px; }
aside.toc-rail { display: none; }
.doc-hero h1 { font-size: 1.9rem; }
}
</style>
</head>
<body>
<site-nav active="docs" version="v1.1.0"></site-nav>
<main class="layout">
<article id="content">
<div class="doc-hero">
<h1>Documentation</h1>
<p>
Everything you need to build production-ready Web Components with
Tan Compose — declarative syntax, Shadow DOM isolation,
theming via CSS variables, and reactive state.
</p>
</div>
<section id="introduction">
<h2>Introduction</h2>
<p>
<strong>Tan Compose</strong> is a lightweight, zero-dependency
library for authoring native Web Components with a declarative
API. You describe a component once with <code>describe()</code>
and register it with <code>build()</code>; the library handles
Shadow DOM setup, theming via CSS custom properties, inline
styles, lifecycle hooks, attribute reactivity, custom events,
and slots.
</p>
</section>
<section id="installation">
<h2>Installation</h2>
<p>Install from JSR for Deno or Node:</p>
<tc-code language="bash" copy><span class="tc-com"># Deno</span>
deno add jsr:@ra9/tan-compose
<span class="tc-com"># Node / npm</span>
npx jsr add @ra9/tan-compose</tc-code>
<h3>CDN / Deno import</h3>
<p>You can also import directly in Deno or the browser:</p>
<tc-code language="ts" copy><span class="tc-kw">import</span> { build, describe } <span class="tc-kw">from</span> <span class="tc-str">"https://deno.land/x/tan_compose/mod.ts"</span>;</tc-code>
</section>
<section id="quick-start">
<h2>Quick Start</h2>
<p>
Describe a component, then register it under a custom tag name.
Custom tag names must be lowercase and contain a hyphen.
</p>
<tc-code language="ts" copy><span class="tc-kw">import</span> { build, describe } <span class="tc-kw">from</span> <span class="tc-str">"@ra9/tan-compose"</span>;
<span class="tc-kw">const</span> button = describe({
tag: <span class="tc-str">"button"</span>,
template: <span class="tc-str">"Click me"</span>,
styles: {
padding: <span class="tc-str">"10px 20px"</span>,
backgroundColor: <span class="tc-str">"#5b6cf0"</span>,
color: <span class="tc-str">"white"</span>,
border: <span class="tc-str">"none"</span>,
borderRadius: <span class="tc-str">"8px"</span>,
cursor: <span class="tc-str">"pointer"</span>,
},
action: () => alert(<span class="tc-str">"Hello, Tan Compose!"</span>),
});
build(<span class="tc-str">"my-button"</span>, button);</tc-code>
<p>Then use it in HTML like any native element:</p>
<tc-code language="html"><span class="tc-tag"><my-button></my-button></span></tc-code>
</section>
<section id="api">
<h2>API Reference</h2>
<h3 id="describe">describe(options)</h3>
<div class="signature">describe(options: DescribeOptions) => DescribeOptions</div>
<p>
Validates and returns a component description. As of
<strong>v0.2.0</strong>, this function throws on invalid options
(missing <code>tag</code>, malformed <code>observedAttributes</code>, etc.).
</p>
<h3 id="build">build(tagName, description)</h3>
<div class="signature">build(tagName: string, description: DescribeOptions) => string</div>
<p>
Registers a custom element with the browser’s
<code>CustomElementRegistry</code> and returns the registered
tag name. The <code>tagName</code> must be lowercase and contain
a hyphen (e.g. <code>my-button</code>); otherwise
<code>build()</code> throws.
</p>
<h3 id="is-registered">isComponentRegistered(tagName)</h3>
<div class="signature">isComponentRegistered(tagName: string) => boolean</div>
<p>Returns <code>true</code> if a component with the given tag name has already been registered.</p>
<h3 id="registry">getRegisteredComponents()</h3>
<div class="signature">getRegisteredComponents() => string[]</div>
<p>
Returns an array of every tag name registered through
<code>build()</code> in the current document.
</p>
<h3 id="describe-options">DescribeOptions</h3>
<p>The full shape accepted by <code>describe()</code>:</p>
<table class="api" aria-label="DescribeOptions fields">
<thead>
<tr><th>Field</th><th>Type</th><th>Default</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>tag</td><td>string</td><td>—</td><td>HTML element used internally inside the Shadow DOM root (required).</td></tr>
<tr><td>theme</td><td>Record<string, string></td><td><code>{}</code></td><td>CSS custom properties exposed as <code>--name</code> variables on the host.</td></tr>
<tr><td>styles</td><td>Record<string, string></td><td><code>{}</code></td><td>Inline styles applied to the component’s root element (camelCase keys).</td></tr>
<tr><td>className</td><td>string</td><td><code>""</code></td><td>Class name added to the root element.</td></tr>
<tr><td>attributes</td><td>Record<string, string></td><td><code>{}</code></td><td>Static HTML attributes set on the root element.</td></tr>
<tr><td>template</td><td>string | TemplateFn</td><td><code>""</code></td><td>Inner HTML rendered inside the root element. Function form re-evaluates on every render.</td></tr>
<tr><td>children</td><td>DescribeOptions[]</td><td><code>[]</code></td><td>Nested component descriptions composed inside the root.</td></tr>
<tr><td>action</td><td>(event: Event) => void</td><td>—</td><td>Click handler attached to the root element.</td></tr>
<tr><td>events</td><td>Record<string, Handler></td><td><code>{}</code></td><td>Delegated event handlers keyed by <code>"<type> <selector>"</code>.</td></tr>
<tr><td>props</td><td>Record<string, PropDef></td><td><code>{}</code></td><td>Typed properties exposed on the host instance.</td></tr>
<tr><td>refs</td><td>Record<string, string></td><td><code>{}</code></td><td>Map of name → CSS selector. Populated as <code>host.refs.<name></code> after every render.</td></tr>
<tr><td>for</td><td>ListConfig</td><td>—</td><td>Keyed list rendering: <code>{ items, key, render }</code>.</td></tr>
<tr><td>if</td><td>(ctx) => boolean</td><td>—</td><td>Skip the subtree when the predicate returns false.</td></tr>
<tr><td>beforeMount</td><td>(this: HTMLElement) => void</td><td>—</td><td>Lifecycle hook called before the Shadow DOM is constructed.</td></tr>
<tr><td>afterMount</td><td>(this: HTMLElement) => void</td><td>—</td><td>Lifecycle hook called after the component is connected to the DOM.</td></tr>
<tr><td>afterRender</td><td>(this: HTMLElement) => void</td><td>—</td><td><strong>New in 1.1.0.</strong> Lifecycle hook called after every render.</td></tr>
<tr><td>unmount</td><td>(this: HTMLElement) => void</td><td>—</td><td>Lifecycle hook called on <code>disconnectedCallback</code>.</td></tr>
<tr><td>observedAttributes</td><td>string[]</td><td><code>[]</code></td><td>Explicit list of attributes that should trigger re-renders when changed.</td></tr>
<tr><td>formAssociated</td><td>boolean</td><td><code>false</code></td><td><strong>New in 0.4.0.</strong> Opt into the Form-Associated Custom Elements API.</td></tr>
</tbody>
</table>
<h3 id="instance-methods">Component instance methods</h3>
<p>Available as <code>this</code> inside lifecycle hooks and actions:</p>
<table class="api" aria-label="Component instance methods">
<thead>
<tr><th>Method</th><th>Returns</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>setState(key, value)</td><td>void</td><td>Stores a value in component state. <strong>In 0.2.0 this triggers an automatic re-render.</strong></td></tr>
<tr><td>getState(key)</td><td>unknown</td><td>Reads the current value for a state key.</td></tr>
<tr><td>emitEvent(name, data)</td><td>void</td><td>Dispatches a <code>CustomEvent</code> declared in <code>emit</code>, with <code>detail = data</code>.</td></tr>
<tr><td>render()</td><td>void</td><td>Manually re-renders the Shadow DOM. Rarely needed since <code>setState</code> handles this.</td></tr>
<tr><td>refs</td><td>Record<string, Element | null></td><td><strong>New in 0.4.0.</strong> Live references populated from the <code>refs</code> selector map.</td></tr>
<tr><td>internals</td><td>ElementInternals?</td><td><strong>New in 0.4.0.</strong> Set when <code>formAssociated: true</code>.</td></tr>
</tbody>
</table>
</section>
<section id="lifecycle">
<h2>Lifecycle</h2>
<p>Tan Compose components follow a deterministic lifecycle. Hooks run in the order below:</p>
<ol>
<li><code>beforeMount</code> — runs before the Shadow DOM is constructed. Use it to read attributes or seed state.</li>
<li>DOM construction — the Shadow DOM, root element, theme variables, styles, and children are built.</li>
<li>Mounted — the element is connected (<code>connectedCallback</code>).</li>
<li><code>afterMount</code> — runs after the component is in the DOM. Safe place to attach listeners or call <code>setState</code>.</li>
<li>Updates — an attribute change in <code>observedAttributes</code> or a <code>setState</code> call triggers a re-render.</li>
<li><code>afterRender</code> — runs after every render (initial and subsequent). Use for imperative DOM work that needs to repeat.</li>
<li><code>unmount</code> — runs on <code>disconnectedCallback</code>. Use it to clean up timers and listeners.</li>
</ol>
<tc-callout variant="note" title="Tip">
Avoid mutating the Shadow DOM directly inside <code>afterMount</code> —
prefer <code>setState</code> so subsequent updates stay consistent.
</tc-callout>
</section>
<section id="reactivity">
<h2>Reactivity</h2>
<p>There are two ways to update a Tan Compose component after it has mounted:</p>
<ul>
<li><strong>State</strong> — call <code>this.setState(key, value)</code>. As of <strong>0.2.0</strong>, every <code>setState</code> call triggers an automatic re-render of the Shadow DOM.</li>
<li><strong>Attributes</strong> — declare them explicitly in <code>observedAttributes</code>. Only attributes listed there will trigger a re-render when changed via <code>setAttribute()</code> or directly in markup.</li>
</ul>
<tc-code language="ts" copy><span class="tc-kw">const</span> counter = describe({
tag: <span class="tc-str">"div"</span>,
observedAttributes: [<span class="tc-str">"count"</span>],
template: <span class="tc-str">"0"</span>,
afterMount() {
<span class="tc-kw">this</span>.setState(<span class="tc-str">"count"</span>, <span class="tc-kw">Number</span>(<span class="tc-kw">this</span>.getAttribute(<span class="tc-str">"count"</span>) ?? 0));
},
});
build(<span class="tc-str">"my-counter"</span>, counter);</tc-code>
</section>
<section id="props">
<h2>Typed properties (0.3+)</h2>
<p>
The <code>props</code> field declares typed properties on the host element.
Setting one (<code>el.rows = […]</code>) triggers a re-render when the value differs.
Initial values come from the matching attribute (string-coerced) when present, otherwise from <code>default</code>.
</p>
<tc-code language="ts" copy>build(<span class="tc-str">"user-card"</span>, describe({
props: {
name: { type: <span class="tc-str">"string"</span>, default: <span class="tc-str">"Anonymous"</span> },
age: { type: <span class="tc-str">"number"</span>, default: 0 },
open: { type: <span class="tc-str">"boolean"</span>, default: <span class="tc-kw">false</span>, reflect: <span class="tc-kw">true</span> },
tags: { type: <span class="tc-str">"json"</span>, default: [] },
},
template: ({ props }) =>
`<h3>${props.name}, ${props.age}</h3>
${props.open ? <span class="tc-str">"<span>open</span>"</span> : <span class="tc-str">""</span>}`,
}));</tc-code>
<h3>PropDef</h3>
<table class="api">
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td>type</td><td>"string" | "number" | "boolean" | "json"</td><td>How attribute strings coerce into the runtime value.</td></tr>
<tr><td>default</td><td>unknown</td><td>Used when the attribute is absent and no value has been set.</td></tr>
<tr><td>reflect</td><td>boolean</td><td>When true, setting the property mirrors the value to the matching attribute. Ignored for <code>json</code> props.</td></tr>
</tbody>
</table>
<tc-callout variant="info" title="Note">
Prop names are also added to the underlying <code>observedAttributes</code> list,
so an external <code>setAttribute</code> still updates the property (with type coercion).
</tc-callout>
</section>
<section id="template-fn">
<h2>Function templates (0.3+)</h2>
<p>
<code>template</code> may be a function that receives the render context
and returns an HTML string. It runs on every render, so any expression
in scope (props, state) is implicitly reactive.
</p>
<tc-code language="ts" copy>describe({
props: { greeting: { type: <span class="tc-str">"string"</span>, default: <span class="tc-str">"hi"</span> } },
template: ({ props, state }) =>
`<p>${props.greeting} #${state.tick ?? 0}</p>`,
});</tc-code>
<h3>ComponentCtx</h3>
<table class="api">
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td>props</td><td>Readonly<Record<string, unknown>></td><td>Snapshot of current property values.</td></tr>
<tr><td>state</td><td>Readonly<Record<string, unknown>></td><td>Snapshot of current state values.</td></tr>
<tr><td>host</td><td>HTMLElement</td><td>The custom element instance.</td></tr>
<tr><td>refs</td><td>Record<string, Element | null></td><td>Live references from the <code>refs</code> selector map.</td></tr>
<tr><td>setState</td><td>(key, value) => void</td><td>Sets state and re-renders if the value changed.</td></tr>
<tr><td>getState</td><td><T>(key) => T | undefined</td><td>Reads a state value.</td></tr>
<tr><td>emit</td><td>(name, detail?) => void</td><td>Dispatches a bubbling, composed CustomEvent from the host.</td></tr>
</tbody>
</table>
<tc-callout variant="warning" title="Security">
Template strings are inserted via <code>innerHTML</code>. Never interpolate
untrusted input. Use the DOM API in <code>afterMount</code> if you need to
insert user content as text.
</tc-callout>
</section>
<section id="events">
<h2>Event delegation (0.3+)</h2>
<p>
The <code>events</code> field maps <code>"<event-type> <css-selector>"</code>
keys to handlers. One listener per event type is attached at the shadow root;
matches are resolved via <code>composedPath()</code>. Listeners are torn down on disconnect.
</p>
<tc-code language="ts" copy>describe({
template: `
<button class="bump">+</button>
<button class="reset">reset</button>
`,
events: {
<span class="tc-str">"click .bump"</span>: (_e, ctx) => ctx.setState(<span class="tc-str">"count"</span>, (ctx.state.count ?? 0) + 1),
<span class="tc-str">"click .reset"</span>: (_e, ctx) => ctx.setState(<span class="tc-str">"count"</span>, 0),
},
});</tc-code>
<p>
Handler signature is <code>(event: Event, ctx: ComponentCtx) => void</code>.
Selector-less keys (e.g. <code>"submit"</code>) match events that target the host directly.
</p>
</section>
<section id="for">
<h2>Keyed list rendering (0.3+)</h2>
<p>
A child describe can take <code>for: { items, key, render }</code>.
The renderer keeps a per-block cache keyed by <code>key(item)</code>
and reuses DOM nodes between renders when item identity is unchanged.
Stale items have their cleanups run.
</p>
<tc-code language="ts" copy>describe({
tag: <span class="tc-str">"tbody"</span>,
<span class="tc-kw">for</span>: {
items: ({ props }) => props.rows,
key: (row) => row.id,
render: (row) => describe({
tag: <span class="tc-str">"tr"</span>,
template: `<td>${row.name}</td><td>${row.email}</td>`,
}),
},
});</tc-code>
<h3>ListConfig</h3>
<table class="api">
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td>items</td><td>(ctx) => readonly T[]</td><td>Returns the current items. Recomputed on every parent render.</td></tr>
<tr><td>key</td><td>(item, index) => string | number</td><td>Stable identity used for DOM-node reuse.</td></tr>
<tr><td>render</td><td>(item, index, ctx) => DescribeOptions</td><td>Returns the description for one item.</td></tr>
</tbody>
</table>
<tc-callout variant="warning" title="Identity matters">
The cache reuses a node only when <code>Object.is(prevItem, newItem)</code>.
If you mutate items in place the renderer can't tell that the data changed;
replace the array (and any changed entries) with new references instead.
</tc-callout>
</section>
<section id="if">
<h2>Conditional rendering (0.3+)</h2>
<p>
A child describe can take <code>if: (ctx) => boolean</code>.
When the predicate returns false, the subtree is omitted from the
rendered output (no DOM created at all).
</p>
<tc-code language="ts" copy>describe({
tag: <span class="tc-str">"div"</span>,
className: <span class="tc-str">"empty"</span>,
template: <span class="tc-str">"No results"</span>,
<span class="tc-kw">if</span>: ({ props }) => props.rows.length === 0,
});</tc-code>
</section>
<section id="datatable">
<h2>Putting it together: a real datatable</h2>
<p>
The features above compose into a paginated, filterable, sortable
datatable. Open the
<a href="./examples.html#datatable">examples page</a> for the live
version; here is the abridged source:
</p>
<tc-code language="ts" copy>build(<span class="tc-str">"data-table"</span>, describe({
props: {
rows: { type: <span class="tc-str">"json"</span>, default: [] },
columns: { type: <span class="tc-str">"json"</span>, default: [] },
pageSize: { type: <span class="tc-str">"number"</span>, default: 10 },
},
template: ({ state, props }) => `
<input class="filter" placeholder="Search..." value="${state.q ?? <span class="tc-str">""</span>}" />
<table>
<thead><tr>
${(props.columns ?? []).map((c) =>
`<th data-col="${c.key}">${c.label}</th>`).join(<span class="tc-str">""</span>)}
</tr></thead>
</table>
`,
events: {
<span class="tc-str">"input .filter"</span>: (e, ctx) => {
ctx.setState(<span class="tc-str">"q"</span>, e.target.value);
ctx.setState(<span class="tc-str">"page"</span>, 0);
},
},
children: [
describe({
tag: <span class="tc-str">"tbody"</span>,
<span class="tc-kw">for</span>: {
items: ({ state, props }) => visibleRows(state, props),
key: (row) => row.id,
render: (row, _i, { props }) => describe({
tag: <span class="tc-str">"tr"</span>,
template: (props.columns ?? []).map((c) =>
`<td>${row[c.key]}</td>`).join(<span class="tc-str">""</span>),
}),
},
}),
],
}));</tc-code>
<p>
Because <code>for</code> is keyed by <code>row.id</code>, typing in
the search box re-uses the same row nodes — the renderer just
updates which slice is visible. No virtual DOM, no diff library: just
the platform plus a couple hundred lines of glue.
</p>
</section>
<section id="refs">
<h2>Refs (0.4+)</h2>
<p>
The <code>refs</code> field is a name → CSS-selector map.
After every render, the matching shadow-root elements are exposed
as <code>host.refs.<name></code> and <code>ctx.refs.<name></code>.
Selectors that match nothing yield <code>null</code>. Refs are re-queried
on every render so they always point at the current DOM.
</p>
<tc-code language="ts" copy>build(<span class="tc-str">"search-box"</span>, describe({
refs: { input: <span class="tc-str">".q"</span>, clear: <span class="tc-str">".x"</span> },
template: `<input class="q" /><button class="x">clear</button>`,
events: {
<span class="tc-str">"click .x"</span>: (_e, ctx) => {
<span class="tc-com">// ctx.refs.input is the live <input></span>
(ctx.refs.input <span class="tc-kw">as</span> HTMLInputElement).value = <span class="tc-str">""</span>;
(ctx.refs.input <span class="tc-kw">as</span> HTMLInputElement).focus();
},
},
}));</tc-code>
<tc-callout variant="note" title="Tip">
Use refs whenever you find yourself calling <code>this.shadowRoot.querySelector(...)</code>
in <code>afterMount</code>. Refs are cheaper, declarative, and stay in sync with re-renders automatically.
</tc-callout>
</section>
<section id="form-associated">
<h2>Form-Associated Custom Elements (0.4+)</h2>
<p>
Set <code>formAssociated: true</code> and the host calls
<code>attachInternals()</code> automatically. If you declare a
<code>value</code> prop, its setter syncs to <code>internals.setFormValue</code>
so the host participates in form submissions and the validity API.
</p>
<tc-code language="ts" copy>build(<span class="tc-str">"tc-input"</span>, describe({
formAssociated: <span class="tc-kw">true</span>,
props: {
value: { type: <span class="tc-str">"string"</span>, default: <span class="tc-str">""</span> },
placeholder: { type: <span class="tc-str">"string"</span>, default: <span class="tc-str">""</span> },
},
template: ({ props }) =>
`<input class="q" placeholder="${props.placeholder}" value="${props.value}" />`,
refs: { input: <span class="tc-str">".q"</span> },
events: {
<span class="tc-str">"input .q"</span>: (e, ctx) => {
ctx.host.value = (e.target <span class="tc-kw">as</span> HTMLInputElement).value;
},
},
formResetCallback() {
<span class="tc-com">// fired when the owning <form> is reset</span>
(<span class="tc-kw">this</span> <span class="tc-kw">as</span> <span class="tc-kw">unknown</span> <span class="tc-kw">as</span> { value: <span class="tc-kw">string</span> }).value = <span class="tc-str">""</span>;
},
}));</tc-code>
<p>
The internals are exposed at <code>host.internals</code> so you can
call <code>setValidity()</code>, <code>setFormValue()</code> directly,
or check <code>internals.form</code>.
</p>
<h3>Lifecycle hooks</h3>
<table class="api">
<thead><tr><th>Hook</th><th>Fires on</th></tr></thead>
<tbody>
<tr><td>formAssociatedCallback(form)</td><td>Element associated with a form (or moved between forms).</td></tr>
<tr><td>formDisabledCallback(disabled)</td><td>Disabled state changes (e.g. via parent fieldset).</td></tr>
<tr><td>formResetCallback()</td><td>Owning form is reset.</td></tr>
<tr><td>formStateRestoreCallback(state, mode)</td><td>State restored on history navigation or autocomplete.</td></tr>
</tbody>
</table>
</section>
<section id="adopted-styles">
<h2>Adopted stylesheets (0.4+)</h2>
<p>
<code>theme</code> and <code>styles</code> are compiled into
<code>CSSStyleSheet</code> objects <strong>once per registered tag</strong>
and applied to each instance via <code>shadowRoot.adoptedStyleSheets</code>.
100 instances of the same tag share 1–2 sheets instead of 100 inline
<code><style></code> elements.
</p>
<p>
Falls back to per-instance <code><style></code> tags when
constructable stylesheets aren't available. The change is invisible
to user code — same CSS, same theming, less work for the renderer.
</p>
<tc-callout variant="info" title="When this matters">
A long list using <code>for: { ... }</code> with rows that are
themselves custom elements. Before 0.4, every row got its own copy
of the stylesheet; now they share.
</tc-callout>
</section>
<section id="migration">
<h2>Migration: 0.1.x → 0.2.0</h2>
<p>Version 0.2.0 is a minor release with breaking changes. Review each item below before upgrading:</p>
<ul>
<li><strong>Attribute reactivity is now opt-in.</strong> Add a string array to <code>observedAttributes</code> to receive re-renders. The old behavior of inferring observed attributes from the <code>attributes</code> keys has been removed.</li>
<li><strong><code>setState</code> auto-renders.</strong> You no longer need to call <code>render()</code> manually after updating state.</li>
<li><strong><code>describe()</code> validates input.</strong> Invalid options (missing <code>tag</code>, non-array <code>observedAttributes</code>, non-function lifecycle hooks) now throw at description time.</li>
<li><strong><code>build()</code> validates the tag name.</strong> The first argument must be lowercase and contain a hyphen. Names like <code>MyButton</code> or <code>button</code> are rejected.</li>
<li><strong>New <code>unmount</code> lifecycle hook.</strong> Pair it with <code>afterMount</code> for cleanup on <code>disconnectedCallback</code>.</li>
</ul>
</section>
<section id="migration-0-3">
<h2>Migration: 0.2.x → 0.3.0</h2>
<p>Version 0.3.0 is additive — existing 0.2.x code continues to work. To opt in to the new features:</p>
<ul>
<li><strong>Pass complex data via properties, not attributes.</strong> Add a <code>props</code> map and assign on the instance: <code>el.rows = […]</code>. JSON props are parsed from attributes when present and serialized only on read.</li>
<li><strong>Use function templates for derived text.</strong> Replace template strings that hard-code values with <code>template: ({ props, state }) => "..."</code> so changes flow through automatically.</li>
<li><strong>Replace manual <code>afterMount</code> wiring with <code>events</code>.</strong> Selector-keyed handlers attach once and clean up on disconnect, no per-render <code>addEventListener</code> calls.</li>
<li><strong>Use <code>for</code> for any list of three or more items.</strong> It's the only path to keyed DOM reuse, which matters the moment you have a filter input over a list.</li>
</ul>
</section>
<section id="migration-0-4">
<h2>Migration: 0.3.x → 0.4.0</h2>
<p>Additive release. Existing 0.3.x code continues to work. Opt in to the new features as you need them:</p>
<ul>
<li><strong>Replace <code>this.shadowRoot.querySelector</code> with <code>refs</code>.</strong> Declare the selectors once, get fresh references on every render.</li>
<li><strong>Add <code>formAssociated: true</code> + a <code>value</code> prop</strong> to anything that represents a form field. It will start submitting with <code><form></code> automatically.</li>
<li><strong>No code change is required for adopted stylesheets</strong> — the renderer picks the optimal path automatically wherever <code>CSSStyleSheet.replaceSync</code> is supported.</li>
</ul>
</section>
<section id="migration-1-0">
<h2>Migration: 0.4.x → 1.0.0</h2>
<p>The 1.0 release is mostly additive. One subtle change to be aware of:</p>
<ul>
<li><strong><code>for:</code> now wraps in an element.</strong> If you had a child describe with ONLY <code>for:</code> (no other fields) and relied on items appearing ungrouped, they're now wrapped in a <code><div></code> by default. Set <code>tag</code> to override (e.g. <code>tag: "tbody"</code> when the parent is a <code><table></code>).</li>
<li><strong><code>children</code> + <code>for:</code> can coexist.</strong> Code that caught the old <em>"cannot set both"</em> TypeError can drop the special case.</li>
</ul>
</section>
</article>
<aside class="toc-rail">
<tc-toc target="#content" levels="h2,h3" label="On this page"></tc-toc>
</aside>
</main>
<site-footer></site-footer>
</body>
</html>