jails-js 6.9.7 → 6.9.9

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/ai/recipes.md ADDED
@@ -0,0 +1,393 @@
1
+ # Recipes
2
+
3
+ ## Counter
4
+
5
+ Explanation: minimal local state update with delegated events and `html-inner`.
6
+
7
+ ```html
8
+ <app-counter>
9
+ <button data-subtract>-</button>
10
+ <span html-inner="counter">0</span>
11
+ <button data-add>+</button>
12
+ </app-counter>
13
+ ```
14
+
15
+ ```ts
16
+ export default function appCounter({ main, on, state }) {
17
+ main(() => {
18
+ on('click', '[data-add]', add)
19
+ on('click', '[data-subtract]', subtract)
20
+ })
21
+
22
+ const add = () => {
23
+ state.set(s => {
24
+ s.counter += 1
25
+ })
26
+ }
27
+
28
+ const subtract = () => {
29
+ state.set(s => {
30
+ s.counter -= 1
31
+ })
32
+ }
33
+ }
34
+
35
+ export const model = {
36
+ counter: 0
37
+ }
38
+ ```
39
+
40
+ Rendering explanation: `html-inner="counter"` replaces fallback `0` after mount and updates after each `state.set()`.
41
+
42
+ Performance notes: one listener per event type at the component root; no per-button rebinding.
43
+
44
+ Common mistakes: omitting `state` from controller helpers; reading DOM text as source of truth instead of state.
45
+
46
+ ## Loading and Error States
47
+
48
+ ```html
49
+ <app-users>
50
+ <p html-if="loading">Loading</p>
51
+ <p html-if="error">{{ error.message }}</p>
52
+ <ul html-if="!loading && !error">
53
+ <li html-for="user in users">{{ user.name }}</li>
54
+ </ul>
55
+ </app-users>
56
+ ```
57
+
58
+ ```ts
59
+ export default function appUsers({ main, state, dependencies }) {
60
+ const { http } = dependencies
61
+
62
+ main(() => {
63
+ load()
64
+ })
65
+
66
+ const load = () => {
67
+ state.set({ loading: true, error: null })
68
+ http.get('/users')
69
+ .then(users => state.set({ users, loading: false }))
70
+ .catch(error => state.set({ error, loading: false }))
71
+ }
72
+ }
73
+
74
+ export const model = {
75
+ users: [],
76
+ loading: false,
77
+ error: null
78
+ }
79
+ ```
80
+
81
+ Rendering explanation: mutually exclusive `html-if` regions create/remove the visible branch.
82
+
83
+ Performance notes: fetch never runs from the template. The loop only renders when not loading and without error.
84
+
85
+ Common mistakes: storing formatted error strings only; keep the original error if handlers need it.
86
+
87
+ ## Modal
88
+
89
+ ```html
90
+ <app-modal>
91
+ <button data-open>Open</button>
92
+ <section html-if="open" role="dialog" aria-modal="true">
93
+ <h2>{{ title }}</h2>
94
+ <button data-close>Close</button>
95
+ </section>
96
+ </app-modal>
97
+ ```
98
+
99
+ ```ts
100
+ export default function appModal({ main, on, state }) {
101
+ main(() => {
102
+ on('click', '[data-open]', () => state.set({ open: true }))
103
+ on('click', '[data-close]', () => state.set({ open: false }))
104
+ })
105
+ }
106
+
107
+ export const model = {
108
+ open: false,
109
+ title: 'Dialog'
110
+ }
111
+ ```
112
+
113
+ Rendering explanation: modal DOM is created only when `open` is truthy and removed when false.
114
+
115
+ Performance notes: fine for small dialogs. For heavy dialog content that should preserve input state, use CSS visibility or place form controls under `html-static` as appropriate.
116
+
117
+ Common mistakes: expecting internal form values to persist across `html-if` removal.
118
+
119
+ ## Tabs
120
+
121
+ ```html
122
+ <app-tabs>
123
+ <nav>
124
+ <button data-tab="profile" html-class="activeTab === 'profile' ? 'active' : ''">Profile</button>
125
+ <button data-tab="billing" html-class="activeTab === 'billing' ? 'active' : ''">Billing</button>
126
+ </nav>
127
+ <section html-if="activeTab === 'profile'">Profile content</section>
128
+ <section html-if="activeTab === 'billing'">Billing content</section>
129
+ </app-tabs>
130
+ ```
131
+
132
+ ```ts
133
+ export default function appTabs({ main, on, state }) {
134
+ main(() => {
135
+ on('click', '[data-tab]', select)
136
+ })
137
+
138
+ const select = e => {
139
+ state.set({ activeTab: e.delegateTarget.dataset.tab })
140
+ }
141
+ }
142
+
143
+ export const model = {
144
+ activeTab: 'profile'
145
+ }
146
+ ```
147
+
148
+ Rendering explanation: button classes and panel branches derive from one state property.
149
+
150
+ Performance notes: no listener per tab panel. Avoid rendering large inactive panels repeatedly.
151
+
152
+ Common mistakes: reading `event.target.dataset.tab`; nested elements can make `event.target` wrong. Use `delegateTarget`.
153
+
154
+ ## Accordion
155
+
156
+ ```html
157
+ <app-accordion>
158
+ <article html-for="item in items">
159
+ <button data-toggle html-data-index="$index">{{ item.title }}</button>
160
+ <div html-if="openIndex === $index">{{ item.body }}</div>
161
+ </article>
162
+ </app-accordion>
163
+ ```
164
+
165
+ ```ts
166
+ export default function appAccordion({ main, on, state }) {
167
+ main(() => {
168
+ on('click', '[data-toggle]', toggle)
169
+ })
170
+
171
+ const toggle = e => {
172
+ const index = Number(e.delegateTarget.dataset.index)
173
+ state.set(s => {
174
+ s.openIndex = s.openIndex === index ? -1 : index
175
+ })
176
+ }
177
+ }
178
+
179
+ export const model = {
180
+ openIndex: -1,
181
+ items: []
182
+ }
183
+ ```
184
+
185
+ Rendering explanation: each repeated item receives `$index`; only matching content branch renders.
186
+
187
+ Performance notes: for long accordions, do not put heavy widgets inside every body unless guarded or static.
188
+
189
+ Common mistakes: not converting dataset strings to numbers.
190
+
191
+ ## Fetch API With Service Dependency
192
+
193
+ ```ts
194
+ // main.ts
195
+ register('app-posts', appPosts, { http })
196
+ ```
197
+
198
+ ```ts
199
+ export default function appPosts({ main, state, dependencies }) {
200
+ const { http } = dependencies
201
+
202
+ main(() => {
203
+ load()
204
+ })
205
+
206
+ const load = async () => {
207
+ state.set({ loading: true })
208
+ const posts = await http.get('/posts')
209
+ state.set({ posts, loading: false })
210
+ }
211
+ }
212
+
213
+ export const model = {
214
+ posts: [],
215
+ loading: false
216
+ }
217
+ ```
218
+
219
+ Rendering explanation: state changes drive any `html-if` and `html-for` bound to `loading` and `posts`.
220
+
221
+ Performance notes: dependency injection keeps generic components free of unused service code.
222
+
223
+ Common mistakes: importing app services inside reusable library components.
224
+
225
+ ## Optimistic Updates
226
+
227
+ ```ts
228
+ export default function appTodos({ main, on, state, dependencies }) {
229
+ const { todosApi } = dependencies
230
+
231
+ main(() => {
232
+ on('click', '[data-toggle]', toggle)
233
+ })
234
+
235
+ const toggle = async e => {
236
+ const id = e.delegateTarget.dataset.id
237
+ const previous = state.get().todos
238
+ const todos = previous.map(todo =>
239
+ todo.id === id ? { ...todo, done: !todo.done } : todo
240
+ )
241
+ await state.set({ todos })
242
+
243
+ try {
244
+ await todosApi.toggle(id)
245
+ } catch (error) {
246
+ state.set({ todos: previous, error })
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ Rendering explanation: UI updates before the API resolves; rollback restores previous state on failure.
253
+
254
+ Performance notes: clone changed collections instead of mutating external references.
255
+
256
+ Common mistakes: losing the previous state snapshot before the optimistic write.
257
+
258
+ ## Nested Templates
259
+
260
+ ```html
261
+ <app-page>
262
+ <h1>Server-rendered heading</h1>
263
+ <template>
264
+ <section html-if="user">
265
+ Welcome, {{ user.name }}
266
+ </section>
267
+ </template>
268
+ </app-page>
269
+ ```
270
+
271
+ Rendering explanation: native `<template>` prevents unresolved mustache markers from flashing before data exists.
272
+
273
+ Performance notes: use this for correctness of first paint; it is not a substitute for limiting render work.
274
+
275
+ Common mistakes: hiding content that should be visible at first paint.
276
+
277
+ ## Debounced Input
278
+
279
+ ```html
280
+ <app-search>
281
+ <input type="search" html-static>
282
+ <p html-if="loading">Searching</p>
283
+ <ul>
284
+ <li html-for="item in results">{{ item.label }}</li>
285
+ </ul>
286
+ </app-search>
287
+ ```
288
+
289
+ ```ts
290
+ export default function appSearch({ main, on, state, dependencies }) {
291
+ const { search } = dependencies
292
+ let timer
293
+
294
+ main(() => {
295
+ on('input', 'input[type=search]', schedule)
296
+ })
297
+
298
+ const schedule = e => {
299
+ const query = e.delegateTarget.value
300
+ clearTimeout(timer)
301
+ timer = setTimeout(() => run(query), 250)
302
+ }
303
+
304
+ const run = async query => {
305
+ state.set({ loading: true })
306
+ const results = await search(query)
307
+ state.set({ results, loading: false })
308
+ }
309
+ }
310
+
311
+ export const model = {
312
+ results: [],
313
+ loading: false
314
+ }
315
+ ```
316
+
317
+ Rendering explanation: input value is browser-owned through `html-static`; search results are Jails-owned state.
318
+
319
+ Performance notes: debounce avoids one network request and render per keystroke.
320
+
321
+ Common mistakes: binding input value to state when no other render logic needs it.
322
+
323
+ ## Intersection Observer Lazy Rendering
324
+
325
+ ```ts
326
+ export default function lazyPanel({ main, elm, state, unmount }) {
327
+ let observer
328
+
329
+ main(() => {
330
+ observer = new IntersectionObserver(entries => {
331
+ if (entries.some(entry => entry.isIntersecting)) {
332
+ state.set({ visible: true })
333
+ observer.disconnect()
334
+ }
335
+ })
336
+ observer.observe(elm)
337
+ })
338
+
339
+ unmount(() => {
340
+ if (observer) observer.disconnect()
341
+ })
342
+ }
343
+
344
+ export const model = {
345
+ visible: false
346
+ }
347
+ ```
348
+
349
+ ```html
350
+ <lazy-panel>
351
+ <section html-if="visible">Expensive content</section>
352
+ </lazy-panel>
353
+ ```
354
+
355
+ Rendering explanation: expensive content is not created until the component enters the viewport.
356
+
357
+ Performance notes: cleanup prevents observers from retaining detached elements.
358
+
359
+ Common mistakes: forgetting `unmount()`.
360
+
361
+ ## Virtual List Boundary
362
+
363
+ ```html
364
+ <app-virtual-list>
365
+ <div class="virtual-list" html-static></div>
366
+ <p>Total: {{ total }}</p>
367
+ </app-virtual-list>
368
+ ```
369
+
370
+ ```ts
371
+ export default function appVirtualList({ main, elm, state, dependencies }) {
372
+ const { createVirtualList } = dependencies
373
+ const target = elm.querySelector('.virtual-list')
374
+ let list
375
+
376
+ main(() => {
377
+ list = createVirtualList(target, {
378
+ onCountChange: total => state.set({ total })
379
+ })
380
+ })
381
+ }
382
+
383
+ export const model = {
384
+ total: 0
385
+ }
386
+ ```
387
+
388
+ Rendering explanation: the virtual list library owns its DOM; Jails renders surrounding counters/state.
389
+
390
+ Performance notes: `html-static` prevents Jails diffing a large, imperatively managed list.
391
+
392
+ Common mistakes: putting `html-for` inside a virtualized area managed by another library.
393
+
package/dist/index.js CHANGED
@@ -1187,7 +1187,8 @@ const transformTemplate = (clone) => {
1187
1187
  }
1188
1188
  if (htmlClass) {
1189
1189
  element.removeAttribute("html-class");
1190
- element.className = (element.className + ` %%_=${htmlClass}_%%`).trim();
1190
+ const value = element.getAttribute("class") || "";
1191
+ element.setAttribute("class", `${value} %%_=${htmlClass}_%%`);
1191
1192
  }
1192
1193
  if (element.localName === "template") {
1193
1194
  transformTemplate(element.content);