lume-js 0.2.1 โ†’ 0.3.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 CHANGED
@@ -1,51 +1,440 @@
1
- # lume-js
1
+ # Lume.js
2
2
 
3
- Minimal reactive state + DOM binding with **zero runtime** overhead.
4
- Inspired by Go-style simplicity.
3
+ **Reactivity that follows web standards.**
5
4
 
6
- ## Philosophy
7
- - โšก Zero runtime, direct DOM updates (no VDOM, no diffing).
8
- - ๐Ÿ”‘ Simple `state()` and `bindDom()` โ€” that's it.
9
- - ๐Ÿงฉ Explicit nesting (`state({ user: state({ name: "Alice" }) })`).
10
- - โœจ Works anywhere โ€” plain JS, or alongside other frameworks.
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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![Version](https://img.shields.io/badge/version-0.2.1-green.svg)](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.**
31
+
32
+ ---
33
+
34
+ ## Installation
11
35
 
12
- ## Install
36
+ ### Via npm
13
37
 
14
38
  ```bash
15
39
  npm install lume-js
16
40
  ```
17
41
 
18
- ## Example
42
+ ### Via CDN
43
+
44
+ ```html
45
+ <script type="module">
46
+ import { state, bindDom } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
47
+ </script>
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Quick Start
53
+
54
+ **HTML:**
55
+ ```html
56
+ <div>
57
+ <p>Count: <span data-bind="count"></span></p>
58
+ <input data-bind="name" placeholder="Enter name">
59
+ <p>Hello, <span data-bind="name"></span>!</p>
60
+
61
+ <button id="increment">+</button>
62
+ </div>
63
+ ```
19
64
 
20
- **main.js**
21
- ```js
22
- import { state, bindDom } from "lume-js";
65
+ **JavaScript:**
66
+ ```javascript
67
+ import { state, bindDom } from 'lume-js';
23
68
 
69
+ // Create reactive state
24
70
  const store = state({
25
71
  count: 0,
26
- user: state({ name: "Alice" })
72
+ name: 'World'
27
73
  });
28
74
 
29
- bindDom(document.body, store);
75
+ // Bind to DOM
76
+ const cleanup = bindDom(document.body, store);
30
77
 
31
- document.getElementById("inc").addEventListener("click", () => {
78
+ // Update state with standard JavaScript
79
+ document.getElementById('increment').addEventListener('click', () => {
32
80
  store.count++;
33
81
  });
34
82
 
35
- document.getElementById("changeName").addEventListener("click", () => {
36
- store.user.name = store.user.name === "Alice" ? "Bob" : "Alice";
83
+ // Cleanup when done (important!)
84
+ window.addEventListener('beforeunload', () => cleanup());
85
+ ```
86
+
87
+ That's it! No build step, no custom syntax, just HTML and JavaScript.
88
+
89
+ ---
90
+
91
+ ## Core API
92
+
93
+ ### `state(object)`
94
+
95
+ Creates a reactive state object using Proxy.
96
+
97
+ ```javascript
98
+ const store = state({
99
+ count: 0,
100
+ user: state({ // Nested state must be wrapped
101
+ name: 'Alice',
102
+ email: 'alice@example.com'
103
+ })
37
104
  });
105
+
106
+ // Update state
107
+ store.count++;
108
+ store.user.name = 'Bob';
109
+ ```
110
+
111
+ **Features:**
112
+ - โœ… Validates input (must be plain object)
113
+ - โœ… Only triggers updates when value actually changes
114
+ - โœ… Returns cleanup function from `$subscribe`
115
+
116
+ ### `bindDom(root, store)`
117
+
118
+ Binds reactive state to DOM elements with `data-bind` attributes.
119
+
120
+ ```javascript
121
+ const cleanup = bindDom(document.body, store);
122
+
123
+ // Later: cleanup all bindings
124
+ cleanup();
125
+ ```
126
+
127
+ **Supports:**
128
+ - โœ… Text content: `<span data-bind="count"></span>`
129
+ - โœ… Input values: `<input data-bind="name">`
130
+ - โœ… Textareas: `<textarea data-bind="bio"></textarea>`
131
+ - โœ… Selects: `<select data-bind="theme"></select>`
132
+ - โœ… Checkboxes: `<input type="checkbox" data-bind="enabled">`
133
+ - โœ… Numbers: `<input type="number" data-bind="age">`
134
+ - โœ… Radio buttons: `<input type="radio" data-bind="choice">`
135
+ - โœ… Nested paths: `<span data-bind="user.name"></span>`
136
+
137
+ **NEW in v0.3.0:**
138
+ - Returns cleanup function
139
+ - Better error messages with `[Lume.js]` prefix
140
+ - Handles edge cases (empty bindings, invalid paths)
141
+
142
+ ### `$subscribe(key, callback)`
143
+
144
+ Manually subscribe to state changes. Returns unsubscribe function.
145
+
146
+ ```javascript
147
+ const unsubscribe = store.$subscribe('count', (value) => {
148
+ console.log('Count changed:', value);
149
+
150
+ // Integrate with other libraries
151
+ if (value > 10) {
152
+ showNotification('Count is high!');
153
+ }
154
+ });
155
+
156
+ // Cleanup
157
+ unsubscribe();
158
+ ```
159
+
160
+ **NEW in v0.3.0:**
161
+ - Returns unsubscribe function (was missing in v0.2.x)
162
+ - Validates callback is a function
163
+ - Only notifies on actual value changes
164
+
165
+ ---
166
+
167
+ ## Examples
168
+
169
+ ### Basic Counter
170
+
171
+ ```javascript
172
+ const store = state({ count: 0 });
173
+ const cleanup = bindDom(document.body, store);
174
+
175
+ document.getElementById('inc').addEventListener('click', () => {
176
+ store.count++;
177
+ });
178
+
179
+ // Cleanup on unmount
180
+ window.addEventListener('beforeunload', () => cleanup());
38
181
  ```
39
182
 
40
- **index.html**
41
183
  ```html
42
184
  <p>Count: <span data-bind="count"></span></p>
43
- <p>User Name: <span data-bind="user.name"></span></p>
44
-
45
185
  <button id="inc">Increment</button>
46
- <button id="changeName">Change Name</button>
47
186
  ```
48
187
 
49
- ## Status
188
+ ### Form Handling with Validation
189
+
190
+ ```javascript
191
+ const form = state({
192
+ email: '',
193
+ age: 25,
194
+ theme: 'light',
195
+ errors: {}
196
+ });
197
+
198
+ const cleanup = bindDom(document.querySelector('form'), form);
199
+
200
+ // Validate on change
201
+ const unsubEmail = form.$subscribe('email', (value) => {
202
+ const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
203
+ form.errors = {
204
+ ...form.errors,
205
+ email: isValid ? '' : 'Invalid email'
206
+ };
207
+ });
208
+
209
+ // Cleanup
210
+ window.addEventListener('beforeunload', () => {
211
+ cleanup();
212
+ unsubEmail();
213
+ });
214
+ ```
215
+
216
+ ```html
217
+ <form>
218
+ <input type="email" data-bind="email">
219
+ <span data-bind="errors.email" style="color: red;"></span>
220
+
221
+ <input type="number" data-bind="age">
222
+
223
+ <select data-bind="theme">
224
+ <option value="light">Light</option>
225
+ <option value="dark">Dark</option>
226
+ </select>
227
+ </form>
228
+ ```
229
+
230
+ ### Nested State
231
+
232
+ ```javascript
233
+ const store = state({
234
+ user: state({
235
+ profile: state({
236
+ name: 'Alice',
237
+ bio: 'Developer'
238
+ }),
239
+ settings: state({
240
+ notifications: true
241
+ })
242
+ })
243
+ });
244
+
245
+ const cleanup = bindDom(document.body, store);
246
+
247
+ // Subscribe to nested changes
248
+ const unsub = store.user.profile.$subscribe('name', (name) => {
249
+ console.log('Profile name changed:', name);
250
+ });
251
+
252
+ // Cleanup
253
+ window.addEventListener('beforeunload', () => {
254
+ cleanup();
255
+ unsub();
256
+ });
257
+ ```
258
+
259
+ ```html
260
+ <input data-bind="user.profile.name">
261
+ <textarea data-bind="user.profile.bio"></textarea>
262
+ <input type="checkbox" data-bind="user.settings.notifications">
263
+ ```
264
+
265
+ ### Integration with GSAP
266
+
267
+ ```javascript
268
+ import gsap from 'gsap';
269
+ import { state } from 'lume-js';
270
+
271
+ const ui = state({ x: 0, y: 0 });
272
+
273
+ const unsubX = ui.$subscribe('x', (value) => {
274
+ gsap.to('.box', { x: value, duration: 0.5 });
275
+ });
276
+
277
+ // Now ui.x = 100 triggers smooth animation
278
+
279
+ // Cleanup
280
+ window.addEventListener('beforeunload', () => unsubX());
281
+ ```
282
+
283
+ ### Cleanup Pattern (Important!)
284
+
285
+ ```javascript
286
+ const store = state({ data: [] });
287
+ const cleanup = bindDom(root, store);
288
+
289
+ const unsub1 = store.$subscribe('data', handleData);
290
+ const unsub2 = store.$subscribe('status', handleStatus);
291
+
292
+ // Cleanup when component unmounts
293
+ function destroy() {
294
+ cleanup(); // Remove DOM bindings
295
+ unsub1(); // Remove subscription 1
296
+ unsub2(); // Remove subscription 2
297
+ }
298
+
299
+ // For SPA frameworks
300
+ onUnmount(destroy);
301
+
302
+ // For vanilla JS
303
+ window.addEventListener('beforeunload', destroy);
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Philosophy
309
+
310
+ ### Standards-Only
311
+ - Uses only `data-*` attributes (HTML5 standard)
312
+ - Uses only standard JavaScript APIs (Proxy, addEventListener)
313
+ - No custom syntax that breaks validators
314
+ - Works with any tool/library
315
+
316
+ ### No Artificial Limitations
317
+ ```javascript
318
+ // โœ… Use with jQuery
319
+ $('.modal').fadeIn();
320
+ store.modalOpen = true;
321
+
322
+ // โœ… Use with GSAP
323
+ gsap.to(el, { x: store.position });
324
+
325
+ // โœ… Use with any router
326
+ router.on('/home', () => store.route = 'home');
327
+
328
+ // โœ… Mix with vanilla JS
329
+ document.addEventListener('click', () => store.clicks++);
330
+ ```
331
+
332
+ **Lume.js doesn't hijack your architecture - it enhances it.**
333
+
334
+ ### Progressive Enhancement
335
+
336
+ ```html
337
+ <!-- Works without JavaScript (server-rendered) -->
338
+ <form action="/submit" method="POST">
339
+ <input name="email" value="alice@example.com">
340
+ <button type="submit">Save</button>
341
+ </form>
342
+
343
+ <script type="module">
344
+ // Enhanced when JS loads
345
+ import { state, bindDom } from 'lume-js';
346
+
347
+ const form = state({ email: 'alice@example.com' });
348
+ const cleanup = bindDom(document.querySelector('form'), form);
349
+
350
+ // Prevent default, use AJAX
351
+ document.querySelector('form').addEventListener('submit', async (e) => {
352
+ e.preventDefault();
353
+ await fetch('/submit', {
354
+ method: 'POST',
355
+ body: JSON.stringify({ email: form.email })
356
+ });
357
+ });
358
+
359
+ window.addEventListener('beforeunload', () => cleanup());
360
+ </script>
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Who Should Use Lume.js?
366
+
367
+ ### โœ… Perfect For:
368
+ - WordPress/Shopify theme developers
369
+ - Accessibility-focused teams (government, healthcare, education)
370
+ - Legacy codebases that can't do full rewrites
371
+ - Developers who hate learning framework-specific syntax
372
+ - Progressive enhancement advocates
373
+ - Projects requiring HTML validation
374
+ - Adding reactivity to server-rendered apps
375
+
376
+ ### โš ๏ธ Consider Alternatives:
377
+ - **Complex SPAs** โ†’ Use React, Vue, or Svelte
378
+ - **Need routing/SSR** โ†’ Use Next.js, Nuxt, or SvelteKit
379
+ - **Prefer terse syntax** โ†’ Use Alpine.js (if custom syntax is okay)
380
+
381
+ ---
382
+
383
+ ## What's New in v0.3.0?
384
+
385
+ ### Breaking Changes
386
+ - โœ… `subscribe` โ†’ `$subscribe` (restored from v0.1.0)
387
+ - โœ… `$subscribe` now returns unsubscribe function
388
+
389
+ ### New Features
390
+ - โœ… TypeScript definitions (`index.d.ts`)
391
+ - โœ… `bindDom()` returns cleanup function
392
+ - โœ… Better error handling with `[Lume.js]` prefix
393
+ - โœ… Input validation (only plain objects)
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
402
+
403
+ ---
404
+
405
+ ## Browser Support
406
+
407
+ - Chrome/Edge 49+
408
+ - Firefox 18+
409
+ - Safari 10+
410
+ - No IE11 (Proxy can't be polyfilled)
411
+
412
+ **Basically: Modern browsers with ES6 Proxy support.**
413
+
414
+ ---
415
+
416
+ ## Contributing
417
+
418
+ We welcome contributions! Please:
419
+
420
+ 1. **Focus on:** Examples, documentation, bug fixes, performance
421
+ 2. **Avoid:** Adding core features without discussion (keep it minimal!)
422
+ 3. **Check:** Project specification for philosophy
423
+
424
+ ---
425
+
426
+ ## License
427
+
428
+ MIT ยฉ Sathvik C
429
+
430
+ ---
431
+
432
+ ## Inspiration
433
+
434
+ Lume.js is inspired by:
435
+ - **Knockout.js** - The original `data-bind` approach
436
+ - **Alpine.js** - Minimal, HTML-first philosophy
437
+ - **Go** - Simplicity and explicit design
438
+ - **The Web Platform** - Standards over abstractions
50
439
 
51
- ๐Ÿšง Early alpha (`0.1.0`). API may change. Feedback welcome!
440
+ **Built for developers who want reactivity without the framework tax.**
package/package.json CHANGED
@@ -1,16 +1,53 @@
1
1
  {
2
2
  "name": "lume-js",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
+ "description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
4
5
  "main": "src/index.js",
6
+ "types": "src/index.d.ts",
5
7
  "type": "module",
6
8
  "scripts": {
7
9
  "dev": "vite",
8
- "build": "echo 'No build step yet, zero-runtime already!'"
10
+ "build": "echo 'No build step needed - zero-runtime library!'",
11
+ "preview": "vite preview"
9
12
  },
10
13
  "files": [
11
- "src"
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
12
17
  ],
18
+ "keywords": [
19
+ "reactive",
20
+ "state",
21
+ "dom",
22
+ "binding",
23
+ "standards",
24
+ "minimal",
25
+ "no-build",
26
+ "vanilla-js",
27
+ "data-bind",
28
+ "proxy",
29
+ "knockout",
30
+ "html-first",
31
+ "framework-agnostic",
32
+ "minimal-runtime",
33
+ "no-vdom",
34
+ "web-standards",
35
+ "lightweight"
36
+ ],
37
+ "author": "Sathvik C",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/sathvikc/lume-js.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/sathvikc/lume-js/issues"
45
+ },
46
+ "homepage": "https://github.com/sathvikc/lume-js#readme",
13
47
  "devDependencies": {
14
48
  "vite": "^7.1.9"
49
+ },
50
+ "engines": {
51
+ "node": ">=20.19.0"
15
52
  }
16
53
  }
@@ -1,7 +1,21 @@
1
1
  /**
2
2
  * computed - creates a derived value based on state
3
+ *
4
+ * NOTE: This is a basic implementation. For production use,
5
+ * consider more robust solutions with automatic dependency tracking.
6
+ *
3
7
  * @param {Function} fn - function that computes value from state
4
- * @returns {Object} - { get: () => value, recompute: () => void, subscribe: (cb) => unsubscribe }
8
+ * @returns {Object} - { value, recompute, subscribe }
9
+ *
10
+ * @example
11
+ * const store = state({ count: 0 });
12
+ * const doubled = computed(() => store.count * 2);
13
+ *
14
+ * // Subscribe to changes
15
+ * doubled.subscribe(val => console.log('Doubled:', val));
16
+ *
17
+ * // Manually trigger recomputation after state changes
18
+ * store.$subscribe('count', () => doubled.recompute());
5
19
  */
6
20
  export function computed(fn) {
7
21
  let value;
@@ -20,7 +34,7 @@ export function computed(fn) {
20
34
  };
21
35
 
22
36
  return {
23
- get: () => {
37
+ get value() {
24
38
  if (dirty) recalc();
25
39
  return value;
26
40
  },
@@ -32,7 +46,8 @@ export function computed(fn) {
32
46
  subscribers.add(cb);
33
47
  // Immediately notify subscriber with current value
34
48
  if (!dirty) cb(value);
49
+ else recalc(); // Compute first time
35
50
  return () => subscribers.delete(cb); // unsubscribe function
36
51
  },
37
52
  };
38
- }
53
+ }
@@ -3,10 +3,11 @@
3
3
  * @param {Object} store - reactive store created with state()
4
4
  * @param {string} key - key in store to watch
5
5
  * @param {Function} callback - called with new value
6
+ * @returns {Function} unsubscribe function
6
7
  */
7
8
  export function watch(store, key, callback) {
8
- if (!store.subscribe) {
9
+ if (!store.$subscribe) {
9
10
  throw new Error("store must be created with state()");
10
11
  }
11
- store.subscribe(key, callback);
12
- }
12
+ return store.$subscribe(key, callback);
13
+ }
@@ -1,50 +1,128 @@
1
+ // src/core/bindDom.js
1
2
  /**
2
- * Lume-JS Zero-runtime DOM binding
3
+ * Lume-JS DOM Binding
3
4
  *
4
5
  * Binds reactive state to DOM elements using [data-bind].
5
- * Supports two-way binding for INPUT/TEXTAREA.
6
+ * Supports two-way binding for INPUT/TEXTAREA/SELECT.
6
7
  *
7
8
  * Usage:
8
9
  * import { bindDom } from "lume-js";
9
- * bindDom(document.body, store);
10
+ * const cleanup = bindDom(document.body, store);
11
+ * // Later: cleanup();
10
12
  *
11
13
  * HTML:
12
14
  * <span data-bind="count"></span>
13
15
  * <input data-bind="user.name">
16
+ * <select data-bind="theme"></select>
14
17
  */
15
18
 
16
19
  import { resolvePath } from "./utils.js";
17
20
 
18
21
  /**
19
- * Zero-runtime DOM binding for a reactive store
22
+ * DOM binding for reactive state
20
23
  *
21
- * @param {HTMLElement} root - root element to scan for [data-bind]
22
- * @param {object} store - reactive state object
24
+ * @param {HTMLElement} root - Root element to scan for [data-bind]
25
+ * @param {object} store - Reactive state object
26
+ * @returns {function} Cleanup function to remove all bindings
23
27
  */
24
28
  export function bindDom(root, store) {
29
+ if (!(root instanceof HTMLElement)) {
30
+ throw new Error('bindDom() requires a valid HTMLElement as root');
31
+ }
32
+
33
+ if (!store || typeof store !== 'object') {
34
+ throw new Error('bindDom() requires a reactive state object');
35
+ }
36
+
25
37
  const nodes = root.querySelectorAll("[data-bind]");
38
+ const unsubscribers = [];
26
39
 
27
40
  nodes.forEach(el => {
28
- const pathArr = el.getAttribute("data-bind").split(".");
41
+ const bindPath = el.getAttribute("data-bind");
42
+
43
+ if (!bindPath) {
44
+ console.warn('[Lume.js] Empty data-bind attribute found', el);
45
+ return;
46
+ }
47
+
48
+ const pathArr = bindPath.split(".");
29
49
  const lastKey = pathArr.pop();
30
50
 
31
51
  let target;
32
52
  try {
33
- target = resolvePath(store, pathArr); // must be wrapped with state() if nested
53
+ target = resolvePath(store, pathArr);
34
54
  } catch (err) {
35
- console.warn(`Skipping binding for ${el}: ${err.message}`);
55
+ console.warn(`[Lume.js] Invalid binding path "${bindPath}":`, err.message);
36
56
  return;
37
57
  }
38
58
 
39
- // Subscribe once
40
- target.subscribe(lastKey, val => {
41
- if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") el.value = val;
42
- else el.textContent = val;
59
+ if (!target || typeof target.$subscribe !== 'function') {
60
+ console.warn(`[Lume.js] Target for "${bindPath}" is not a reactive state object`);
61
+ return;
62
+ }
63
+
64
+ // Subscribe to changes
65
+ const unsub = target.$subscribe(lastKey, val => {
66
+ updateElement(el, val);
43
67
  });
44
68
 
45
- // 2-way binding for inputs
46
- if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
47
- el.addEventListener("input", e => target[lastKey] = e.target.value);
69
+ unsubscribers.push(unsub);
70
+
71
+ // Two-way binding for form inputs
72
+ if (isFormInput(el)) {
73
+ el.addEventListener("input", e => {
74
+ target[lastKey] = getInputValue(e.target);
75
+ });
48
76
  }
49
77
  });
78
+
79
+ // Return cleanup function
80
+ return () => {
81
+ unsubscribers.forEach(unsub => unsub());
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Update DOM element with new value
87
+ * @private
88
+ */
89
+ function updateElement(el, val) {
90
+ if (el.tagName === "INPUT") {
91
+ if (el.type === "checkbox") {
92
+ el.checked = Boolean(val);
93
+ } else if (el.type === "radio") {
94
+ el.checked = el.value === String(val);
95
+ } else {
96
+ el.value = val ?? '';
97
+ }
98
+ } else if (el.tagName === "TEXTAREA") {
99
+ el.value = val ?? '';
100
+ } else if (el.tagName === "SELECT") {
101
+ el.value = val ?? '';
102
+ } else {
103
+ el.textContent = val ?? '';
104
+ }
50
105
  }
106
+
107
+ /**
108
+ * Get value from form input
109
+ * @private
110
+ */
111
+ function getInputValue(el) {
112
+ if (el.type === "checkbox") {
113
+ return el.checked;
114
+ } else if (el.type === "number" || el.type === "range") {
115
+ return el.valueAsNumber;
116
+ }
117
+ return el.value;
118
+ }
119
+
120
+ /**
121
+ * Check if element is a form input
122
+ * @private
123
+ */
124
+ function isFormInput(el) {
125
+ return el.tagName === "INPUT" ||
126
+ el.tagName === "TEXTAREA" ||
127
+ el.tagName === "SELECT";
128
+ }
package/src/core/state.js CHANGED
@@ -1,32 +1,41 @@
1
+ // src/core/state.js
1
2
  /**
2
3
  * Lume-JS Reactive State Core
3
4
  *
4
- * Provides minimal, zero-runtime reactive state.
5
+ * Provides minimal reactive state with standard JavaScript.
5
6
  *
6
7
  * Features:
7
8
  * - Lightweight and Go-style
8
9
  * - Explicit nested states
9
- * - subscribe for listening to key changes
10
+ * - $subscribe for listening to key changes
11
+ * - Cleanup with unsubscribe
10
12
  *
11
13
  * Usage:
12
14
  * import { state } from "lume-js";
13
15
  * const store = state({ count: 0 });
14
- * store.subscribe("count", val => console.log(val));
16
+ * const unsub = store.$subscribe("count", val => console.log(val));
17
+ * unsub(); // cleanup
15
18
  */
16
19
 
17
-
18
20
  /**
19
21
  * Creates a reactive state object.
20
22
  *
21
23
  * @param {Object} obj - Initial state object
22
- * @returns {Proxy} Reactive proxy with subscribe method
24
+ * @returns {Proxy} Reactive proxy with $subscribe method
23
25
  */
24
26
  export function state(obj) {
27
+ // Validate input
28
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
29
+ throw new Error('state() requires a plain object');
30
+ }
31
+
25
32
  const listeners = {};
26
33
 
27
34
  // Notify subscribers of a key
28
35
  function notify(key, val) {
29
- if (listeners[key]) listeners[key].forEach(fn => fn(val));
36
+ if (listeners[key]) {
37
+ listeners[key].forEach(fn => fn(val));
38
+ }
30
39
  }
31
40
 
32
41
  const proxy = new Proxy(obj, {
@@ -34,8 +43,13 @@ export function state(obj) {
34
43
  return target[key];
35
44
  },
36
45
  set(target, key, value) {
46
+ const oldValue = target[key];
37
47
  target[key] = value;
38
- notify(key, value);
48
+
49
+ // Only notify if value actually changed
50
+ if (oldValue !== value) {
51
+ notify(key, value);
52
+ }
39
53
  return true;
40
54
  }
41
55
  });
@@ -43,15 +57,30 @@ export function state(obj) {
43
57
  /**
44
58
  * Subscribe to changes for a specific key.
45
59
  * Calls the callback immediately with the current value.
60
+ * Returns an unsubscribe function for cleanup.
46
61
  *
47
- * @param {string} key
48
- * @param {function} fn
62
+ * @param {string} key - Property key to watch
63
+ * @param {function} fn - Callback function
64
+ * @returns {function} Unsubscribe function
49
65
  */
50
- proxy.subscribe = (key, fn) => {
66
+ proxy.$subscribe = (key, fn) => {
67
+ if (typeof fn !== 'function') {
68
+ throw new Error('Subscriber must be a function');
69
+ }
70
+
51
71
  if (!listeners[key]) listeners[key] = [];
52
72
  listeners[key].push(fn);
53
- fn(proxy[key]); // initialize
73
+
74
+ // Call immediately with current value
75
+ fn(proxy[key]);
76
+
77
+ // Return unsubscribe function
78
+ return () => {
79
+ if (listeners[key]) {
80
+ listeners[key] = listeners[key].filter(subscriber => subscriber !== fn);
81
+ }
82
+ };
54
83
  };
55
84
 
56
85
  return proxy;
57
- }
86
+ }
package/src/core/utils.js CHANGED
@@ -1,14 +1,41 @@
1
+ /**
2
+ * Utility functions for Lume.js
3
+ */
4
+
1
5
  /**
2
6
  * Resolve a nested path in an object.
3
- * Example: path "user.name" returns obj.user.name
7
+ * Example: resolvePath(obj, ['user', 'address']) returns obj.user.address
4
8
  *
5
9
  * @param {object} obj - The root object
6
10
  * @param {string[]} pathArr - Array of keys
7
11
  * @returns {object} Last object in the path
12
+ * @throws {Error} If path is invalid or doesn't exist
8
13
  */
9
14
  export function resolvePath(obj, pathArr) {
10
- return pathArr.reduce((acc, key) => {
11
- if (!acc) throw new Error(`Invalid path: ${pathArr.join(".")}`);
12
- return acc[key];
13
- }, obj);
14
- }
15
+ // If no path, return the object itself
16
+ if (!pathArr || pathArr.length === 0) {
17
+ return obj;
18
+ }
19
+
20
+ let current = obj;
21
+
22
+ for (let i = 0; i < pathArr.length; i++) {
23
+ const key = pathArr[i];
24
+
25
+ if (current === null || current === undefined) {
26
+ throw new Error(
27
+ `Cannot access property "${key}" of ${current} at path: ${pathArr.slice(0, i + 1).join('.')}`
28
+ );
29
+ }
30
+
31
+ if (!(key in current)) {
32
+ throw new Error(
33
+ `Property "${key}" does not exist at path: ${pathArr.slice(0, i + 1).join('.')}`
34
+ );
35
+ }
36
+
37
+ current = current[key];
38
+ }
39
+
40
+ return current;
41
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Lume.js TypeScript Definitions
3
+ *
4
+ * Provides type safety for reactive state management
5
+ */
6
+
7
+ /**
8
+ * Unsubscribe function returned by $subscribe
9
+ */
10
+ export type Unsubscribe = () => void;
11
+
12
+ /**
13
+ * Subscriber callback function
14
+ */
15
+ export type Subscriber<T> = (value: T) => void;
16
+
17
+ /**
18
+ * Reactive state object with $subscribe method
19
+ */
20
+ export type ReactiveState<T extends object> = T & {
21
+ /**
22
+ * Subscribe to changes on a specific property key
23
+ * @param key - Property key to watch
24
+ * @param callback - Function called when property changes
25
+ * @returns Unsubscribe function for cleanup
26
+ */
27
+ $subscribe<K extends keyof T>(
28
+ key: K,
29
+ callback: Subscriber<T[K]>
30
+ ): Unsubscribe;
31
+ };
32
+
33
+ /**
34
+ * Create a reactive state object
35
+ *
36
+ * @param obj - Plain object to make reactive
37
+ * @returns Reactive proxy with $subscribe method
38
+ * @throws {Error} If obj is not a plain object
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const store = state({
43
+ * count: 0,
44
+ * user: state({
45
+ * name: 'Alice'
46
+ * })
47
+ * });
48
+ *
49
+ * store.count++; // Triggers reactivity
50
+ *
51
+ * const unsub = store.$subscribe('count', (val) => {
52
+ * console.log('Count:', val);
53
+ * });
54
+ *
55
+ * // Cleanup
56
+ * unsub();
57
+ * ```
58
+ */
59
+ export function state<T extends object>(obj: T): ReactiveState<T>;
60
+
61
+ /**
62
+ * Bind reactive state to DOM elements
63
+ *
64
+ * @param root - Root element to scan for [data-bind] attributes
65
+ * @param store - Reactive state object
66
+ * @returns Cleanup function to remove all bindings
67
+ * @throws {Error} If root is not an HTMLElement
68
+ * @throws {Error} If store is not a reactive state object
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const store = state({ count: 0 });
73
+ * const cleanup = bindDom(document.body, store);
74
+ *
75
+ * // Later: cleanup all bindings
76
+ * cleanup();
77
+ * ```
78
+ *
79
+ * HTML:
80
+ * ```html
81
+ * <span data-bind="count"></span>
82
+ * <input data-bind="name">
83
+ * ```
84
+ */
85
+ export function bindDom(
86
+ root: HTMLElement,
87
+ store: ReactiveState<any>
88
+ ): Unsubscribe;