lume-js 0.2.1 โ 0.4.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 +623 -24
- package/package.json +47 -4
- package/src/addons/computed.js +126 -28
- package/src/addons/watch.js +4 -3
- package/src/core/bindDom.js +94 -16
- package/src/core/effect.js +104 -0
- package/src/core/state.js +115 -13
- package/src/core/utils.js +33 -6
- package/src/index.d.ts +115 -0
- package/src/index.js +3 -1
package/README.md
CHANGED
|
@@ -1,51 +1,650 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Lume.js
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Inspired by Go-style simplicity.
|
|
3
|
+
**Reactivity that follows web standards.**
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
5
|
+
Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required, no framework lock-in.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](package.json)
|
|
9
|
+
|
|
10
|
+
## Why Lume.js?
|
|
11
|
+
|
|
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
|
+
|
|
20
|
+
### vs Other Libraries
|
|
21
|
+
|
|
22
|
+
| Feature | Lume.js | Alpine.js | Vue | React |
|
|
23
|
+
|---------|---------|-----------|-----|-------|
|
|
24
|
+
| Custom Syntax | โ No | โ
`x-data`, `@click` | โ
`v-bind`, `v-model` | โ
JSX |
|
|
25
|
+
| Build Step | โ Optional | โ Optional | โ ๏ธ Recommended | โ
Required |
|
|
26
|
+
| Bundle Size | ~2KB | ~15KB | ~35KB | ~45KB |
|
|
27
|
+
| HTML Validation | โ
Pass | โ ๏ธ Warnings | โ ๏ธ Warnings | โ JSX |
|
|
28
|
+
| Cleanup API | โ
Yes | โ ๏ธ Limited | โ
Yes | โ
Yes |
|
|
29
|
+
|
|
30
|
+
**Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
|
|
11
31
|
|
|
12
|
-
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### Via npm
|
|
13
37
|
|
|
14
38
|
```bash
|
|
15
39
|
npm install lume-js
|
|
16
40
|
```
|
|
17
41
|
|
|
18
|
-
|
|
42
|
+
### Via CDN
|
|
19
43
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import { state, bindDom } from
|
|
44
|
+
```html
|
|
45
|
+
<script type="module">
|
|
46
|
+
import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
|
|
47
|
+
|
|
48
|
+
// For addons:
|
|
49
|
+
import { computed, watch } from 'https://cdn.jsdelivr.net/npm/lume-js/src/addons/index.js';
|
|
50
|
+
</script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
23
54
|
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
### Examples
|
|
58
|
+
|
|
59
|
+
Run the live examples (including the new Todo app) with Vite:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then open the Examples index (Vite will auto-open): `http://localhost:5173/examples/`
|
|
66
|
+
|
|
67
|
+
**HTML:**
|
|
68
|
+
```html
|
|
69
|
+
<div>
|
|
70
|
+
<p>Count: <span data-bind="count"></span></p>
|
|
71
|
+
<input data-bind="name" placeholder="Enter name">
|
|
72
|
+
<p>Hello, <span data-bind="name"></span>!</p>
|
|
73
|
+
|
|
74
|
+
<button id="increment">+</button>
|
|
75
|
+
</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**JavaScript:**
|
|
79
|
+
```javascript
|
|
80
|
+
import { state, bindDom, effect } from 'lume-js';
|
|
81
|
+
|
|
82
|
+
// Create reactive state
|
|
24
83
|
const store = state({
|
|
25
84
|
count: 0,
|
|
26
|
-
|
|
85
|
+
name: 'World'
|
|
27
86
|
});
|
|
28
87
|
|
|
29
|
-
|
|
88
|
+
// Bind to DOM (updates on state changes)
|
|
89
|
+
const cleanup = bindDom(document.body, store);
|
|
90
|
+
|
|
91
|
+
// Auto-update document title when name changes
|
|
92
|
+
const effectCleanup = effect(() => {
|
|
93
|
+
document.title = `Hello, ${store.name}!`;
|
|
94
|
+
});
|
|
30
95
|
|
|
31
|
-
|
|
96
|
+
// Update state with standard JavaScript
|
|
97
|
+
document.getElementById('increment').addEventListener('click', () => {
|
|
32
98
|
store.count++;
|
|
33
99
|
});
|
|
34
100
|
|
|
35
|
-
|
|
36
|
-
|
|
101
|
+
// Cleanup when done (important!)
|
|
102
|
+
window.addEventListener('beforeunload', () => {
|
|
103
|
+
cleanup();
|
|
104
|
+
effectCleanup();
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That's it! No build step, no custom syntax, just HTML and JavaScript.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Core API
|
|
113
|
+
|
|
114
|
+
### `state(object)`
|
|
115
|
+
|
|
116
|
+
Creates a reactive state object using Proxy with automatic dependency tracking.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const store = state({
|
|
120
|
+
count: 0,
|
|
121
|
+
user: state({ // Nested state must be wrapped
|
|
122
|
+
name: 'Alice',
|
|
123
|
+
email: 'alice@example.com'
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Update state
|
|
128
|
+
store.count++;
|
|
129
|
+
store.user.name = 'Bob';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Features:**
|
|
133
|
+
- โ
Automatic dependency tracking for effects
|
|
134
|
+
- โ
Per-state microtask batching for performance
|
|
135
|
+
- โ
Validates input (must be plain object)
|
|
136
|
+
- โ
Only triggers updates when value actually changes
|
|
137
|
+
- โ
Returns cleanup function from `$subscribe`
|
|
138
|
+
- โ
Deduplicates effect runs per flush cycle
|
|
139
|
+
|
|
140
|
+
### `effect(fn)`
|
|
141
|
+
|
|
142
|
+
Creates an effect that automatically tracks dependencies and re-runs when they change.
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const store = state({ count: 0, name: 'Alice' });
|
|
146
|
+
|
|
147
|
+
const cleanup = effect(() => {
|
|
148
|
+
// Only tracks 'count' (name is not accessed)
|
|
149
|
+
console.log(`Count: ${store.count}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
store.count = 5; // Effect re-runs
|
|
153
|
+
store.name = 'Bob'; // Effect does NOT re-run
|
|
154
|
+
|
|
155
|
+
cleanup(); // Stop the effect
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Features:**
|
|
159
|
+
- โ
Automatic dependency collection (tracks what you actually access)
|
|
160
|
+
- โ
Dynamic dependencies (re-tracks on every execution)
|
|
161
|
+
- โ
Returns cleanup function
|
|
162
|
+
- โ
Prevents infinite recursion
|
|
163
|
+
- โ
Integrates with per-state batching (no global scheduler)
|
|
164
|
+
|
|
165
|
+
**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.
|
|
166
|
+
|
|
167
|
+
### `bindDom(root, store)`
|
|
168
|
+
|
|
169
|
+
Binds reactive state to DOM elements with `data-bind` attributes.
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const cleanup = bindDom(document.body, store);
|
|
173
|
+
|
|
174
|
+
// Later: cleanup all bindings
|
|
175
|
+
cleanup();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Supports:**
|
|
179
|
+
- โ
Text content: `<span data-bind="count"></span>`
|
|
180
|
+
- โ
Input values: `<input data-bind="name">`
|
|
181
|
+
- โ
Textareas: `<textarea data-bind="bio"></textarea>`
|
|
182
|
+
- โ
Selects: `<select data-bind="theme"></select>`
|
|
183
|
+
- โ
Checkboxes: `<input type="checkbox" data-bind="enabled">`
|
|
184
|
+
- โ
Numbers: `<input type="number" data-bind="age">`
|
|
185
|
+
- โ
Radio buttons: `<input type="radio" data-bind="choice">`
|
|
186
|
+
- โ
Nested paths: `<span data-bind="user.name"></span>`
|
|
187
|
+
|
|
188
|
+
**Features:**
|
|
189
|
+
- โ
Returns cleanup function
|
|
190
|
+
- โ
Better error messages with `[Lume.js]` prefix
|
|
191
|
+
- โ
Handles edge cases (empty bindings, invalid paths)
|
|
192
|
+
- โ
Two-way binding for form inputs
|
|
193
|
+
|
|
194
|
+
### `$subscribe(key, callback)`
|
|
195
|
+
|
|
196
|
+
Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
const unsubscribe = store.$subscribe('count', (value) => {
|
|
200
|
+
console.log('Count changed:', value);
|
|
201
|
+
|
|
202
|
+
// Integrate with other libraries
|
|
203
|
+
if (value > 10) {
|
|
204
|
+
showNotification('Count is high!');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Cleanup
|
|
209
|
+
unsubscribe();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Features:**
|
|
213
|
+
- โ
Returns unsubscribe function
|
|
214
|
+
- โ
Validates callback is a function
|
|
215
|
+
- โ
Calls immediately with current value (not batched)
|
|
216
|
+
- โ
Only notifies on actual value changes (via batching)
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Addons
|
|
221
|
+
|
|
222
|
+
Lume.js provides optional addons for advanced reactivity patterns. Import from `lume-js/addons`.
|
|
223
|
+
|
|
224
|
+
### `computed(fn)`
|
|
225
|
+
|
|
226
|
+
Creates a computed value that automatically updates when its dependencies change.
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
import { state, effect } from 'lume-js';
|
|
230
|
+
import { computed } from 'lume-js/addons';
|
|
231
|
+
|
|
232
|
+
const store = state({ count: 5 });
|
|
233
|
+
|
|
234
|
+
const doubled = computed(() => store.count * 2);
|
|
235
|
+
console.log(doubled.value); // 10
|
|
236
|
+
|
|
237
|
+
store.count = 10;
|
|
238
|
+
// After microtask:
|
|
239
|
+
console.log(doubled.value); // 20 (auto-updated)
|
|
240
|
+
|
|
241
|
+
// Subscribe to changes
|
|
242
|
+
const unsub = doubled.subscribe(value => {
|
|
243
|
+
console.log('Doubled:', value);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Cleanup
|
|
247
|
+
doubled.dispose();
|
|
248
|
+
unsub();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Features:**
|
|
252
|
+
- โ
Automatic dependency tracking using `effect()`
|
|
253
|
+
- โ
Cached values (only recomputes when dependencies change)
|
|
254
|
+
- โ
Subscribe to changes with `.subscribe(callback)`
|
|
255
|
+
- โ
Cleanup with `.dispose()`
|
|
256
|
+
- โ
Error handling (sets to undefined on error)
|
|
257
|
+
|
|
258
|
+
### `watch(store, key, callback)`
|
|
259
|
+
|
|
260
|
+
Alias for `$subscribe` - observes changes to a specific state key.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
import { state } from 'lume-js';
|
|
264
|
+
import { watch } from 'lume-js/addons';
|
|
265
|
+
|
|
266
|
+
const store = state({ count: 0 });
|
|
267
|
+
|
|
268
|
+
const unwatch = watch(store, 'count', (value) => {
|
|
269
|
+
console.log('Count is now:', value);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Cleanup
|
|
273
|
+
unwatch();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Note:** `watch()` is just a convenience wrapper around `store.$subscribe()`. Use whichever feels more natural.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Examples
|
|
281
|
+
|
|
282
|
+
### Basic Counter
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
const store = state({ count: 0 });
|
|
286
|
+
const cleanup = bindDom(document.body, store);
|
|
287
|
+
|
|
288
|
+
document.getElementById('inc').addEventListener('click', () => {
|
|
289
|
+
store.count++;
|
|
37
290
|
});
|
|
291
|
+
|
|
292
|
+
// Cleanup on unmount
|
|
293
|
+
window.addEventListener('beforeunload', () => cleanup());
|
|
38
294
|
```
|
|
39
295
|
|
|
40
|
-
**index.html**
|
|
41
296
|
```html
|
|
42
297
|
<p>Count: <span data-bind="count"></span></p>
|
|
43
|
-
<p>User Name: <span data-bind="user.name"></span></p>
|
|
44
|
-
|
|
45
298
|
<button id="inc">Increment</button>
|
|
46
|
-
<button id="changeName">Change Name</button>
|
|
47
299
|
```
|
|
48
300
|
|
|
49
|
-
|
|
301
|
+
### Form Handling with Validation
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
const form = state({
|
|
305
|
+
email: '',
|
|
306
|
+
age: 25,
|
|
307
|
+
theme: 'light',
|
|
308
|
+
errors: {}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const cleanup = bindDom(document.querySelector('form'), form);
|
|
312
|
+
|
|
313
|
+
// Validate on change
|
|
314
|
+
const unsubEmail = form.$subscribe('email', (value) => {
|
|
315
|
+
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
316
|
+
form.errors = {
|
|
317
|
+
...form.errors,
|
|
318
|
+
email: isValid ? '' : 'Invalid email'
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Cleanup
|
|
323
|
+
window.addEventListener('beforeunload', () => {
|
|
324
|
+
cleanup();
|
|
325
|
+
unsubEmail();
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<form>
|
|
331
|
+
<input type="email" data-bind="email">
|
|
332
|
+
<span data-bind="errors.email" style="color: red;"></span>
|
|
333
|
+
|
|
334
|
+
<input type="number" data-bind="age">
|
|
335
|
+
|
|
336
|
+
<select data-bind="theme">
|
|
337
|
+
<option value="light">Light</option>
|
|
338
|
+
<option value="dark">Dark</option>
|
|
339
|
+
</select>
|
|
340
|
+
</form>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Nested State
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
const store = state({
|
|
347
|
+
user: state({
|
|
348
|
+
profile: state({
|
|
349
|
+
name: 'Alice',
|
|
350
|
+
bio: 'Developer'
|
|
351
|
+
}),
|
|
352
|
+
settings: state({
|
|
353
|
+
notifications: true
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const cleanup = bindDom(document.body, store);
|
|
359
|
+
|
|
360
|
+
// Subscribe to nested changes
|
|
361
|
+
const unsub = store.user.profile.$subscribe('name', (name) => {
|
|
362
|
+
console.log('Profile name changed:', name);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Cleanup
|
|
366
|
+
window.addEventListener('beforeunload', () => {
|
|
367
|
+
cleanup();
|
|
368
|
+
unsub();
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<input data-bind="user.profile.name">
|
|
374
|
+
<textarea data-bind="user.profile.bio"></textarea>
|
|
375
|
+
<input type="checkbox" data-bind="user.settings.notifications">
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Using Effects for Auto-Updates
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
import { state, effect } from 'lume-js';
|
|
382
|
+
|
|
383
|
+
const store = state({
|
|
384
|
+
firstName: 'Alice',
|
|
385
|
+
lastName: 'Smith'
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Auto-update title when name changes
|
|
389
|
+
effect(() => {
|
|
390
|
+
document.title = `${store.firstName} ${store.lastName}`;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
store.firstName = 'Bob';
|
|
394
|
+
// Title automatically updates to "Bob Smith" in next microtask
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Computed Values
|
|
398
|
+
|
|
399
|
+
```javascript
|
|
400
|
+
import { state } from 'lume-js';
|
|
401
|
+
import { computed } from 'lume-js/addons';
|
|
402
|
+
|
|
403
|
+
const cart = state({
|
|
404
|
+
items: state([
|
|
405
|
+
state({ price: 10, quantity: 2 }),
|
|
406
|
+
state({ price: 15, quantity: 1 })
|
|
407
|
+
])
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const total = computed(() => {
|
|
411
|
+
return cart.items.reduce((sum, item) =>
|
|
412
|
+
sum + (item.price * item.quantity), 0
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
console.log(total.value); // 35
|
|
417
|
+
|
|
418
|
+
cart.items[0].quantity = 3;
|
|
419
|
+
// After microtask:
|
|
420
|
+
console.log(total.value); // 45
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Integration with GSAP
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
import gsap from 'gsap';
|
|
427
|
+
import { state, effect } from 'lume-js';
|
|
428
|
+
|
|
429
|
+
const ui = state({ x: 0, y: 0 });
|
|
430
|
+
|
|
431
|
+
// Use effect for automatic animation updates
|
|
432
|
+
effect(() => {
|
|
433
|
+
gsap.to('.box', { x: ui.x, y: ui.y, duration: 0.5 });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Or use $subscribe
|
|
437
|
+
const unsubX = ui.$subscribe('x', (value) => {
|
|
438
|
+
gsap.to('.box', { x: value, duration: 0.5 });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Now ui.x = 100 triggers smooth animation
|
|
442
|
+
|
|
443
|
+
// Cleanup
|
|
444
|
+
window.addEventListener('beforeunload', () => unsubX());
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Cleanup Pattern (Important!)
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
import { state, effect, bindDom } from 'lume-js';
|
|
451
|
+
import { computed } from 'lume-js/addons';
|
|
452
|
+
|
|
453
|
+
const store = state({ data: [] });
|
|
454
|
+
const cleanup = bindDom(root, store);
|
|
455
|
+
|
|
456
|
+
const unsub1 = store.$subscribe('data', handleData);
|
|
457
|
+
const unsub2 = store.$subscribe('status', handleStatus);
|
|
458
|
+
|
|
459
|
+
const effectCleanup = effect(() => {
|
|
460
|
+
console.log('Data length:', store.data.length);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const total = computed(() => store.data.length * 2);
|
|
464
|
+
|
|
465
|
+
// Cleanup when component unmounts
|
|
466
|
+
function destroy() {
|
|
467
|
+
cleanup(); // Remove DOM bindings
|
|
468
|
+
unsub1(); // Remove subscription 1
|
|
469
|
+
unsub2(); // Remove subscription 2
|
|
470
|
+
effectCleanup(); // Stop effect
|
|
471
|
+
total.dispose(); // Stop computed
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// For SPA frameworks
|
|
475
|
+
onUnmount(destroy);
|
|
476
|
+
|
|
477
|
+
// For vanilla JS
|
|
478
|
+
window.addEventListener('beforeunload', destroy);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Philosophy
|
|
484
|
+
|
|
485
|
+
### Standards-Only
|
|
486
|
+
- Uses only `data-*` attributes (HTML5 standard)
|
|
487
|
+
- Uses only standard JavaScript APIs (Proxy, addEventListener)
|
|
488
|
+
- No custom syntax that breaks validators
|
|
489
|
+
- Works with any tool/library
|
|
490
|
+
|
|
491
|
+
### No Artificial Limitations
|
|
492
|
+
```javascript
|
|
493
|
+
// โ
Use with jQuery
|
|
494
|
+
$('.modal').fadeIn();
|
|
495
|
+
store.modalOpen = true;
|
|
496
|
+
|
|
497
|
+
// โ
Use with GSAP
|
|
498
|
+
gsap.to(el, { x: store.position });
|
|
499
|
+
|
|
500
|
+
// โ
Use with any router
|
|
501
|
+
router.on('/home', () => store.route = 'home');
|
|
502
|
+
|
|
503
|
+
// โ
Mix with vanilla JS
|
|
504
|
+
document.addEventListener('click', () => store.clicks++);
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Lume.js doesn't hijack your architecture - it enhances it.**
|
|
508
|
+
|
|
509
|
+
### Progressive Enhancement
|
|
510
|
+
|
|
511
|
+
```html
|
|
512
|
+
<!-- Works without JavaScript (server-rendered) -->
|
|
513
|
+
<form action="/submit" method="POST">
|
|
514
|
+
<input name="email" value="alice@example.com">
|
|
515
|
+
<button type="submit">Save</button>
|
|
516
|
+
</form>
|
|
517
|
+
|
|
518
|
+
<script type="module">
|
|
519
|
+
// Enhanced when JS loads
|
|
520
|
+
import { state, bindDom } from 'lume-js';
|
|
521
|
+
|
|
522
|
+
const form = state({ email: 'alice@example.com' });
|
|
523
|
+
const cleanup = bindDom(document.querySelector('form'), form);
|
|
524
|
+
|
|
525
|
+
// Prevent default, use AJAX
|
|
526
|
+
document.querySelector('form').addEventListener('submit', async (e) => {
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
await fetch('/submit', {
|
|
529
|
+
method: 'POST',
|
|
530
|
+
body: JSON.stringify({ email: form.email })
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
window.addEventListener('beforeunload', () => cleanup());
|
|
535
|
+
</script>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Who Should Use Lume.js?
|
|
541
|
+
|
|
542
|
+
### โ
Perfect For:
|
|
543
|
+
- WordPress/Shopify theme developers
|
|
544
|
+
- Accessibility-focused teams (government, healthcare, education)
|
|
545
|
+
- Legacy codebases that can't do full rewrites
|
|
546
|
+
- Developers who hate learning framework-specific syntax
|
|
547
|
+
- Progressive enhancement advocates
|
|
548
|
+
- Projects requiring HTML validation
|
|
549
|
+
- Adding reactivity to server-rendered apps
|
|
550
|
+
|
|
551
|
+
### โ ๏ธ Consider Alternatives:
|
|
552
|
+
- **Complex SPAs** โ Use React, Vue, or Svelte
|
|
553
|
+
- **Need routing/SSR** โ Use Next.js, Nuxt, or SvelteKit
|
|
554
|
+
- **Prefer terse syntax** โ Use Alpine.js (if custom syntax is okay)
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## What's New
|
|
559
|
+
|
|
560
|
+
### Core Features
|
|
561
|
+
- โ
**Automatic dependency tracking** - `effect()` automatically tracks which state properties are accessed
|
|
562
|
+
- โ
**Per-state batching** - Each state object maintains its own microtask flush for optimal performance
|
|
563
|
+
- โ
**Effect deduplication** - Effects only run once per flush cycle, even if multiple dependencies change
|
|
564
|
+
- โ
**TypeScript support** - Full type definitions in `index.d.ts`
|
|
565
|
+
- โ
**Cleanup functions** - All reactive APIs return cleanup/unsubscribe functions
|
|
566
|
+
|
|
567
|
+
### Addons
|
|
568
|
+
- โ
**`computed()`** - Memoized computed values with automatic dependency tracking
|
|
569
|
+
- โ
**`watch()`** - Convenience alias for `$subscribe`
|
|
570
|
+
|
|
571
|
+
### API Design
|
|
572
|
+
- โ
`state()` - Create reactive state with automatic tracking support
|
|
573
|
+
- โ
`effect()` - Core reactivity primitive (automatic dependency collection)
|
|
574
|
+
- โ
`bindDom()` - DOM binding with two-way sync for form inputs
|
|
575
|
+
- โ
`$subscribe()` - Manual subscriptions (calls immediately with current value)
|
|
576
|
+
- โ
All functions return cleanup/unsubscribe functions
|
|
577
|
+
- โ
Better error handling with `[Lume.js]` prefix
|
|
578
|
+
- โ
Input validation on all public APIs
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Browser Support
|
|
583
|
+
|
|
584
|
+
- Chrome/Edge 49+
|
|
585
|
+
- Firefox 18+
|
|
586
|
+
- Safari 10+
|
|
587
|
+
- No IE11 (Proxy can't be polyfilled)
|
|
588
|
+
|
|
589
|
+
**Basically: Modern browsers with ES6 Proxy support.**
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Testing
|
|
594
|
+
|
|
595
|
+
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/**`.
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
# Run tests
|
|
599
|
+
npm test
|
|
600
|
+
|
|
601
|
+
# Watch mode
|
|
602
|
+
npm run test:watch
|
|
603
|
+
|
|
604
|
+
# Coverage with HTML report in ./coverage
|
|
605
|
+
npm run coverage
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Import alias:** Tests use an alias so you can import from `src/...` without relative `../../` paths.
|
|
609
|
+
|
|
610
|
+
```js
|
|
611
|
+
// vitest.config.js
|
|
612
|
+
resolve: {
|
|
613
|
+
alias: {
|
|
614
|
+
src: fileURLToPath(new URL('./src', import.meta.url))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Current coverage:**
|
|
620
|
+
- 100% statements, functions, and lines
|
|
621
|
+
- 100% branches (including edge-case paths)
|
|
622
|
+
- 37 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, and cleanup semantics
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Contributing
|
|
627
|
+
|
|
628
|
+
We welcome contributions! Please:
|
|
629
|
+
|
|
630
|
+
1. **Focus on:** Examples, documentation, bug fixes, performance
|
|
631
|
+
2. **Avoid:** Adding core features without discussion (keep it minimal!)
|
|
632
|
+
3. **Check:** Project specification for philosophy
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## License
|
|
637
|
+
|
|
638
|
+
MIT ยฉ Sathvik C
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Inspiration
|
|
643
|
+
|
|
644
|
+
Lume.js is inspired by:
|
|
645
|
+
- **Knockout.js** - The original `data-bind` approach
|
|
646
|
+
- **Alpine.js** - Minimal, HTML-first philosophy
|
|
647
|
+
- **Go** - Simplicity and explicit design
|
|
648
|
+
- **The Web Platform** - Standards over abstractions
|
|
50
649
|
|
|
51
|
-
|
|
650
|
+
**Built for developers who want reactivity without the framework tax.**
|