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/anti-patterns.md +102 -0
- package/ai/api.md +388 -0
- package/ai/architecture.md +79 -0
- package/ai/best-practices.md +183 -0
- package/ai/concepts.md +228 -0
- package/ai/directives.md +556 -0
- package/ai/examples.md +199 -0
- package/ai/faq.md +70 -0
- package/ai/glossary.md +28 -0
- package/ai/json/api.json +190 -0
- package/ai/json/directives.json +107 -0
- package/ai/json/patterns.json +69 -0
- package/ai/json/recipes.json +96 -0
- package/ai/llms.txt +96 -0
- package/ai/overview.md +45 -0
- package/ai/patterns.md +182 -0
- package/ai/recipes.md +393 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/jails.js +1 -1
- package/dist/jails.js.map +1 -1
- package/package.json +24 -2
- package/readme.md +22 -9
- package/src/template-system.ts +2 -1
- package/tsconfig.json +0 -1
- package/types.d.ts +3 -3
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
|
-
|
|
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);
|