laghu 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +770 -2
- package/dist/laghu.d.ts +109 -0
- package/dist/laghu.min.js +1 -0
- package/dist/morph.min.js +1 -0
- package/package.json +42 -4
- package/src/laghu.js +1265 -0
- package/src/morph.js +188 -0
package/README.md
CHANGED
|
@@ -1,3 +1,771 @@
|
|
|
1
|
-
#
|
|
1
|
+
# laghu.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
laghu.js is a tiny front-end helper for small pages. It adds reactivity,
|
|
4
|
+
reusable components, and declarative data fetching to plain HTML, using
|
|
5
|
+
only the browser. There is no build step, no JSX, and no TypeScript
|
|
6
|
+
compiler. You add one script tag and write normal HTML.
|
|
7
|
+
|
|
8
|
+
The name "laghu" is a Sanskrit word for light and small. That is the
|
|
9
|
+
whole idea: a light tool for pages that do not need a heavy framework.
|
|
10
|
+
|
|
11
|
+
Contents:
|
|
12
|
+
|
|
13
|
+
- [1. When to use laghu](#1-when-to-use-laghu)
|
|
14
|
+
- [2. Quick start](#2-quick-start)
|
|
15
|
+
- [3. Loading from a CDN](#3-loading-from-a-cdn)
|
|
16
|
+
- [4. Core concepts](#4-core-concepts)
|
|
17
|
+
- [5. HTML attribute reference](#5-html-attribute-reference)
|
|
18
|
+
- [6. JavaScript API reference](#6-javascript-api-reference)
|
|
19
|
+
- [7. Events reference](#7-events-reference)
|
|
20
|
+
- [8. Fetching data (JSON and HTML)](#8-fetching-data-json-and-html)
|
|
21
|
+
- [9. The morph plugin](#9-the-morph-plugin)
|
|
22
|
+
- [10. Type safety without TypeScript](#10-type-safety-without-typescript)
|
|
23
|
+
- [11. Content Security Policy notes](#11-content-security-policy-notes)
|
|
24
|
+
- [12. Loading flash and SEO notes](#12-loading-flash-and-seo-notes)
|
|
25
|
+
- [13. File sizes](#13-file-sizes)
|
|
26
|
+
- [14. Worked examples](#14-worked-examples)
|
|
27
|
+
- [15. Checking your setup](#15-checking-your-setup)
|
|
28
|
+
- [16. Troubleshooting](#16-troubleshooting)
|
|
29
|
+
- [17. Project files](#17-project-files)
|
|
30
|
+
|
|
31
|
+
## 1. When to use laghu
|
|
32
|
+
|
|
33
|
+
Use laghu when your page is mostly static HTML with a few interactive
|
|
34
|
+
parts: counters, toggles, tabs, a list rendered from JSON, a form that
|
|
35
|
+
posts and shows a result, a search box with live results.
|
|
36
|
+
|
|
37
|
+
Do not use laghu when you need client-side routing with auth guards,
|
|
38
|
+
offline sync, huge editable tables, canvas editors, or a large team
|
|
39
|
+
hiring for React or Vue skills. At that size, graduate to Svelte,
|
|
40
|
+
Solid, Astro islands, React, or Vue.
|
|
41
|
+
|
|
42
|
+
## 2. Quick start
|
|
43
|
+
|
|
44
|
+
Step 1. Save this as index.html and open it by double-clicking
|
|
45
|
+
(no server needed):
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<!doctype html>
|
|
49
|
+
<meta charset="utf-8">
|
|
50
|
+
<title>counter</title>
|
|
51
|
+
<style>
|
|
52
|
+
counter-widget:not(:defined) { visibility: hidden; }
|
|
53
|
+
</style>
|
|
54
|
+
|
|
55
|
+
<counter-widget>
|
|
56
|
+
<p>Count: <strong data-bind="count">0</strong></p>
|
|
57
|
+
<button data-click="increment">Add one</button>
|
|
58
|
+
</counter-widget>
|
|
59
|
+
|
|
60
|
+
<script src="laghu.min.js"></script>
|
|
61
|
+
<script>
|
|
62
|
+
Laghu.define('counter-widget', function () {
|
|
63
|
+
return {
|
|
64
|
+
state: { count: 0 },
|
|
65
|
+
actions: {
|
|
66
|
+
increment: function () { this.count++; }
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Step 2. Open the file in a browser. The button adds one per click.
|
|
74
|
+
|
|
75
|
+
What happened: `Laghu.define` registered a real custom element.
|
|
76
|
+
When the element connected, laghu made `{ count: 0 }` reactive,
|
|
77
|
+
mapped `data-bind="count"` to the `strong` node, and attached the
|
|
78
|
+
click handler. When state changes, only that `strong` node updates.
|
|
79
|
+
|
|
80
|
+
## 3. Loading from a CDN
|
|
81
|
+
|
|
82
|
+
A direct GitHub link does not work as a script source. A page address
|
|
83
|
+
like `github.com/.../blob/...` serves HTML, and
|
|
84
|
+
`raw.githubusercontent.com` serves plain text, which browsers refuse
|
|
85
|
+
to run as a script.
|
|
86
|
+
|
|
87
|
+
Use jsDelivr instead. It serves GitHub files with the correct
|
|
88
|
+
JavaScript content type:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/laghu.min.js"></script>
|
|
92
|
+
<script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/morph.min.js"></script>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Rules for production pages:
|
|
96
|
+
|
|
97
|
+
- Always pin a version (`@v1.0.0`). Never use `@master`, because
|
|
98
|
+
branch content moves and caches shift under you.
|
|
99
|
+
- When a new laghu release comes out, move the version number
|
|
100
|
+
forward deliberately after testing.
|
|
101
|
+
- For extra safety, add a Subresource Integrity hash to the tag so
|
|
102
|
+
the browser rejects a changed file.
|
|
103
|
+
- For local development with no internet, download the two files
|
|
104
|
+
next to your HTML and reference them relatively:
|
|
105
|
+
`<script src="laghu.min.js"></script>`.
|
|
106
|
+
|
|
107
|
+
Long term, publishing the `laghu` package to npm (the name is free
|
|
108
|
+
at the time of writing) unlocks versioned CDN paths such as
|
|
109
|
+
`cdn.jsdelivr.net/npm/laghu@1.0.0/...` plus automated update tools.
|
|
110
|
+
|
|
111
|
+
## 4. Core concepts
|
|
112
|
+
|
|
113
|
+
State is a plain object. Actions are plain functions that run with
|
|
114
|
+
the state as `this`. Bindings are HTML attributes that connect state
|
|
115
|
+
to the DOM. Scopes decide which state a binding reads.
|
|
116
|
+
|
|
117
|
+
There are two ways to use laghu, sharing one engine:
|
|
118
|
+
|
|
119
|
+
- Islands: any element with `data-scope='{"count": 0}'` becomes an
|
|
120
|
+
independent reactive area, hydrated automatically on page load.
|
|
121
|
+
Good for sprinkling behavior onto server-rendered pages.
|
|
122
|
+
- Components: `Laghu.define('my-tag', setup)` registers a custom
|
|
123
|
+
element. Each instance gets fresh state from `setup()`. Good for
|
|
124
|
+
repeated widgets.
|
|
125
|
+
|
|
126
|
+
Effects are the glue. Each binding creates one effect that re-runs
|
|
127
|
+
when state it read changes. Writes batch in a microtask, so ten
|
|
128
|
+
synchronous writes cause one DOM update.
|
|
129
|
+
|
|
130
|
+
Cleanup is automatic. Every binding registers its teardown on its
|
|
131
|
+
element. Removing nodes, swapping HTML, or disconnecting a component
|
|
132
|
+
stops effects, detaches listeners, clears timers and observers, and
|
|
133
|
+
aborts in-flight fetches.
|
|
134
|
+
|
|
135
|
+
## 5. HTML attribute reference
|
|
136
|
+
|
|
137
|
+
### data-scope
|
|
138
|
+
|
|
139
|
+
Marks an island and carries its initial state as JSON (double quotes
|
|
140
|
+
required, because JSON keeps the parser small and CSP-safe).
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<div data-scope='{"count": 0, "user": {"name": "Ada"}}'>
|
|
144
|
+
<span data-bind="count"></span>
|
|
145
|
+
</div>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Islands hydrate automatically when the document is ready. To hydrate
|
|
149
|
+
manually and keep the state handle:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
var root = document.querySelector('[data-scope]');
|
|
153
|
+
var state = Laghu.hydrate(root, { count: 0 });
|
|
154
|
+
state.count = 5; // bound nodes update shortly after
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### data-bind and data-text
|
|
158
|
+
|
|
159
|
+
One-way text binding. Same behavior, two spellings. Sets
|
|
160
|
+
`textContent`, so values are never parsed as HTML (safe by default).
|
|
161
|
+
|
|
162
|
+
```html
|
|
163
|
+
<span data-bind="user.name"></span>
|
|
164
|
+
<span data-text="count"></span>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### data-html
|
|
168
|
+
|
|
169
|
+
Renders a value as HTML. Explicit opt-in because HTML parsing is an
|
|
170
|
+
XSS risk. Content passes through the sanitize hook (see
|
|
171
|
+
`Laghu.sanitize`). Prefer `data-bind` unless you trust the source.
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<div data-html="post.body"></div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### data-bind:attr
|
|
178
|
+
|
|
179
|
+
Binds any attribute to an expression. Removing values (`false`,
|
|
180
|
+
`null`, `undefined`) remove the attribute.
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<a data-bind:href="user.url">profile</a>
|
|
184
|
+
<img data-bind:src="photo.src" data-bind:alt="photo.alt">
|
|
185
|
+
<button data-bind:disabled="!form.valid">Save</button>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### data-class:name
|
|
189
|
+
|
|
190
|
+
Toggles one CSS class by truthiness.
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<button data-class:active="tab === 'home'">Home</button>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### data-style:prop
|
|
197
|
+
|
|
198
|
+
Sets one inline style property. Falsey values clear it.
|
|
199
|
+
|
|
200
|
+
```html
|
|
201
|
+
<div data-style:color="error ? 'red' : ''">message</div>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### data-model
|
|
205
|
+
|
|
206
|
+
Two-way form binding. Supported: text, number, checkbox (boolean or
|
|
207
|
+
string array for groups sharing one path), radio, single select,
|
|
208
|
+
multi select, textarea. State writes skip a focused text field whose
|
|
209
|
+
value already matches, so typing never loses the caret.
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<input data-model="form.q" placeholder="search">
|
|
213
|
+
<input type="checkbox" data-model="form.agree">
|
|
214
|
+
<input type="checkbox" value="a" data-model="form.tags">
|
|
215
|
+
<input type="checkbox" value="b" data-model="form.tags">
|
|
216
|
+
<input type="radio" value="r" data-model="form.color">
|
|
217
|
+
<select data-model="form.color"><option value="r">red</option></select>
|
|
218
|
+
<select multiple data-model="form.colors">...</select>
|
|
219
|
+
<input type="number" data-model="form.qty">
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### data-show
|
|
223
|
+
|
|
224
|
+
Toggles `hidden` by truthiness. State and focus are kept because the
|
|
225
|
+
node stays in the document.
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<p data-show="loading">Loading...</p>
|
|
229
|
+
<p data-show="todos.length === 0">Nothing here yet.</p>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### data-if on an element
|
|
233
|
+
|
|
234
|
+
Same hidden toggle as `data-show`, for template-style conditions on
|
|
235
|
+
plain elements.
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<p data-if="user.isAdmin">Admin panel link</p>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### template data-if
|
|
242
|
+
|
|
243
|
+
Real attach and detach for a block. Content after the anchor inserts
|
|
244
|
+
when true and removes when false, running cleanups on removal.
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<template data-if="cart.open">
|
|
248
|
+
<aside>Your cart...</aside>
|
|
249
|
+
</template>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### template data-for and data-key
|
|
253
|
+
|
|
254
|
+
Renders a list by cloning the template per item. Syntax:
|
|
255
|
+
`data-for="item in list"` or `data-for="item, i in list"`. Each row
|
|
256
|
+
gets a child scope where `item` (and `i`) are visible alongside the
|
|
257
|
+
parent scope.
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
<ul>
|
|
261
|
+
<template data-for="todo in todos" data-key="id">
|
|
262
|
+
<li>
|
|
263
|
+
<input type="checkbox" data-model="todo.done">
|
|
264
|
+
<span data-bind="todo.title"></span>
|
|
265
|
+
</li>
|
|
266
|
+
</template>
|
|
267
|
+
</ul>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Always add `data-key` when rows hold inputs or components. Keyed rows
|
|
271
|
+
reuse DOM nodes across reorder, so focus and typed text survive.
|
|
272
|
+
Unkeyed lists patch by index and are fine for read-only rows under
|
|
273
|
+
about a hundred items.
|
|
274
|
+
|
|
275
|
+
### data-click
|
|
276
|
+
|
|
277
|
+
Runs an action on click. Forms, in order: a state helper
|
|
278
|
+
(`count++`, `count--`, `path = expr`), a named action
|
|
279
|
+
(`increment` or `increment()`), or any expression resolving to a
|
|
280
|
+
function. Accepts `data-trigger` modifiers (see section 5).
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<button data-click="increment">Add</button>
|
|
284
|
+
<button data-click="count++">Add (no action needed)</button>
|
|
285
|
+
<button data-click="q = ''">Clear</button>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Actions run with state as `this`:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
Laghu.define('x-y', function () {
|
|
292
|
+
return {
|
|
293
|
+
state: { count: 0 },
|
|
294
|
+
actions: {
|
|
295
|
+
increment: function () { this.count++; }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Unknown names warn in the console during development and dispatch
|
|
302
|
+
`laghu:unknown-action` on document, so typos fail loudly.
|
|
303
|
+
|
|
304
|
+
### data-on:event
|
|
305
|
+
|
|
306
|
+
Any DOM event, with optional `.prevent`, `.stop`, `.once` suffixes.
|
|
307
|
+
The event is visible inside as `$event`.
|
|
308
|
+
|
|
309
|
+
```html
|
|
310
|
+
<form data-on:submit.prevent="save">...</form>
|
|
311
|
+
<input data-on:input="q = $event.target.value">
|
|
312
|
+
<div data-on:keydown.enter="send">...</div>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Note: `.enter` style key filters are not built in. Branch on
|
|
316
|
+
`$event.key` inside the action instead.
|
|
317
|
+
|
|
318
|
+
### data-init
|
|
319
|
+
|
|
320
|
+
Runs once per hydration as an action string. Useful for seeding a
|
|
321
|
+
row scope or kicking off a load.
|
|
322
|
+
|
|
323
|
+
```html
|
|
324
|
+
<div data-scope='{}' data-init="load">...</div>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### data-get, data-post, data-put, data-patch, data-delete
|
|
328
|
+
|
|
329
|
+
Declarative fetch. The attribute value is the URL. `data-method`
|
|
330
|
+
overrides the method. See section 8 for the full fetch guide.
|
|
331
|
+
|
|
332
|
+
```html
|
|
333
|
+
<button data-get="/api/user" data-target="#out">Load</button>
|
|
334
|
+
<form data-post="/api/save" data-target="#msg" data-swap="inner">
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### data-target, data-swap, data-select, data-vals, data-headers
|
|
338
|
+
|
|
339
|
+
Fetch companions:
|
|
340
|
+
|
|
341
|
+
- `data-target="#id"`: where the HTML goes. Default is the element.
|
|
342
|
+
- `data-swap`: how it goes in. One of `inner`, `outer`, `text`,
|
|
343
|
+
`append` (also `beforeend`), `prepend` (also `afterbegin`),
|
|
344
|
+
`beforebegin`, `afterend`, `delete`, `none`, `morph:inner`,
|
|
345
|
+
`morph:outer`. Default is `inner`.
|
|
346
|
+
- `data-select="#part"`: picks one selector out of an HTML response
|
|
347
|
+
before swapping, for endpoints returning full pages.
|
|
348
|
+
- `data-vals`: JSON params (`'{"q": "x"}'`), or the word `form`
|
|
349
|
+
to serialize the enclosing form. GET appends a query string,
|
|
350
|
+
other methods send a JSON body. A surrounding form also merges
|
|
351
|
+
for non-GET requests.
|
|
352
|
+
- `data-headers`: extra headers as JSON.
|
|
353
|
+
- `data-indicator="#spin"`: element shown while the request flies.
|
|
354
|
+
- `data-disable`: disables the element while its request flies.
|
|
355
|
+
|
|
356
|
+
### data-trigger
|
|
357
|
+
|
|
358
|
+
Controls when clicks and fetches fire. Grammar (any order, first bare
|
|
359
|
+
word is the event):
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<button data-get="/s" data-trigger="click delay:500ms">search</button>
|
|
363
|
+
<input data-get="/s" data-trigger="change changed throttle:1s">
|
|
364
|
+
<div data-get="/t" data-trigger="every 5s">
|
|
365
|
+
<div data-get="/b" data-trigger="load">
|
|
366
|
+
<div data-get="/b" data-trigger="revealed">
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Modifiers:
|
|
370
|
+
|
|
371
|
+
- `delay:500ms`: wait before firing; retrigger restarts the wait.
|
|
372
|
+
Good for search-as-you-type (with `changed`).
|
|
373
|
+
- `throttle:1s`: ignore repeats inside the window.
|
|
374
|
+
- `once`: fire a single time, then detach.
|
|
375
|
+
- `changed`: inputs only; skip when the value is unchanged.
|
|
376
|
+
- `queue:last` (default): a new fetch aborts the previous one.
|
|
377
|
+
`queue:first` and `queue:none`: skip while one is in flight.
|
|
378
|
+
`queue:all`: allow parallel requests.
|
|
379
|
+
- `from:document`, `from:body`, `from:.selector`: listen on another
|
|
380
|
+
root instead of the element.
|
|
381
|
+
- `every 5s`: poll on an interval (cleaned up on removal).
|
|
382
|
+
- `load`: fire once the document is ready.
|
|
383
|
+
- `revealed`: fire when scrolled into view (needs
|
|
384
|
+
IntersectionObserver, fires at once without it).
|
|
385
|
+
|
|
386
|
+
Default event by tag: forms use `submit`, inputs, selects, and
|
|
387
|
+
textareas use `change`, everything else uses `click`.
|
|
388
|
+
|
|
389
|
+
### data-preserve
|
|
390
|
+
|
|
391
|
+
Marks a node (with an `id`) that survives HTML swaps by identity.
|
|
392
|
+
The live node replaces the fresh copy, keeping video playback,
|
|
393
|
+
third-party widgets, and local state.
|
|
394
|
+
|
|
395
|
+
```html
|
|
396
|
+
<video id="v" data-preserve src="..."></video>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### data-morph-skip
|
|
400
|
+
|
|
401
|
+
Morph plugin only. Freezes a subtree during morph swaps.
|
|
402
|
+
|
|
403
|
+
## 6. JavaScript API reference
|
|
404
|
+
|
|
405
|
+
All methods live on `window.Laghu`.
|
|
406
|
+
|
|
407
|
+
### Laghu.define(tag, setup)
|
|
408
|
+
|
|
409
|
+
Registers a Light-DOM custom component. Tag must contain a hyphen.
|
|
410
|
+
`setup()` runs fresh per instance and returns state, actions,
|
|
411
|
+
observed attribute names, and an optional onMount hook.
|
|
412
|
+
|
|
413
|
+
- Observed attributes default to primitive state keys. Attribute
|
|
414
|
+
text coerces to the state type (numbers stay numbers).
|
|
415
|
+
- Properties set before registration upgrade into state.
|
|
416
|
+
- Attributes set before connect apply on connect.
|
|
417
|
+
- Sync is one way (attribute into state). Send objects through
|
|
418
|
+
properties or `element.state` instead of attributes.
|
|
419
|
+
- Removal destroys all bindings. `moveBefore` moves keep state.
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
Laghu.define('user-card', function () {
|
|
423
|
+
return {
|
|
424
|
+
state: { name: 'Ada', likes: 0 },
|
|
425
|
+
actions: {
|
|
426
|
+
like: function () { this.likes++; }
|
|
427
|
+
},
|
|
428
|
+
observed: ['name'],
|
|
429
|
+
onMount: function () { /* this is the state */ }
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
```html
|
|
435
|
+
<user-card name="Grace">
|
|
436
|
+
<span data-bind="name"></span>
|
|
437
|
+
<button data-click="like">like</button>
|
|
438
|
+
</userhtml>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Laghu.hydrate(root, state, actions)
|
|
442
|
+
|
|
443
|
+
Scans `root` and wires all bindings. Returns the reactive state.
|
|
444
|
+
Islands with `data-scope` call this automatically on page load.
|
|
445
|
+
|
|
446
|
+
### Laghu.reactive(obj)
|
|
447
|
+
|
|
448
|
+
Makes a plain object or array observable. Reads track, writes
|
|
449
|
+
trigger. Nested objects wrap on first read.
|
|
450
|
+
|
|
451
|
+
### Laghu.effect(fn)
|
|
452
|
+
|
|
453
|
+
Runs `fn` now and again whenever state it read changes. Returns a
|
|
454
|
+
stop function. One effect per binding is the normal pattern.
|
|
455
|
+
|
|
456
|
+
### Laghu.eval(scope, expr)
|
|
457
|
+
|
|
458
|
+
Reads one expression without `new Function` (CSP-safe). Supports
|
|
459
|
+
paths, literals, `!`, comparisons, `&&`, `||`, and no-argument
|
|
460
|
+
calls like `reset()`.
|
|
461
|
+
|
|
462
|
+
### Laghu.get(scope, path) and Laghu.set(scope, path, val)
|
|
463
|
+
|
|
464
|
+
Dotted path access with scope-chain lookup. `$store.name.key`
|
|
465
|
+
reaches global stores.
|
|
466
|
+
|
|
467
|
+
### Laghu.parseTrigger(spec, el)
|
|
468
|
+
|
|
469
|
+
Parses `data-trigger` text into a modifier object. Useful for
|
|
470
|
+
testing and custom wiring.
|
|
471
|
+
|
|
472
|
+
### Laghu.swap(target, html, mode, scope)
|
|
473
|
+
|
|
474
|
+
Swaps HTML and hydrates the result. See modes in section 5.
|
|
475
|
+
Dispatches `laghu:before-swap` and `laghu:after-swap`, preserves
|
|
476
|
+
`data-preserve` nodes, restores focus and caret.
|
|
477
|
+
|
|
478
|
+
### Laghu.sanitize(fn)
|
|
479
|
+
|
|
480
|
+
Replaces the HTML pass-through used by `data-html` and swaps.
|
|
481
|
+
Install a real sanitizer when rendering server HTML:
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
Laghu.sanitize(function (h) { return DOMPurify.sanitize(h); });
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Laghu.store(name, init)
|
|
488
|
+
|
|
489
|
+
Creates or fetches a named global reactive store. Read it in any
|
|
490
|
+
binding as `$store.cart.n`.
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
Laghu.store('cart', { n: 0 });
|
|
494
|
+
Laghu.store('cart').n++;
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
```html
|
|
498
|
+
<span data-bind="$store.cart.n"></span>
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Laghu.directive(name, fn)
|
|
502
|
+
|
|
503
|
+
Registers `data-name`. The function receives `{ el, get, effect,
|
|
504
|
+
scope }` and may return a cleanup function.
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
Laghu.directive('focus', function (ctx) {
|
|
508
|
+
var stop = ctx.effect(function () {
|
|
509
|
+
if (ctx.get()) ctx.el.focus();
|
|
510
|
+
});
|
|
511
|
+
return stop;
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<input data-focus="shouldFocus">
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Laghu.destroyTree(root)
|
|
520
|
+
|
|
521
|
+
Stops everything under `root`: effects, listeners, timers,
|
|
522
|
+
observers, in-flight fetches. Runs automatically on removal and
|
|
523
|
+
before swaps. Call it manually before wiping DOM yourself.
|
|
524
|
+
|
|
525
|
+
### Laghu.css and Laghu.version
|
|
526
|
+
|
|
527
|
+
`Laghu.css` is the FOUC guard string (hide unregistered tags, show
|
|
528
|
+
them with `is-defined`). `Laghu.version` is the release string.
|
|
529
|
+
|
|
530
|
+
### LaghuMorph.morph(target, html, opts)
|
|
531
|
+
|
|
532
|
+
From morph.js (load after laghu.js). Reconciles instead of
|
|
533
|
+
replacing. `opts.mode` is `inner` (default) or `outer`.
|
|
534
|
+
`LaghuMorph.morphEl(a, b)` reconciles two elements directly.
|
|
535
|
+
|
|
536
|
+
## 7. Events reference
|
|
537
|
+
|
|
538
|
+
All names are CustomEvents. Listen with `addEventListener`:
|
|
539
|
+
|
|
540
|
+
- `laghu:before-request`: cancelable, `detail` has `url`, `method`.
|
|
541
|
+
Call `preventDefault()` to stop the fetch (for confirm dialogs).
|
|
542
|
+
- `laghu:after-request`: `detail` has `status`, `url`.
|
|
543
|
+
- `laghu:request-error`: network failure or non-OK status.
|
|
544
|
+
`detail` has `error` or `status` plus `url`.
|
|
545
|
+
- `laghu:before-swap`: before HTML changes, `detail` has `swap`.
|
|
546
|
+
- `laghu:after-swap`: after hydrate, `detail` has `swap`.
|
|
547
|
+
- `laghu:unknown-action`: `detail` has `name`. Fires on typos.
|
|
548
|
+
- `laghu:mount` and `laghu:unmount`: on component elements.
|
|
549
|
+
- `laghu:poll`, `laghu:load`, `laghu:revealed`: synthetic trigger
|
|
550
|
+
events passed to interval, load, and reveal handlers.
|
|
551
|
+
|
|
552
|
+
## 8. Fetching data (JSON and HTML)
|
|
553
|
+
|
|
554
|
+
laghu has one rule: JSON merges into state, HTML swaps into the DOM.
|
|
555
|
+
|
|
556
|
+
JSON example. The response object merges with `Object.assign`, so
|
|
557
|
+
bound nodes update through normal reactivity:
|
|
558
|
+
|
|
559
|
+
```html
|
|
560
|
+
<button data-get="/api/user" data-trigger="click queue:last">
|
|
561
|
+
Load user
|
|
562
|
+
</button>
|
|
563
|
+
<p>Hello, <span data-bind="user.name">stranger</span></p>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
HTML example. The fragment swaps into the target and hydrates, so
|
|
567
|
+
new `data-bind` and `data-click` attributes work at once:
|
|
568
|
+
|
|
569
|
+
```html
|
|
570
|
+
<button data-get="/rows?page=2" data-target="#list" data-swap="append">
|
|
571
|
+
More
|
|
572
|
+
</button>
|
|
573
|
+
<ul id="list">...</ul>
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Form example. The enclosing form serializes to JSON automatically:
|
|
577
|
+
|
|
578
|
+
```html
|
|
579
|
+
<form data-post="/api/save" data-target="#msg">
|
|
580
|
+
<input name="email">
|
|
581
|
+
<button>Save</button>
|
|
582
|
+
<p id="msg" data-bind="result"></p>
|
|
583
|
+
</form>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Confirm example. Cancel in `before-request`:
|
|
587
|
+
|
|
588
|
+
```js
|
|
589
|
+
document.querySelector('#del').addEventListener('laghu:before-request', function (e) {
|
|
590
|
+
if (!confirm('Delete?')) e.preventDefault();
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## 9. The morph plugin
|
|
595
|
+
|
|
596
|
+
Default swaps replace nodes. Replacement loses input focus, caret,
|
|
597
|
+
scroll position, video playback, and component instances inside the
|
|
598
|
+
target. The morph plugin updates nodes in place instead.
|
|
599
|
+
|
|
600
|
+
Load it after the core:
|
|
601
|
+
|
|
602
|
+
```html
|
|
603
|
+
<script src="laghu.min.js"></script>
|
|
604
|
+
<script src="morph.min.js"></script>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Then use `morph:inner` (reconcile children) or `morph:outer`
|
|
608
|
+
(reconcile the element itself):
|
|
609
|
+
|
|
610
|
+
```html
|
|
611
|
+
<ul id="list" data-get="/rows" data-target="#list" data-swap="morph:inner">
|
|
612
|
+
<li id="r1">one</li>
|
|
613
|
+
</ul>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Rules: stable rows need an `id` or `data-bkey`. Unkeyed same-tag
|
|
617
|
+
children reuse positionally. `data-morph-skip` freezes a subtree.
|
|
618
|
+
Focused inputs keep typed values. Removed nodes run their laghu
|
|
619
|
+
cleanups. Without morph.js loaded, `morph:*` warns in development
|
|
620
|
+
and falls back to a plain inner swap.
|
|
621
|
+
|
|
622
|
+
Use morph for server-fed lists and tables. Keep destructive swaps
|
|
623
|
+
for one-shot content where replacement is cheaper and simpler.
|
|
624
|
+
|
|
625
|
+
## 10. Type safety without TypeScript
|
|
626
|
+
|
|
627
|
+
laghu ships type checking with zero build output. Every source file
|
|
628
|
+
starts with `// @ts-check`, and editors run the TypeScript language
|
|
629
|
+
server on plain `.js` files. JSDoc comments carry the types.
|
|
630
|
+
|
|
631
|
+
To check the whole project:
|
|
632
|
+
|
|
633
|
+
```sh
|
|
634
|
+
npx tsc --noEmit -p jsconfig.json
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
This typechecks only and emits nothing. The browser runs the same
|
|
638
|
+
`.js` files untouched.
|
|
639
|
+
|
|
640
|
+
One blind spot: HTML attribute strings are invisible to the
|
|
641
|
+
checker. Renaming `increment` to `add` breaks
|
|
642
|
+
`data-click="increment"` silently at the type level. laghu covers
|
|
643
|
+
this three ways: unknown actions warn in development, every unknown
|
|
644
|
+
action dispatches `laghu:unknown-action`, and the test suite boots
|
|
645
|
+
fixture pages and asserts no unknown-action warnings. Keep action
|
|
646
|
+
names in one registry object and test the fixtures.
|
|
647
|
+
|
|
648
|
+
Library consumers get `dist/laghu.d.ts` with full signatures.
|
|
649
|
+
|
|
650
|
+
## 11. Content Security Policy notes
|
|
651
|
+
|
|
652
|
+
laghu runs under strict CSP with no `unsafe-eval` and no
|
|
653
|
+
`unsafe-inline`:
|
|
654
|
+
|
|
655
|
+
- No `new Function`, no `eval`, no string `setTimeout`. Expressions
|
|
656
|
+
use a small built-in reader (paths, literals, `!`, comparisons,
|
|
657
|
+
`&&`, `||`, no-argument calls).
|
|
658
|
+
- No inline handlers. All listeners attach with
|
|
659
|
+
`addEventListener` from `data-*` attributes.
|
|
660
|
+
- Ship laghu as an external file with a Subresource Integrity hash.
|
|
661
|
+
- `data-scope` takes JSON only, for the same no-eval reason.
|
|
662
|
+
- `data-html` and swaps accept HTML by design. Point
|
|
663
|
+
`Laghu.sanitize` at DOMPurify when content comes from a server.
|
|
664
|
+
|
|
665
|
+
## 12. Loading flash and SEO notes
|
|
666
|
+
|
|
667
|
+
Empty custom tags render as unknown inline elements until the script
|
|
668
|
+
registers them. Two fixes, best used together:
|
|
669
|
+
|
|
670
|
+
1. Ship real HTML inside the tag (progressive enhancement). Content
|
|
671
|
+
shows before JS, works without JS, and stays indexable.
|
|
672
|
+
2. Hide only the unknown state with CSS:
|
|
673
|
+
|
|
674
|
+
```css
|
|
675
|
+
my-card:not(:defined) { visibility: hidden; }
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`visibility` (not `display`) avoids layout shift. `Laghu.css`
|
|
679
|
+
carries a ready-made version of this rule. Pre-rendered Light DOM
|
|
680
|
+
also solves SEO: crawlers and readers see content even when scripts
|
|
681
|
+
never run.
|
|
682
|
+
|
|
683
|
+
## 13. File sizes
|
|
684
|
+
|
|
685
|
+
Measured after minification:
|
|
686
|
+
|
|
687
|
+
- Core `dist/laghu.min.js`: about 21kB raw, 6.8kB gzip, 6.1kB brotli.
|
|
688
|
+
- Morph `dist/morph.min.js`: about 2.3kB raw, 1kB gzip.
|
|
689
|
+
- Budgets enforced by `node scripts/size.mjs`: core at or under
|
|
690
|
+
7.5kB gzip, morph at or under 3kB gzip.
|
|
691
|
+
|
|
692
|
+
For scale: htmx alone is about 16kB gzip, Alpine with a few plugins
|
|
693
|
+
is 12 to 15kB. laghu core plus morph stays under half of either.
|
|
694
|
+
|
|
695
|
+
## 14. Worked examples
|
|
696
|
+
|
|
697
|
+
See the `examples/` folder. Each file opens directly from disk.
|
|
698
|
+
|
|
699
|
+
- `counter.html`: one component, one binding, one action.
|
|
700
|
+
- `todo.html`: island with `data-model` input, keyed `data-for`
|
|
701
|
+
list, checkbox models, `data-show` counter.
|
|
702
|
+
- `fetch.html`: JSON button plus morph list swap (needs a server
|
|
703
|
+
for the URLs, or swap in the stub URLs for local JSON files).
|
|
704
|
+
|
|
705
|
+
## 15. Checking your setup
|
|
706
|
+
|
|
707
|
+
```sh
|
|
708
|
+
npm install
|
|
709
|
+
npx tsc --noEmit -p jsconfig.json
|
|
710
|
+
node scripts/size.mjs
|
|
711
|
+
node test/run.mjs
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
37 tests cover: counter increments, text and attribute bindings,
|
|
715
|
+
conditionals, keyed reorder reuse, two-way inputs, attribute sync,
|
|
716
|
+
triggers, JSON merge, all swap modes, morph focus preservation,
|
|
717
|
+
stores, directives with cleanup, and unknown-action warnings.
|
|
718
|
+
|
|
719
|
+
Releasing a new version (publishes to npm through CI):
|
|
720
|
+
|
|
721
|
+
1. Bump `version` in `package.json` and commit.
|
|
722
|
+
2. Create a GitHub Release with tag `vX.Y.Z` matching that version
|
|
723
|
+
(example: tag `v1.0.1` for version `1.0.1`).
|
|
724
|
+
3. The `Publish` workflow verifies types, tests, sizes, tarball
|
|
725
|
+
contents, and a packed install smoke test, then publishes with
|
|
726
|
+
provenance. It refuses when the tree is dirty or the tag and
|
|
727
|
+
version disagree.
|
|
728
|
+
4. Requirements: an `NPM_TOKEN` secret with publish rights on the
|
|
729
|
+
`laghu` package. To check without uploading, run the workflow
|
|
730
|
+
manually from the Actions tab (dry run is the default).
|
|
731
|
+
|
|
732
|
+
## 16. Troubleshooting
|
|
733
|
+
|
|
734
|
+
Unknown action warning: the `data-click` or `data-on` name matches
|
|
735
|
+
no action and no state function. Check spelling, or add the action
|
|
736
|
+
to the component setup return value.
|
|
737
|
+
|
|
738
|
+
`data-scope must be JSON` warning: attribute text is not valid
|
|
739
|
+
JSON. Use double quotes inside single quotes:
|
|
740
|
+
`data-scope='{"count": 0}'`.
|
|
741
|
+
|
|
742
|
+
Binding shows nothing: the path is missing, so the effect wrote an
|
|
743
|
+
empty string. Check the path against state, including row variable
|
|
744
|
+
names inside `data-for`.
|
|
745
|
+
|
|
746
|
+
Click does nothing on a fetched button: content swapped without
|
|
747
|
+
hydrate cannot happen through `Laghu.swap` (it hydrates), so this
|
|
748
|
+
means the HTML was inserted manually. Call
|
|
749
|
+
`Laghu.hydrate(parent, state, actions)` or route inserts through
|
|
750
|
+
`Laghu.swap`.
|
|
751
|
+
|
|
752
|
+
Focus jumps on list update: rows need `data-key`, or switch the
|
|
753
|
+
swap to `morph:inner` with row ids.
|
|
754
|
+
|
|
755
|
+
Request never fires with `changed`: the input value never differed.
|
|
756
|
+
`changed` compares against the last fired value per element.
|
|
757
|
+
|
|
758
|
+
morph warns and falls back: `morph.min.js` is not loaded. Add the
|
|
759
|
+
script tag after the core.
|
|
760
|
+
|
|
761
|
+
## 17. Project files
|
|
762
|
+
|
|
763
|
+
- `src/laghu.js`: core source with full JSDoc, loaded directly in
|
|
764
|
+
development.
|
|
765
|
+
- `src/morph.js`: opt-in morph plugin source with full JSDoc.
|
|
766
|
+
- `dist/laghu.min.js`, `dist/morph.min.js`: minified builds.
|
|
767
|
+
- `dist/laghu.d.ts`: TypeScript declarations for consumers.
|
|
768
|
+
- `test/run.mjs`: jsdom suite, 37 assertions.
|
|
769
|
+
- `scripts/size.mjs`: minifies and enforces gzip budgets.
|
|
770
|
+
- `examples/`: three pages opening from disk.
|
|
771
|
+
- `jsconfig.json`: strict `checkJs` config used by editors and CI.
|