lume-js 0.5.0 โ 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 -788
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -2,853 +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:**
|
|
38
|
+
**Version Pinning:**
|
|
70
39
|
```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
|
-
|
|
217
|
-
```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!
|
|
274
|
-
</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
|
|
41
|
+
import { state } from 'https://cdn.jsdelivr.net/npm/lume-js@1.0.0/src/index.js';
|
|
287
42
|
</script>
|
|
288
43
|
```
|
|
289
44
|
|
|
290
|
-
|
|
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
|
-
```
|
|
321
|
-
|
|
322
|
-
**Features:**
|
|
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
|
-
```
|
|
385
|
-
|
|
386
|
-
**Note:** `watch()` is just a convenience wrapper around `store.$subscribe()`. Use whichever feels more natural.
|
|
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:**
|
|
45
|
+
### Via NPM (Recommended for bundlers)
|
|
402
46
|
|
|
47
|
+
```bash
|
|
48
|
+
npm install lume-js
|
|
403
49
|
```
|
|
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()
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
**Examples:**
|
|
413
50
|
|
|
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;
|
|
670
|
-
|
|
671
|
-
// โ
Use with GSAP
|
|
672
|
-
gsap.to(el, { x: store.position });
|
|
673
|
-
|
|
674
|
-
// โ
Use with any router
|
|
675
|
-
router.on('/home', () => store.route = 'home');
|
|
676
|
-
|
|
677
|
-
// โ
Mix with vanilla JS
|
|
678
|
-
document.addEventListener('click', () => store.clicks++);
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
**Lume.js doesn't hijack your architecture - it enhances it.**
|
|
72
|
+
import { state, bindDom } from 'lume-js';
|
|
682
73
|
|
|
683
|
-
|
|
74
|
+
// 1. Create state
|
|
75
|
+
const store = state({ name: 'World' });
|
|
684
76
|
|
|
685
|
-
|
|
686
|
-
|
|
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>
|
|
77
|
+
// 2. Bind to DOM
|
|
78
|
+
bindDom(document.body, store);
|
|
710
79
|
```
|
|
711
80
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
### โ
Perfect For:
|
|
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
|
|
724
|
-
|
|
725
|
-
### โ ๏ธ Consider Alternatives:
|
|
726
|
-
- **Complex SPAs** โ Use React, Vue, or Svelte
|
|
727
|
-
- **Need routing/SSR** โ Use Next.js, Nuxt, or SvelteKit
|
|
728
|
-
- **Prefer terse syntax** โ Use Alpine.js (if custom syntax is okay)
|
|
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
|
|
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.
|
|
753
85
|
|
|
754
86
|
---
|
|
755
87
|
|
|
756
|
-
##
|
|
88
|
+
## Documentation
|
|
757
89
|
|
|
758
|
-
|
|
759
|
-
- Firefox 18+
|
|
760
|
-
- Safari 10+
|
|
761
|
-
- No IE11 (Proxy can't be polyfilled)
|
|
90
|
+
Full documentation is available in the [docs/](docs/) directory:
|
|
762
91
|
|
|
763
|
-
**
|
|
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)**
|
|
764
99
|
|
|
765
100
|
---
|
|
766
101
|
|
|
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
|
-
- 114 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, reactive identity, and cleanup semantics
|
|
797
|
-
|
|
798
|
-
---
|
|
799
|
-
|
|
800
|
-
### `repeat(container, store, key, options)`
|
|
801
|
-
|
|
802
|
-
**@experimental** - API may change in future versions.
|
|
803
|
-
|
|
804
|
-
Efficiently renders lists with element reuse and automatic subscription.
|
|
805
|
-
|
|
806
|
-
```javascript
|
|
807
|
-
import { repeat } from 'lume-js/addons/repeat.js';
|
|
808
|
-
|
|
809
|
-
// โ ๏ธ IMPORTANT: Arrays must be updated immutably!
|
|
810
|
-
// store.items.push(newItem); // โ Won't trigger update
|
|
811
|
-
// store.items = [...store.items, newItem]; // โ
Triggers update
|
|
812
|
-
|
|
813
|
-
repeat('#list', store, 'items', {
|
|
814
|
-
key: item => item.id,
|
|
815
|
-
render: (item, el) => {
|
|
816
|
-
el.textContent = item.name;
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
**Features:**
|
|
822
|
-
- โ
**Element Reuse** - Reuses DOM nodes by key (no full re-renders)
|
|
823
|
-
- โ
**Focus Preservation** - Maintains active element and selection during updates
|
|
824
|
-
- โ
**Scroll Preservation** - Maintains scroll position during updates
|
|
825
|
-
- โ
**Automatic Subscription** - Subscribes to the array key automatically
|
|
826
|
-
|
|
827
102
|
## Contributing
|
|
828
103
|
|
|
829
|
-
We welcome contributions! Please
|
|
830
|
-
|
|
831
|
-
1. **Focus on:** Examples, documentation, bug fixes, performance
|
|
832
|
-
2. **Avoid:** Adding core features without discussion (keep it minimal!)
|
|
833
|
-
3. **Read:** [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our philosophy and why certain choices were made
|
|
834
|
-
4. **Propose alternatives:** If you think a design decision should be reconsidered, open an issue with your reasoning
|
|
835
|
-
|
|
836
|
-
Before suggesting new features, check if they align with Lume's core principles: standards-only, minimal API, no build step required.
|
|
837
|
-
|
|
838
|
-
---
|
|
104
|
+
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
839
105
|
|
|
840
106
|
## License
|
|
841
107
|
|
|
842
|
-
MIT ยฉ Sathvik C
|
|
843
|
-
|
|
844
|
-
---
|
|
845
|
-
|
|
846
|
-
## Inspiration
|
|
847
|
-
|
|
848
|
-
Lume.js is inspired by:
|
|
849
|
-
- **Knockout.js** - The original `data-bind` approach
|
|
850
|
-
- **Alpine.js** - Minimal, HTML-first philosophy
|
|
851
|
-
- **Go** - Simplicity and explicit design
|
|
852
|
-
- **The Web Platform** - Standards over abstractions
|
|
853
|
-
|
|
854
|
-
**Built for developers who want reactivity without the framework tax.**
|
|
108
|
+
MIT ยฉ [Sathvik C](https://github.com/sathvikc)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"dev": "vite",
|
|
21
|
+
"dev:site": "node scripts/build-site-assets.js && vite -c vite.site.config.js",
|
|
21
22
|
"build": "echo 'No build step needed - zero-runtime library!'",
|
|
22
23
|
"size": "node scripts/check-size.js",
|
|
23
24
|
"test": "vitest run",
|
|
@@ -57,12 +58,19 @@
|
|
|
57
58
|
"bugs": {
|
|
58
59
|
"url": "https://github.com/sathvikc/lume-js/issues"
|
|
59
60
|
},
|
|
60
|
-
"homepage": "https://github.
|
|
61
|
+
"homepage": "https://sathvikc.github.io/lume-js/",
|
|
61
62
|
"devDependencies": {
|
|
62
|
-
"
|
|
63
|
-
"
|
|
63
|
+
"@tailwindcss/postcss": "^4.1.17",
|
|
64
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
64
65
|
"@vitest/coverage-v8": "^2.1.4",
|
|
65
|
-
"
|
|
66
|
+
"autoprefixer": "^10.4.22",
|
|
67
|
+
"highlight.js": "^11.11.1",
|
|
68
|
+
"jsdom": "^25.0.1",
|
|
69
|
+
"marked": "^17.0.1",
|
|
70
|
+
"postcss": "^8.5.6",
|
|
71
|
+
"tailwindcss": "^4.1.17",
|
|
72
|
+
"vite": "^7.1.9",
|
|
73
|
+
"vitest": "^2.1.4"
|
|
66
74
|
},
|
|
67
75
|
"engines": {
|
|
68
76
|
"node": ">=20.19.0"
|