jails-js 6.9.8 → 6.9.10
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 +6 -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/element.ts +4 -0
- package/src/index.ts +5 -3
- 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
|
@@ -1105,6 +1105,9 @@ const Element$1 = ({ component, templates: templates2, start: start2 }) => {
|
|
|
1105
1105
|
}
|
|
1106
1106
|
};
|
|
1107
1107
|
};
|
|
1108
|
+
const getInstance = (node) => {
|
|
1109
|
+
return register$1.get(node);
|
|
1110
|
+
};
|
|
1108
1111
|
const config = {
|
|
1109
1112
|
tags: ["{{", "}}"]
|
|
1110
1113
|
};
|
|
@@ -1237,10 +1240,11 @@ const wrap = (open, node, close) => {
|
|
|
1237
1240
|
(_a = node.parentNode) == null ? void 0 : _a.insertBefore(open, node);
|
|
1238
1241
|
(_b = node.parentNode) == null ? void 0 : _b.insertBefore(close, node.nextSibling);
|
|
1239
1242
|
};
|
|
1243
|
+
globalThis.__jails__ = globalThis.__jails__ || { components: {} };
|
|
1244
|
+
globalThis.__jails__.getInstance = getInstance;
|
|
1240
1245
|
const templateConfig = (options) => {
|
|
1241
1246
|
templateConfig$1(options);
|
|
1242
1247
|
};
|
|
1243
|
-
globalThis.__jails__ = globalThis.__jails__ || { components: {} };
|
|
1244
1248
|
const register = (name, module, dependencies) => {
|
|
1245
1249
|
const { components } = globalThis.__jails__;
|
|
1246
1250
|
components[name] = { name, module, dependencies };
|
|
@@ -1259,6 +1263,7 @@ const start = (target) => {
|
|
|
1259
1263
|
});
|
|
1260
1264
|
};
|
|
1261
1265
|
export {
|
|
1266
|
+
getInstance,
|
|
1262
1267
|
publish,
|
|
1263
1268
|
register,
|
|
1264
1269
|
start,
|