lume-js 0.3.0 → 0.4.1
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 +426 -39
- package/package.json +20 -3
- package/src/addons/computed.js +116 -33
- package/src/addons/index.d.ts +85 -0
- package/src/core/bindDom.js +81 -38
- package/src/core/effect.js +104 -0
- package/src/core/state.js +105 -11
- package/src/index.d.ts +56 -2
- package/src/index.js +4 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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
9
|
|
|
10
10
|
## Why Lume.js?
|
|
11
11
|
|
|
@@ -29,6 +29,8 @@ Minimal reactive state management using only standard JavaScript and HTML - no c
|
|
|
29
29
|
|
|
30
30
|
**Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
|
|
31
31
|
|
|
32
|
+
📖 **New to the project?** Read [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our design philosophy and why certain choices were made.
|
|
33
|
+
|
|
32
34
|
---
|
|
33
35
|
|
|
34
36
|
## Installation
|
|
@@ -43,7 +45,10 @@ npm install lume-js
|
|
|
43
45
|
|
|
44
46
|
```html
|
|
45
47
|
<script type="module">
|
|
46
|
-
import { state, bindDom } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
|
|
48
|
+
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';
|
|
47
52
|
</script>
|
|
48
53
|
```
|
|
49
54
|
|
|
@@ -51,6 +56,16 @@ npm install lume-js
|
|
|
51
56
|
|
|
52
57
|
## Quick Start
|
|
53
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
|
+
|
|
54
69
|
**HTML:**
|
|
55
70
|
```html
|
|
56
71
|
<div>
|
|
@@ -64,7 +79,7 @@ npm install lume-js
|
|
|
64
79
|
|
|
65
80
|
**JavaScript:**
|
|
66
81
|
```javascript
|
|
67
|
-
import { state, bindDom } from 'lume-js';
|
|
82
|
+
import { state, bindDom, effect } from 'lume-js';
|
|
68
83
|
|
|
69
84
|
// Create reactive state
|
|
70
85
|
const store = state({
|
|
@@ -72,16 +87,24 @@ const store = state({
|
|
|
72
87
|
name: 'World'
|
|
73
88
|
});
|
|
74
89
|
|
|
75
|
-
// Bind to DOM
|
|
90
|
+
// Bind to DOM (updates on state changes)
|
|
76
91
|
const cleanup = bindDom(document.body, store);
|
|
77
92
|
|
|
93
|
+
// Auto-update document title when name changes
|
|
94
|
+
const effectCleanup = effect(() => {
|
|
95
|
+
document.title = `Hello, ${store.name}!`;
|
|
96
|
+
});
|
|
97
|
+
|
|
78
98
|
// Update state with standard JavaScript
|
|
79
99
|
document.getElementById('increment').addEventListener('click', () => {
|
|
80
100
|
store.count++;
|
|
81
101
|
});
|
|
82
102
|
|
|
83
103
|
// Cleanup when done (important!)
|
|
84
|
-
window.addEventListener('beforeunload', () =>
|
|
104
|
+
window.addEventListener('beforeunload', () => {
|
|
105
|
+
cleanup();
|
|
106
|
+
effectCleanup();
|
|
107
|
+
});
|
|
85
108
|
```
|
|
86
109
|
|
|
87
110
|
That's it! No build step, no custom syntax, just HTML and JavaScript.
|
|
@@ -92,7 +115,7 @@ That's it! No build step, no custom syntax, just HTML and JavaScript.
|
|
|
92
115
|
|
|
93
116
|
### `state(object)`
|
|
94
117
|
|
|
95
|
-
Creates a reactive state object using Proxy.
|
|
118
|
+
Creates a reactive state object using Proxy with automatic dependency tracking.
|
|
96
119
|
|
|
97
120
|
```javascript
|
|
98
121
|
const store = state({
|
|
@@ -109,21 +132,63 @@ store.user.name = 'Bob';
|
|
|
109
132
|
```
|
|
110
133
|
|
|
111
134
|
**Features:**
|
|
135
|
+
- ✅ Automatic dependency tracking for effects
|
|
136
|
+
- ✅ Per-state microtask batching for performance
|
|
112
137
|
- ✅ Validates input (must be plain object)
|
|
113
138
|
- ✅ Only triggers updates when value actually changes
|
|
114
139
|
- ✅ Returns cleanup function from `$subscribe`
|
|
140
|
+
- ✅ Deduplicates effect runs per flush cycle
|
|
115
141
|
|
|
116
|
-
### `
|
|
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?)`
|
|
117
170
|
|
|
118
171
|
Binds reactive state to DOM elements with `data-bind` attributes.
|
|
119
172
|
|
|
173
|
+
**Automatically waits for DOMContentLoaded** if the document is still loading, making it safe to call from anywhere (even in `<head>`).
|
|
174
|
+
|
|
120
175
|
```javascript
|
|
176
|
+
// Default: Auto-waits for DOM (safe anywhere)
|
|
121
177
|
const cleanup = bindDom(document.body, store);
|
|
122
178
|
|
|
179
|
+
// Advanced: Force immediate binding (no auto-wait)
|
|
180
|
+
const cleanup = bindDom(myElement, store, { immediate: true });
|
|
181
|
+
|
|
123
182
|
// Later: cleanup all bindings
|
|
124
183
|
cleanup();
|
|
125
184
|
```
|
|
126
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
|
+
|
|
127
192
|
**Supports:**
|
|
128
193
|
- ✅ Text content: `<span data-bind="count"></span>`
|
|
129
194
|
- ✅ Input values: `<input data-bind="name">`
|
|
@@ -134,14 +199,111 @@ cleanup();
|
|
|
134
199
|
- ✅ Radio buttons: `<input type="radio" data-bind="choice">`
|
|
135
200
|
- ✅ Nested paths: `<span data-bind="user.name"></span>`
|
|
136
201
|
|
|
137
|
-
**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
<script type="module">
|
|
271
|
+
import { state, bindDom } from 'lume-js';
|
|
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
|
|
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
|
+
```
|
|
141
303
|
|
|
142
304
|
### `$subscribe(key, callback)`
|
|
143
305
|
|
|
144
|
-
Manually subscribe to state changes.
|
|
306
|
+
Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
|
|
145
307
|
|
|
146
308
|
```javascript
|
|
147
309
|
const unsubscribe = store.$subscribe('count', (value) => {
|
|
@@ -157,10 +319,135 @@ const unsubscribe = store.$subscribe('count', (value) => {
|
|
|
157
319
|
unsubscribe();
|
|
158
320
|
```
|
|
159
321
|
|
|
160
|
-
**
|
|
161
|
-
- Returns unsubscribe function
|
|
162
|
-
- Validates callback is a function
|
|
163
|
-
-
|
|
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:**
|
|
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()
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Examples:**
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
// 1. bindDom - Zero code DOM sync
|
|
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);
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Gotchas:**
|
|
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
|
+
```
|
|
164
451
|
|
|
165
452
|
---
|
|
166
453
|
|
|
@@ -262,14 +549,65 @@ window.addEventListener('beforeunload', () => {
|
|
|
262
549
|
<input type="checkbox" data-bind="user.settings.notifications">
|
|
263
550
|
```
|
|
264
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
|
+
|
|
265
597
|
### Integration with GSAP
|
|
266
598
|
|
|
267
599
|
```javascript
|
|
268
600
|
import gsap from 'gsap';
|
|
269
|
-
import { state } from 'lume-js';
|
|
601
|
+
import { state, effect } from 'lume-js';
|
|
270
602
|
|
|
271
603
|
const ui = state({ x: 0, y: 0 });
|
|
272
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
|
|
273
611
|
const unsubX = ui.$subscribe('x', (value) => {
|
|
274
612
|
gsap.to('.box', { x: value, duration: 0.5 });
|
|
275
613
|
});
|
|
@@ -283,17 +621,28 @@ window.addEventListener('beforeunload', () => unsubX());
|
|
|
283
621
|
### Cleanup Pattern (Important!)
|
|
284
622
|
|
|
285
623
|
```javascript
|
|
624
|
+
import { state, effect, bindDom } from 'lume-js';
|
|
625
|
+
import { computed } from 'lume-js/addons';
|
|
626
|
+
|
|
286
627
|
const store = state({ data: [] });
|
|
287
628
|
const cleanup = bindDom(root, store);
|
|
288
629
|
|
|
289
630
|
const unsub1 = store.$subscribe('data', handleData);
|
|
290
631
|
const unsub2 = store.$subscribe('status', handleStatus);
|
|
291
632
|
|
|
633
|
+
const effectCleanup = effect(() => {
|
|
634
|
+
console.log('Data length:', store.data.length);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const total = computed(() => store.data.length * 2);
|
|
638
|
+
|
|
292
639
|
// Cleanup when component unmounts
|
|
293
640
|
function destroy() {
|
|
294
|
-
cleanup();
|
|
295
|
-
unsub1();
|
|
296
|
-
unsub2();
|
|
641
|
+
cleanup(); // Remove DOM bindings
|
|
642
|
+
unsub1(); // Remove subscription 1
|
|
643
|
+
unsub2(); // Remove subscription 2
|
|
644
|
+
effectCleanup(); // Stop effect
|
|
645
|
+
total.dispose(); // Stop computed
|
|
297
646
|
}
|
|
298
647
|
|
|
299
648
|
// For SPA frameworks
|
|
@@ -380,25 +729,27 @@ document.addEventListener('click', () => store.clicks++);
|
|
|
380
729
|
|
|
381
730
|
---
|
|
382
731
|
|
|
383
|
-
## What's New
|
|
384
|
-
|
|
385
|
-
###
|
|
386
|
-
- ✅ `
|
|
387
|
-
- ✅
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
- ✅
|
|
391
|
-
|
|
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
|
|
392
751
|
- ✅ Better error handling with `[Lume.js]` prefix
|
|
393
|
-
- ✅ Input validation
|
|
394
|
-
- ✅ Only triggers on actual value changes
|
|
395
|
-
- ✅ Support for checkboxes, radio buttons, number inputs
|
|
396
|
-
- ✅ Comprehensive example in `/examples/comprehensive/`
|
|
397
|
-
|
|
398
|
-
### Bug Fixes
|
|
399
|
-
- ✅ Fixed memory leaks (no cleanup in v0.2.x)
|
|
400
|
-
- ✅ Fixed addon examples (used wrong API)
|
|
401
|
-
- ✅ Better path resolution with detailed errors
|
|
752
|
+
- ✅ Input validation on all public APIs
|
|
402
753
|
|
|
403
754
|
---
|
|
404
755
|
|
|
@@ -413,13 +764,49 @@ document.addEventListener('click', () => store.clicks++);
|
|
|
413
764
|
|
|
414
765
|
---
|
|
415
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
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
416
800
|
## Contributing
|
|
417
801
|
|
|
418
802
|
We welcome contributions! Please:
|
|
419
803
|
|
|
420
804
|
1. **Focus on:** Examples, documentation, bug fixes, performance
|
|
421
805
|
2. **Avoid:** Adding core features without discussion (keep it minimal!)
|
|
422
|
-
3. **
|
|
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.
|
|
423
810
|
|
|
424
811
|
---
|
|
425
812
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"types": "./src/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./addons": {
|
|
15
|
+
"import": "./src/addons/index.js",
|
|
16
|
+
"types": "./src/addons/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
8
19
|
"scripts": {
|
|
9
20
|
"dev": "vite",
|
|
10
21
|
"build": "echo 'No build step needed - zero-runtime library!'",
|
|
11
|
-
"
|
|
22
|
+
"size": "node scripts/check-size.js",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"coverage": "vitest run --coverage"
|
|
12
26
|
},
|
|
13
27
|
"files": [
|
|
14
28
|
"src",
|
|
@@ -45,7 +59,10 @@
|
|
|
45
59
|
},
|
|
46
60
|
"homepage": "https://github.com/sathvikc/lume-js#readme",
|
|
47
61
|
"devDependencies": {
|
|
48
|
-
"vite": "^7.1.9"
|
|
62
|
+
"vite": "^7.1.9",
|
|
63
|
+
"vitest": "^2.1.4",
|
|
64
|
+
"@vitest/coverage-v8": "^2.1.4",
|
|
65
|
+
"jsdom": "^25.0.1"
|
|
49
66
|
},
|
|
50
67
|
"engines": {
|
|
51
68
|
"node": ">=20.19.0"
|