lume-js 0.4.1 โ 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/README.md +42 -761
- package/package.json +13 -5
- package/src/addons/index.d.ts +173 -0
- package/src/addons/index.js +2 -1
- package/src/addons/repeat.js +342 -0
package/README.md
CHANGED
|
@@ -2,826 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
**Reactivity that follows web standards.**
|
|
4
4
|
|
|
5
|
-
Minimal reactive state management using only standard JavaScript and HTML
|
|
5
|
+
Minimal reactive state management using only standard JavaScript and HTML. No custom syntax, no build step required, no framework lock-in.
|
|
6
6
|
|
|
7
7
|
[](LICENSE)
|
|
8
|
-
[](package.json)
|
|
9
|
+
[](tests/)
|
|
10
|
+
[](dist/)
|
|
9
11
|
|
|
10
12
|
## Why Lume.js?
|
|
11
13
|
|
|
12
|
-
- ๐ฏ **Standards-Only** - Uses only `data-*` attributes and standard JavaScript
|
|
13
|
-
- ๐ฆ **Tiny** - <2KB gzipped
|
|
14
|
-
- โก **Fast** - Direct DOM updates, no virtual DOM overhead
|
|
15
|
-
- ๐ง **No Build Step** - Works directly in the browser
|
|
16
|
-
- ๐จ **No Lock-in** - Works with any library (GSAP, jQuery, D3, etc.)
|
|
17
|
-
- โฟ **Accessible** - HTML validators love it, screen readers work perfectly
|
|
18
|
-
- ๐งน **Clean API** - Cleanup functions prevent memory leaks
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
> **Note:** The `repeat` addon is *experimental* in v1.0.0. Its API may evolve in future releases as it is refined to best fit Lume.js philosophy.
|
|
21
16
|
|
|
22
17
|
| Feature | Lume.js | Alpine.js | Vue | React |
|
|
23
18
|
|---------|---------|-----------|-----|-------|
|
|
24
|
-
| Custom Syntax | โ No | โ
`x-data
|
|
19
|
+
| Custom Syntax | โ No | โ
`x-data` | โ
`v-bind` | โ
JSX |
|
|
25
20
|
| Build Step | โ Optional | โ Optional | โ ๏ธ Recommended | โ
Required |
|
|
26
21
|
| Bundle Size | ~2KB | ~15KB | ~35KB | ~45KB |
|
|
27
22
|
| HTML Validation | โ
Pass | โ ๏ธ Warnings | โ ๏ธ Warnings | โ JSX |
|
|
28
|
-
| Cleanup API | โ
Yes | โ ๏ธ Limited | โ
Yes | โ
Yes |
|
|
29
23
|
|
|
30
24
|
**Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
|
|
31
25
|
|
|
32
|
-
๐ **New to the project?** Read [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our design philosophy and why certain choices were made.
|
|
33
|
-
|
|
34
26
|
---
|
|
35
27
|
|
|
36
28
|
## Installation
|
|
37
29
|
|
|
38
|
-
### Via
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install lume-js
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Via CDN
|
|
30
|
+
### Via CDN (Recommended for simple projects)
|
|
45
31
|
|
|
46
32
|
```html
|
|
47
33
|
<script type="module">
|
|
48
34
|
import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
|
|
49
|
-
|
|
50
|
-
// For addons:
|
|
51
|
-
import { computed, watch } from 'https://cdn.jsdelivr.net/npm/lume-js/src/addons/index.js';
|
|
52
35
|
</script>
|
|
53
36
|
```
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## Quick Start
|
|
58
|
-
|
|
59
|
-
### Examples
|
|
60
|
-
|
|
61
|
-
Run the live examples (including the new Todo app) with Vite:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npm run dev
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Then open the Examples index (Vite will auto-open): `http://localhost:5173/examples/`
|
|
68
|
-
|
|
69
|
-
**HTML:**
|
|
70
|
-
```html
|
|
71
|
-
<div>
|
|
72
|
-
<p>Count: <span data-bind="count"></span></p>
|
|
73
|
-
<input data-bind="name" placeholder="Enter name">
|
|
74
|
-
<p>Hello, <span data-bind="name"></span>!</p>
|
|
75
|
-
|
|
76
|
-
<button id="increment">+</button>
|
|
77
|
-
</div>
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**JavaScript:**
|
|
81
|
-
```javascript
|
|
82
|
-
import { state, bindDom, effect } from 'lume-js';
|
|
83
|
-
|
|
84
|
-
// Create reactive state
|
|
85
|
-
const store = state({
|
|
86
|
-
count: 0,
|
|
87
|
-
name: 'World'
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Bind to DOM (updates on state changes)
|
|
91
|
-
const cleanup = bindDom(document.body, store);
|
|
92
|
-
|
|
93
|
-
// Auto-update document title when name changes
|
|
94
|
-
const effectCleanup = effect(() => {
|
|
95
|
-
document.title = `Hello, ${store.name}!`;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Update state with standard JavaScript
|
|
99
|
-
document.getElementById('increment').addEventListener('click', () => {
|
|
100
|
-
store.count++;
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Cleanup when done (important!)
|
|
104
|
-
window.addEventListener('beforeunload', () => {
|
|
105
|
-
cleanup();
|
|
106
|
-
effectCleanup();
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
That's it! No build step, no custom syntax, just HTML and JavaScript.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Core API
|
|
115
|
-
|
|
116
|
-
### `state(object)`
|
|
117
|
-
|
|
118
|
-
Creates a reactive state object using Proxy with automatic dependency tracking.
|
|
119
|
-
|
|
120
|
-
```javascript
|
|
121
|
-
const store = state({
|
|
122
|
-
count: 0,
|
|
123
|
-
user: state({ // Nested state must be wrapped
|
|
124
|
-
name: 'Alice',
|
|
125
|
-
email: 'alice@example.com'
|
|
126
|
-
})
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Update state
|
|
130
|
-
store.count++;
|
|
131
|
-
store.user.name = 'Bob';
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Features:**
|
|
135
|
-
- โ
Automatic dependency tracking for effects
|
|
136
|
-
- โ
Per-state microtask batching for performance
|
|
137
|
-
- โ
Validates input (must be plain object)
|
|
138
|
-
- โ
Only triggers updates when value actually changes
|
|
139
|
-
- โ
Returns cleanup function from `$subscribe`
|
|
140
|
-
- โ
Deduplicates effect runs per flush cycle
|
|
141
|
-
|
|
142
|
-
### `effect(fn)`
|
|
143
|
-
|
|
144
|
-
Creates an effect that automatically tracks dependencies and re-runs when they change.
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
const store = state({ count: 0, name: 'Alice' });
|
|
148
|
-
|
|
149
|
-
const cleanup = effect(() => {
|
|
150
|
-
// Only tracks 'count' (name is not accessed)
|
|
151
|
-
console.log(`Count: ${store.count}`);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
store.count = 5; // Effect re-runs
|
|
155
|
-
store.name = 'Bob'; // Effect does NOT re-run
|
|
156
|
-
|
|
157
|
-
cleanup(); // Stop the effect
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Features:**
|
|
161
|
-
- โ
Automatic dependency collection (tracks what you actually access)
|
|
162
|
-
- โ
Dynamic dependencies (re-tracks on every execution)
|
|
163
|
-
- โ
Returns cleanup function
|
|
164
|
-
- โ
Prevents infinite recursion
|
|
165
|
-
- โ
Integrates with per-state batching (no global scheduler)
|
|
166
|
-
|
|
167
|
-
**How it works:** Effects use `globalThis.__LUME_CURRENT_EFFECT__` to track which state properties are accessed during execution. When any tracked property changes, the effect is queued in that state's pending effects set and runs once in the next microtask.
|
|
168
|
-
|
|
169
|
-
### `bindDom(root, store, options?)`
|
|
170
|
-
|
|
171
|
-
Binds reactive state to DOM elements with `data-bind` attributes.
|
|
172
|
-
|
|
173
|
-
**Automatically waits for DOMContentLoaded** if the document is still loading, making it safe to call from anywhere (even in `<head>`).
|
|
174
|
-
|
|
175
|
-
```javascript
|
|
176
|
-
// Default: Auto-waits for DOM (safe anywhere)
|
|
177
|
-
const cleanup = bindDom(document.body, store);
|
|
178
|
-
|
|
179
|
-
// Advanced: Force immediate binding (no auto-wait)
|
|
180
|
-
const cleanup = bindDom(myElement, store, { immediate: true });
|
|
181
|
-
|
|
182
|
-
// Later: cleanup all bindings
|
|
183
|
-
cleanup();
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
**Parameters:**
|
|
187
|
-
- `root` (HTMLElement) - Root element to scan for `[data-bind]` attributes
|
|
188
|
-
- `store` (Object) - Reactive state object
|
|
189
|
-
- `options` (Object, optional)
|
|
190
|
-
- `immediate` (Boolean) - Skip auto-wait, bind immediately. Default: `false`
|
|
191
|
-
|
|
192
|
-
**Supports:**
|
|
193
|
-
- โ
Text content: `<span data-bind="count"></span>`
|
|
194
|
-
- โ
Input values: `<input data-bind="name">`
|
|
195
|
-
- โ
Textareas: `<textarea data-bind="bio"></textarea>`
|
|
196
|
-
- โ
Selects: `<select data-bind="theme"></select>`
|
|
197
|
-
- โ
Checkboxes: `<input type="checkbox" data-bind="enabled">`
|
|
198
|
-
- โ
Numbers: `<input type="number" data-bind="age">`
|
|
199
|
-
- โ
Radio buttons: `<input type="radio" data-bind="choice">`
|
|
200
|
-
- โ
Nested paths: `<span data-bind="user.name"></span>`
|
|
201
|
-
|
|
202
|
-
**Multiple Checkboxes Pattern:**
|
|
203
|
-
|
|
204
|
-
For multiple checkboxes, use nested state instead of arrays:
|
|
205
|
-
|
|
206
|
-
```javascript
|
|
207
|
-
// โ
Recommended: Nested state objects
|
|
208
|
-
const store = state({
|
|
209
|
-
tags: state({
|
|
210
|
-
javascript: true,
|
|
211
|
-
python: false,
|
|
212
|
-
go: true
|
|
213
|
-
})
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
38
|
+
**Version Pinning:**
|
|
217
39
|
```html
|
|
218
|
-
<input type="checkbox" data-bind="tags.javascript"> JavaScript
|
|
219
|
-
<input type="checkbox" data-bind="tags.python"> Python
|
|
220
|
-
<input type="checkbox" data-bind="tags.go"> Go
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Nested state is **more explicit and easier to validate** than array-based bindings. See [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md#why-nested-state-for-multiple-checkboxes-instead-of-arrays) for the full rationale.
|
|
224
|
-
|
|
225
|
-
**Features:**
|
|
226
|
-
- โ
Auto-waits for DOM if needed (no timing issues!)
|
|
227
|
-
- โ
Returns cleanup function
|
|
228
|
-
- โ
Better error messages with `[Lume.js]` prefix
|
|
229
|
-
- โ
Handles edge cases (empty bindings, invalid paths)
|
|
230
|
-
- โ
Two-way binding for form inputs
|
|
231
|
-
|
|
232
|
-
### `isReactive(obj)`
|
|
233
|
-
|
|
234
|
-
Checks whether a value is a reactive proxy created by `state()`.
|
|
235
|
-
|
|
236
|
-
```javascript
|
|
237
|
-
import { state, isReactive } from 'lume-js';
|
|
238
|
-
|
|
239
|
-
const original = { count: 1 };
|
|
240
|
-
const store = state(original);
|
|
241
|
-
|
|
242
|
-
isReactive(store); // true
|
|
243
|
-
isReactive(original); // false
|
|
244
|
-
isReactive(null); // false
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**How it works:**
|
|
248
|
-
Lume.js uses an internal `Symbol` checked via the Proxy `get` trap rather than mutating the proxy or storing external WeakSet state. Accessing `obj[REACTIVE_SYMBOL]` returns `true` only for reactive proxies, and the symbol is not enumerable or visible via `Object.getOwnPropertySymbols`.
|
|
249
|
-
|
|
250
|
-
**Characteristics:**
|
|
251
|
-
- โ
Zero mutation of the proxy
|
|
252
|
-
- โ
Invisible to enumeration and reflection
|
|
253
|
-
- โ
Fast: single symbol identity check in the `get` path
|
|
254
|
-
- โ
Supports nested reactive states naturally
|
|
255
|
-
- โ
Skips tracking meta `$`-prefixed methods (e.g. `$subscribe`)
|
|
256
|
-
|
|
257
|
-
**When to use:** Utility/debugging, conditional wrapping patterns like:
|
|
258
|
-
```javascript
|
|
259
|
-
function ensureReactive(val) {
|
|
260
|
-
return isReactive(val) ? val : state(val);
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**Why Auto-Ready?**
|
|
265
|
-
|
|
266
|
-
Works seamlessly regardless of script placement:
|
|
267
|
-
|
|
268
|
-
```html
|
|
269
|
-
<!-- โ
Works in <head> -->
|
|
270
40
|
<script type="module">
|
|
271
|
-
import { state
|
|
272
|
-
const store = state({ count: 0 });
|
|
273
|
-
bindDom(document.body, store); // Auto-waits for DOM!
|
|
41
|
+
import { state } from 'https://cdn.jsdelivr.net/npm/lume-js@1.0.0/src/index.js';
|
|
274
42
|
</script>
|
|
275
|
-
|
|
276
|
-
<!-- โ
Works inline in <body> -->
|
|
277
|
-
<body>
|
|
278
|
-
<span data-bind="count"></span>
|
|
279
|
-
<script type="module">
|
|
280
|
-
// bindDom() waits for DOMContentLoaded automatically
|
|
281
|
-
</script>
|
|
282
|
-
</body>
|
|
283
|
-
|
|
284
|
-
<!-- โ
Works with defer -->
|
|
285
|
-
<script type="module" defer>
|
|
286
|
-
// Already loaded, executes immediately
|
|
287
|
-
</script>
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**When to use `immediate: true`:**
|
|
291
|
-
|
|
292
|
-
Rare scenarios where you're dynamically creating DOM or need precise control:
|
|
293
|
-
|
|
294
|
-
```javascript
|
|
295
|
-
// Dynamic DOM injection
|
|
296
|
-
const container = document.createElement('div');
|
|
297
|
-
container.innerHTML = '<span data-bind="count"></span>';
|
|
298
|
-
document.body.appendChild(container);
|
|
299
|
-
|
|
300
|
-
// Bind immediately (DOM already exists)
|
|
301
|
-
bindDom(container, store, { immediate: true });
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### `$subscribe(key, callback)`
|
|
305
|
-
|
|
306
|
-
Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
|
|
307
|
-
|
|
308
|
-
```javascript
|
|
309
|
-
const unsubscribe = store.$subscribe('count', (value) => {
|
|
310
|
-
console.log('Count changed:', value);
|
|
311
|
-
|
|
312
|
-
// Integrate with other libraries
|
|
313
|
-
if (value > 10) {
|
|
314
|
-
showNotification('Count is high!');
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Cleanup
|
|
319
|
-
unsubscribe();
|
|
320
43
|
```
|
|
321
44
|
|
|
322
|
-
|
|
323
|
-
- โ
Returns unsubscribe function
|
|
324
|
-
- โ
Validates callback is a function
|
|
325
|
-
- โ
Calls immediately with current value (not batched)
|
|
326
|
-
- โ
Only notifies on actual value changes (via batching)
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## Addons
|
|
331
|
-
|
|
332
|
-
Lume.js provides optional addons for advanced reactivity patterns. Import from `lume-js/addons`.
|
|
333
|
-
|
|
334
|
-
### `computed(fn)`
|
|
335
|
-
|
|
336
|
-
Creates a computed value that automatically updates when its dependencies change.
|
|
337
|
-
|
|
338
|
-
```javascript
|
|
339
|
-
import { state, effect } from 'lume-js';
|
|
340
|
-
import { computed } from 'lume-js/addons';
|
|
341
|
-
|
|
342
|
-
const store = state({ count: 5 });
|
|
343
|
-
|
|
344
|
-
const doubled = computed(() => store.count * 2);
|
|
345
|
-
console.log(doubled.value); // 10
|
|
346
|
-
|
|
347
|
-
store.count = 10;
|
|
348
|
-
// After microtask:
|
|
349
|
-
console.log(doubled.value); // 20 (auto-updated)
|
|
350
|
-
|
|
351
|
-
// Subscribe to changes
|
|
352
|
-
const unsub = doubled.subscribe(value => {
|
|
353
|
-
console.log('Doubled:', value);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Cleanup
|
|
357
|
-
doubled.dispose();
|
|
358
|
-
unsub();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Features:**
|
|
362
|
-
- โ
Automatic dependency tracking using `effect()`
|
|
363
|
-
- โ
Cached values (only recomputes when dependencies change)
|
|
364
|
-
- โ
Subscribe to changes with `.subscribe(callback)`
|
|
365
|
-
- โ
Cleanup with `.dispose()`
|
|
366
|
-
- โ
Error handling (sets to undefined on error)
|
|
367
|
-
|
|
368
|
-
### `watch(store, key, callback)`
|
|
369
|
-
|
|
370
|
-
Alias for `$subscribe` - observes changes to a specific state key.
|
|
371
|
-
|
|
372
|
-
```javascript
|
|
373
|
-
import { state } from 'lume-js';
|
|
374
|
-
import { watch } from 'lume-js/addons';
|
|
375
|
-
|
|
376
|
-
const store = state({ count: 0 });
|
|
377
|
-
|
|
378
|
-
const unwatch = watch(store, 'count', (value) => {
|
|
379
|
-
console.log('Count is now:', value);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Cleanup
|
|
383
|
-
unwatch();
|
|
384
|
-
```
|
|
45
|
+
### Via NPM (Recommended for bundlers)
|
|
385
46
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
---
|
|
389
|
-
|
|
390
|
-
## Choosing the Right Reactive Pattern
|
|
391
|
-
|
|
392
|
-
Lume.js provides three ways to react to state changes. Here's when to use each:
|
|
393
|
-
|
|
394
|
-
| Pattern | Use When | Pros | Cons |
|
|
395
|
-
|---------|----------|------|------|
|
|
396
|
-
| **`bindDom()`** | Syncing state โ DOM | Zero code, declarative HTML | DOM-only, no custom logic |
|
|
397
|
-
| **`$subscribe()`** | Listening to specific keys | Explicit, immediate, simple | Manual dependency tracking |
|
|
398
|
-
| **`effect()`** | Auto-run code on any state access | Automatic dependencies, concise | Microtask delay, can infinite loop |
|
|
399
|
-
| **`computed()`** | Deriving values from state | Cached, automatic recompute | Addon import, slight overhead |
|
|
400
|
-
|
|
401
|
-
**Quick Decision Tree:**
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
Need to update DOM?
|
|
405
|
-
โโ Yes, just sync form/text โ Use bindDom()
|
|
406
|
-
โโ No, custom logic needed
|
|
407
|
-
โโ Watch single key? โ Use $subscribe()
|
|
408
|
-
โโ Watch multiple keys dynamically? โ Use effect()
|
|
409
|
-
โโ Derive a value? โ Use computed()
|
|
47
|
+
```bash
|
|
48
|
+
npm install lume-js
|
|
410
49
|
```
|
|
411
50
|
|
|
412
|
-
**Examples:**
|
|
413
|
-
|
|
414
51
|
```javascript
|
|
415
|
-
|
|
416
|
-
<span data-bind="count"></span>
|
|
417
|
-
bindDom(document.body, store);
|
|
418
|
-
|
|
419
|
-
// 2. $subscribe - Specific key, immediate execution
|
|
420
|
-
store.$subscribe('count', (val) => {
|
|
421
|
-
if (val > 10) showNotification('High!');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// 3. effect - Multiple keys, automatic tracking
|
|
425
|
-
effect(() => {
|
|
426
|
-
document.title = `${store.user.name}: ${store.count}`;
|
|
427
|
-
// Tracks both user.name and count automatically
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// 4. computed - Derive cached value
|
|
431
|
-
import { computed } from 'lume-js/addons';
|
|
432
|
-
const total = computed(() => store.items.reduce((sum, i) => sum + i.price, 0));
|
|
433
|
-
console.log(total.value);
|
|
52
|
+
import { state, bindDom } from 'lume-js';
|
|
434
53
|
```
|
|
435
54
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
- โ ๏ธ **effect()** runs in next microtask (~0.002ms delay). Use `$subscribe()` for immediate execution.
|
|
439
|
-
- โ ๏ธ **Don't mutate tracked state inside effect** - causes infinite loops:
|
|
440
|
-
```javascript
|
|
441
|
-
// โ BAD - Infinite loop
|
|
442
|
-
effect(() => {
|
|
443
|
-
store.count++; // Writes to what it reads!
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// โ
GOOD - Read-only or separate keys
|
|
447
|
-
effect(() => {
|
|
448
|
-
store.displayCount = store.count * 2; // Different keys
|
|
449
|
-
});
|
|
450
|
-
```
|
|
55
|
+
### Browser Support
|
|
56
|
+
Works in all modern browsers (Chrome 49+, Firefox 18+, Safari 10+, Edge 79+). **IE11 is NOT supported.**
|
|
451
57
|
|
|
452
58
|
---
|
|
453
59
|
|
|
454
|
-
##
|
|
455
|
-
|
|
456
|
-
### Basic Counter
|
|
457
|
-
|
|
458
|
-
```javascript
|
|
459
|
-
const store = state({ count: 0 });
|
|
460
|
-
const cleanup = bindDom(document.body, store);
|
|
461
|
-
|
|
462
|
-
document.getElementById('inc').addEventListener('click', () => {
|
|
463
|
-
store.count++;
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// Cleanup on unmount
|
|
467
|
-
window.addEventListener('beforeunload', () => cleanup());
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
```html
|
|
471
|
-
<p>Count: <span data-bind="count"></span></p>
|
|
472
|
-
<button id="inc">Increment</button>
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### Form Handling with Validation
|
|
476
|
-
|
|
477
|
-
```javascript
|
|
478
|
-
const form = state({
|
|
479
|
-
email: '',
|
|
480
|
-
age: 25,
|
|
481
|
-
theme: 'light',
|
|
482
|
-
errors: {}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
const cleanup = bindDom(document.querySelector('form'), form);
|
|
486
|
-
|
|
487
|
-
// Validate on change
|
|
488
|
-
const unsubEmail = form.$subscribe('email', (value) => {
|
|
489
|
-
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
490
|
-
form.errors = {
|
|
491
|
-
...form.errors,
|
|
492
|
-
email: isValid ? '' : 'Invalid email'
|
|
493
|
-
};
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// Cleanup
|
|
497
|
-
window.addEventListener('beforeunload', () => {
|
|
498
|
-
cleanup();
|
|
499
|
-
unsubEmail();
|
|
500
|
-
});
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
```html
|
|
504
|
-
<form>
|
|
505
|
-
<input type="email" data-bind="email">
|
|
506
|
-
<span data-bind="errors.email" style="color: red;"></span>
|
|
507
|
-
|
|
508
|
-
<input type="number" data-bind="age">
|
|
509
|
-
|
|
510
|
-
<select data-bind="theme">
|
|
511
|
-
<option value="light">Light</option>
|
|
512
|
-
<option value="dark">Dark</option>
|
|
513
|
-
</select>
|
|
514
|
-
</form>
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### Nested State
|
|
518
|
-
|
|
519
|
-
```javascript
|
|
520
|
-
const store = state({
|
|
521
|
-
user: state({
|
|
522
|
-
profile: state({
|
|
523
|
-
name: 'Alice',
|
|
524
|
-
bio: 'Developer'
|
|
525
|
-
}),
|
|
526
|
-
settings: state({
|
|
527
|
-
notifications: true
|
|
528
|
-
})
|
|
529
|
-
})
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
const cleanup = bindDom(document.body, store);
|
|
533
|
-
|
|
534
|
-
// Subscribe to nested changes
|
|
535
|
-
const unsub = store.user.profile.$subscribe('name', (name) => {
|
|
536
|
-
console.log('Profile name changed:', name);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Cleanup
|
|
540
|
-
window.addEventListener('beforeunload', () => {
|
|
541
|
-
cleanup();
|
|
542
|
-
unsub();
|
|
543
|
-
});
|
|
544
|
-
```
|
|
60
|
+
## Quick Start
|
|
545
61
|
|
|
62
|
+
**HTML:**
|
|
546
63
|
```html
|
|
547
|
-
<
|
|
548
|
-
<
|
|
549
|
-
<input
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
### Using Effects for Auto-Updates
|
|
553
|
-
|
|
554
|
-
```javascript
|
|
555
|
-
import { state, effect } from 'lume-js';
|
|
556
|
-
|
|
557
|
-
const store = state({
|
|
558
|
-
firstName: 'Alice',
|
|
559
|
-
lastName: 'Smith'
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// Auto-update title when name changes
|
|
563
|
-
effect(() => {
|
|
564
|
-
document.title = `${store.firstName} ${store.lastName}`;
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
store.firstName = 'Bob';
|
|
568
|
-
// Title automatically updates to "Bob Smith" in next microtask
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
### Computed Values
|
|
572
|
-
|
|
573
|
-
```javascript
|
|
574
|
-
import { state } from 'lume-js';
|
|
575
|
-
import { computed } from 'lume-js/addons';
|
|
576
|
-
|
|
577
|
-
const cart = state({
|
|
578
|
-
items: state([
|
|
579
|
-
state({ price: 10, quantity: 2 }),
|
|
580
|
-
state({ price: 15, quantity: 1 })
|
|
581
|
-
])
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
const total = computed(() => {
|
|
585
|
-
return cart.items.reduce((sum, item) =>
|
|
586
|
-
sum + (item.price * item.quantity), 0
|
|
587
|
-
);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
console.log(total.value); // 35
|
|
591
|
-
|
|
592
|
-
cart.items[0].quantity = 3;
|
|
593
|
-
// After microtask:
|
|
594
|
-
console.log(total.value); // 45
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### Integration with GSAP
|
|
598
|
-
|
|
599
|
-
```javascript
|
|
600
|
-
import gsap from 'gsap';
|
|
601
|
-
import { state, effect } from 'lume-js';
|
|
602
|
-
|
|
603
|
-
const ui = state({ x: 0, y: 0 });
|
|
604
|
-
|
|
605
|
-
// Use effect for automatic animation updates
|
|
606
|
-
effect(() => {
|
|
607
|
-
gsap.to('.box', { x: ui.x, y: ui.y, duration: 0.5 });
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// Or use $subscribe
|
|
611
|
-
const unsubX = ui.$subscribe('x', (value) => {
|
|
612
|
-
gsap.to('.box', { x: value, duration: 0.5 });
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// Now ui.x = 100 triggers smooth animation
|
|
616
|
-
|
|
617
|
-
// Cleanup
|
|
618
|
-
window.addEventListener('beforeunload', () => unsubX());
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
### Cleanup Pattern (Important!)
|
|
622
|
-
|
|
623
|
-
```javascript
|
|
624
|
-
import { state, effect, bindDom } from 'lume-js';
|
|
625
|
-
import { computed } from 'lume-js/addons';
|
|
626
|
-
|
|
627
|
-
const store = state({ data: [] });
|
|
628
|
-
const cleanup = bindDom(root, store);
|
|
629
|
-
|
|
630
|
-
const unsub1 = store.$subscribe('data', handleData);
|
|
631
|
-
const unsub2 = store.$subscribe('status', handleStatus);
|
|
632
|
-
|
|
633
|
-
const effectCleanup = effect(() => {
|
|
634
|
-
console.log('Data length:', store.data.length);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
const total = computed(() => store.data.length * 2);
|
|
638
|
-
|
|
639
|
-
// Cleanup when component unmounts
|
|
640
|
-
function destroy() {
|
|
641
|
-
cleanup(); // Remove DOM bindings
|
|
642
|
-
unsub1(); // Remove subscription 1
|
|
643
|
-
unsub2(); // Remove subscription 2
|
|
644
|
-
effectCleanup(); // Stop effect
|
|
645
|
-
total.dispose(); // Stop computed
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// For SPA frameworks
|
|
649
|
-
onUnmount(destroy);
|
|
650
|
-
|
|
651
|
-
// For vanilla JS
|
|
652
|
-
window.addEventListener('beforeunload', destroy);
|
|
64
|
+
<div>
|
|
65
|
+
<h1>Hello, <span data-bind="name"></span>!</h1>
|
|
66
|
+
<input data-bind="name" placeholder="Enter your name">
|
|
67
|
+
</div>
|
|
653
68
|
```
|
|
654
69
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
## Philosophy
|
|
658
|
-
|
|
659
|
-
### Standards-Only
|
|
660
|
-
- Uses only `data-*` attributes (HTML5 standard)
|
|
661
|
-
- Uses only standard JavaScript APIs (Proxy, addEventListener)
|
|
662
|
-
- No custom syntax that breaks validators
|
|
663
|
-
- Works with any tool/library
|
|
664
|
-
|
|
665
|
-
### No Artificial Limitations
|
|
70
|
+
**JavaScript:**
|
|
666
71
|
```javascript
|
|
667
|
-
|
|
668
|
-
$('.modal').fadeIn();
|
|
669
|
-
store.modalOpen = true;
|
|
72
|
+
import { state, bindDom } from 'lume-js';
|
|
670
73
|
|
|
671
|
-
//
|
|
672
|
-
|
|
74
|
+
// 1. Create state
|
|
75
|
+
const store = state({ name: 'World' });
|
|
673
76
|
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// โ
Mix with vanilla JS
|
|
678
|
-
document.addEventListener('click', () => store.clicks++);
|
|
77
|
+
// 2. Bind to DOM
|
|
78
|
+
bindDom(document.body, store);
|
|
679
79
|
```
|
|
680
80
|
|
|
681
|
-
**
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
```html
|
|
686
|
-
<!-- Works without JavaScript (server-rendered) -->
|
|
687
|
-
<form action="/submit" method="POST">
|
|
688
|
-
<input name="email" value="alice@example.com">
|
|
689
|
-
<button type="submit">Save</button>
|
|
690
|
-
</form>
|
|
691
|
-
|
|
692
|
-
<script type="module">
|
|
693
|
-
// Enhanced when JS loads
|
|
694
|
-
import { state, bindDom } from 'lume-js';
|
|
695
|
-
|
|
696
|
-
const form = state({ email: 'alice@example.com' });
|
|
697
|
-
const cleanup = bindDom(document.querySelector('form'), form);
|
|
698
|
-
|
|
699
|
-
// Prevent default, use AJAX
|
|
700
|
-
document.querySelector('form').addEventListener('submit', async (e) => {
|
|
701
|
-
e.preventDefault();
|
|
702
|
-
await fetch('/submit', {
|
|
703
|
-
method: 'POST',
|
|
704
|
-
body: JSON.stringify({ email: form.email })
|
|
705
|
-
});
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
window.addEventListener('beforeunload', () => cleanup());
|
|
709
|
-
</script>
|
|
710
|
-
```
|
|
81
|
+
**What just happened?**
|
|
82
|
+
1. **`state()`** created a reactive object.
|
|
83
|
+
2. **`bindDom()`** scanned the document for `data-bind="name"`.
|
|
84
|
+
3. It set up a two-way binding: typing in the input updates the state, and the state updates the text.
|
|
711
85
|
|
|
712
86
|
---
|
|
713
87
|
|
|
714
|
-
##
|
|
88
|
+
## Documentation
|
|
715
89
|
|
|
716
|
-
|
|
717
|
-
- WordPress/Shopify theme developers
|
|
718
|
-
- Accessibility-focused teams (government, healthcare, education)
|
|
719
|
-
- Legacy codebases that can't do full rewrites
|
|
720
|
-
- Developers who hate learning framework-specific syntax
|
|
721
|
-
- Progressive enhancement advocates
|
|
722
|
-
- Projects requiring HTML validation
|
|
723
|
-
- Adding reactivity to server-rendered apps
|
|
90
|
+
Full documentation is available in the [docs/](docs/) directory:
|
|
724
91
|
|
|
725
|
-
|
|
726
|
-
- **
|
|
727
|
-
- **
|
|
728
|
-
- **
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
## What's New
|
|
733
|
-
|
|
734
|
-
### Core Features
|
|
735
|
-
- โ
**Automatic dependency tracking** - `effect()` automatically tracks which state properties are accessed
|
|
736
|
-
- โ
**Per-state batching** - Each state object maintains its own microtask flush for optimal performance
|
|
737
|
-
- โ
**Effect deduplication** - Effects only run once per flush cycle, even if multiple dependencies change
|
|
738
|
-
- โ
**TypeScript support** - Full type definitions in `index.d.ts`
|
|
739
|
-
- โ
**Cleanup functions** - All reactive APIs return cleanup/unsubscribe functions
|
|
740
|
-
|
|
741
|
-
### Addons
|
|
742
|
-
- โ
**`computed()`** - Memoized computed values with automatic dependency tracking
|
|
743
|
-
- โ
**`watch()`** - Convenience alias for `$subscribe`
|
|
744
|
-
|
|
745
|
-
### API Design
|
|
746
|
-
- โ
`state()` - Create reactive state with automatic tracking support
|
|
747
|
-
- โ
`effect()` - Core reactivity primitive (automatic dependency collection)
|
|
748
|
-
- โ
`bindDom()` - DOM binding with two-way sync for form inputs
|
|
749
|
-
- โ
`$subscribe()` - Manual subscriptions (calls immediately with current value)
|
|
750
|
-
- โ
All functions return cleanup/unsubscribe functions
|
|
751
|
-
- โ
Better error handling with `[Lume.js]` prefix
|
|
752
|
-
- โ
Input validation on all public APIs
|
|
753
|
-
|
|
754
|
-
---
|
|
755
|
-
|
|
756
|
-
## Browser Support
|
|
757
|
-
|
|
758
|
-
- Chrome/Edge 49+
|
|
759
|
-
- Firefox 18+
|
|
760
|
-
- Safari 10+
|
|
761
|
-
- No IE11 (Proxy can't be polyfilled)
|
|
762
|
-
|
|
763
|
-
**Basically: Modern browsers with ES6 Proxy support.**
|
|
764
|
-
|
|
765
|
-
---
|
|
766
|
-
|
|
767
|
-
## Testing
|
|
768
|
-
|
|
769
|
-
Lume.js uses [Vitest](https://vitest.dev) with a jsdom environment. The test suite mirrors the source tree: files under `tests/core/**` map to `src/core/**`, and `tests/addons/**` map to `src/addons/**`.
|
|
770
|
-
|
|
771
|
-
```bash
|
|
772
|
-
# Run tests
|
|
773
|
-
npm test
|
|
774
|
-
|
|
775
|
-
# Watch mode
|
|
776
|
-
npm run test:watch
|
|
777
|
-
|
|
778
|
-
# Coverage with HTML report in ./coverage
|
|
779
|
-
npm run coverage
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
**Import alias:** Tests use an alias so you can import from `src/...` without relative `../../` paths.
|
|
783
|
-
|
|
784
|
-
```js
|
|
785
|
-
// vitest.config.js
|
|
786
|
-
resolve: {
|
|
787
|
-
alias: {
|
|
788
|
-
src: fileURLToPath(new URL('./src', import.meta.url))
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
**Current coverage:**
|
|
794
|
-
- 100% statements, functions, and lines
|
|
795
|
-
- 100% branches (including edge-case paths)
|
|
796
|
-
- 67 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, reactive identity, and cleanup semantics
|
|
92
|
+
- **[Tutorial: Build a Todo App](docs/tutorials/build-todo-app.md)**
|
|
93
|
+
- **[Tutorial: Build Tic-Tac-Toe](docs/tutorials/build-tic-tac-toe.md)**
|
|
94
|
+
- **[Working with Arrays](docs/tutorials/working-with-arrays.md)**
|
|
95
|
+
- **API Reference**
|
|
96
|
+
- [Core (state, bindDom, effect)](docs/api/core/state.md)
|
|
97
|
+
- [Addons (computed, repeat)](docs/api/addons/computed.md)
|
|
98
|
+
- **[Design Philosophy](docs/design/design-decisions.md)**
|
|
797
99
|
|
|
798
100
|
---
|
|
799
101
|
|
|
800
102
|
## Contributing
|
|
801
103
|
|
|
802
|
-
We welcome contributions! Please
|
|
803
|
-
|
|
804
|
-
1. **Focus on:** Examples, documentation, bug fixes, performance
|
|
805
|
-
2. **Avoid:** Adding core features without discussion (keep it minimal!)
|
|
806
|
-
3. **Read:** [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our philosophy and why certain choices were made
|
|
807
|
-
4. **Propose alternatives:** If you think a design decision should be reconsidered, open an issue with your reasoning
|
|
808
|
-
|
|
809
|
-
Before suggesting new features, check if they align with Lume's core principles: standards-only, minimal API, no build step required.
|
|
810
|
-
|
|
811
|
-
---
|
|
104
|
+
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
812
105
|
|
|
813
106
|
## License
|
|
814
107
|
|
|
815
|
-
MIT ยฉ Sathvik C
|
|
816
|
-
|
|
817
|
-
---
|
|
818
|
-
|
|
819
|
-
## Inspiration
|
|
820
|
-
|
|
821
|
-
Lume.js is inspired by:
|
|
822
|
-
- **Knockout.js** - The original `data-bind` approach
|
|
823
|
-
- **Alpine.js** - Minimal, HTML-first philosophy
|
|
824
|
-
- **Go** - Simplicity and explicit design
|
|
825
|
-
- **The Web Platform** - Standards over abstractions
|
|
826
|
-
|
|
827
|
-
**Built for developers who want reactivity without the framework tax.**
|
|
108
|
+
MIT ยฉ [Sathvik C](https://github.com/sathvikc)
|