resonantjs 1.1.8 → 1.2.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 +172 -512
- package/aiagent.md +207 -165
- package/package.json +1 -1
- package/resonant.js +63 -48
- package/resonant.min.js +1 -1
- package/resonant.min.js.map +1 -1
- package/scripts/build.js +8 -1
- package/test/additional_features.test.js +2 -2
- package/test/array_methods.test.js +401 -0
- package/test/complex_scenarios.test.js +413 -0
- package/test/deep_nesting.test.js +2 -2
- package/test/display_selective_rendering.test.js +172 -0
- package/test/edge_cases.test.js +627 -0
- package/test/error_handling.test.js +304 -0
- package/test/existing_variable_binding.test.js +151 -0
- package/test/html_examples.test.js +1 -1
- package/test/input_binding_advanced.test.js +323 -0
- package/test/mockDom.js +5 -4
- package/test/resonant.test.js +1 -1
- package/test/selective_rendering.test.js +1 -1
- package/test/style_and_events.test.js +1 -1
- package/test/template.test.js +1 -1
- package/Demo.gif +0 -0
package/README.md
CHANGED
|
@@ -3,146 +3,68 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/resonantjs)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**Reactive data-binding for vanilla JavaScript. No build step. No virtual DOM. Just HTML attributes and plain objects.**
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
```html
|
|
9
|
+
<span res="count"></span>
|
|
10
|
+
<button onclick="count++">+1</button>
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
<script src="https://unpkg.com/resonantjs@latest/resonant.min.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
const res = new Resonant();
|
|
15
|
+
res.add('count', 0);
|
|
16
|
+
</script>
|
|
17
|
+
```
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
- **Attribute-Based**: Simple HTML attributes create powerful data bindings
|
|
14
|
-
- **Deep Object Support**: Full reactivity for nested objects and arrays
|
|
15
|
-
- **Built-in Persistence**: Automatic localStorage integration for data persistence
|
|
16
|
-
- **Dynamic Styling**: Conditional CSS classes and styles based on your data
|
|
17
|
-
- **Performance**: Efficient updates with minimal overhead
|
|
18
|
-
- **Tiny Footprint**: Under 17KB minified, perfect for any project size
|
|
19
|
+
Change `count` anywhere in your code and the DOM updates automatically.
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
### Installation
|
|
23
|
+
## Install
|
|
25
24
|
|
|
26
|
-
#### NPM
|
|
27
25
|
```bash
|
|
28
26
|
npm install resonantjs
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
```html
|
|
33
|
-
<script src="https://unpkg.com/resonantjs@latest/resonant.js"></script>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Hello World Example
|
|
29
|
+
Or drop in a script tag:
|
|
37
30
|
|
|
38
31
|
```html
|
|
39
|
-
|
|
40
|
-
<html>
|
|
41
|
-
<head>
|
|
42
|
-
<title>ResonantJs Demo</title>
|
|
43
|
-
<script src="https://unpkg.com/resonantjs@latest/resonant.js"></script>
|
|
44
|
-
</head>
|
|
45
|
-
<body>
|
|
46
|
-
<h1>Counter: <span res="counter"></span></h1>
|
|
47
|
-
<button onclick="counter++">Increment</button>
|
|
48
|
-
<button onclick="counter--">Decrement</button>
|
|
49
|
-
|
|
50
|
-
<script>
|
|
51
|
-
const resonant = new Resonant();
|
|
52
|
-
resonant.add("counter", 0, true); // value, localStorage persistence
|
|
53
|
-
</script>
|
|
54
|
-
</body>
|
|
55
|
-
</html>
|
|
32
|
+
<script src="https://unpkg.com/resonantjs@latest/resonant.min.js"></script>
|
|
56
33
|
```
|
|
57
34
|
|
|
58
|
-
|
|
35
|
+
~18 KB minified. Zero dependencies.
|
|
59
36
|
|
|
60
37
|
---
|
|
61
38
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
Copy and paste the snippet below into an `.html` file and open it in your browser.
|
|
39
|
+
## Why ResonantJs?
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</style>
|
|
75
|
-
<script src="https://unpkg.com/resonantjs@latest/resonant.js"></script>
|
|
76
|
-
</head>
|
|
77
|
-
<body>
|
|
78
|
-
<h1>Todos (<span res="tasks.length"></span>)</h1>
|
|
79
|
-
|
|
80
|
-
<input placeholder="Add a task..." res="newTask" />
|
|
81
|
-
<button onclick="addTask()">Add</button>
|
|
82
|
-
|
|
83
|
-
<ul>
|
|
84
|
-
<li res="tasks" res-display="newTask === '' || name.toLowerCase().includes(newTask.toLowerCase())">
|
|
85
|
-
<input type="checkbox" res-prop="completed" />
|
|
86
|
-
<span res-prop="name" res-style="completed ? 'completed' : ''"></span>
|
|
87
|
-
<button res-onclick="removeTask">Remove</button>
|
|
88
|
-
</li>
|
|
89
|
-
</ul>
|
|
90
|
-
|
|
91
|
-
<script>
|
|
92
|
-
const resonant = new Resonant();
|
|
93
|
-
resonant.addAll({
|
|
94
|
-
newTask: '',
|
|
95
|
-
tasks: [
|
|
96
|
-
{ name: 'Learn ResonantJs', completed: false },
|
|
97
|
-
{ name: 'Ship a feature', completed: true }
|
|
98
|
-
]
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
function addTask() {
|
|
102
|
-
const title = newTask.trim();
|
|
103
|
-
if (!title) return;
|
|
104
|
-
tasks.unshift({ name: title, completed: false });
|
|
105
|
-
newTask = '';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function removeTask(item) {
|
|
109
|
-
const idx = tasks.indexOf(item);
|
|
110
|
-
if (idx !== -1) tasks.delete(idx);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Optional: observe changes
|
|
114
|
-
resonant.addCallback('tasks', (list, item, action) => {
|
|
115
|
-
console.log('[tasks]', action, item);
|
|
116
|
-
});
|
|
117
|
-
</script>
|
|
118
|
-
</body>
|
|
119
|
-
</html>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Key Takeaways
|
|
123
|
-
|
|
124
|
-
- Use `res="tasks"` on a template element inside a list container to auto-render each item
|
|
125
|
-
- Use `res-prop` inside that template to bind fields of the current item
|
|
126
|
-
- Use `res-display` for inline filtering/conditional rendering; inside lists, bare props like `completed` refer to the current item
|
|
127
|
-
- `res-style` returns a space-separated class string
|
|
128
|
-
- Event handlers referenced by `res-onclick` are global functions and receive `item` when declared with a parameter
|
|
41
|
+
| | |
|
|
42
|
+
|---|---|
|
|
43
|
+
| **No build tools** | Works with a single `<script>` tag. Ship today. |
|
|
44
|
+
| **Familiar mental model** | Plain objects, plain arrays, plain HTML. No JSX, no templates, no compilation. |
|
|
45
|
+
| **Automatic DOM updates** | Change a value, the page updates. Arrays, nested objects, computed properties -- all reactive. |
|
|
46
|
+
| **Selective re-rendering** | Only the changed array item re-renders. Siblings stay untouched. |
|
|
47
|
+
| **Built-in persistence** | One flag to sync any variable to `localStorage`. |
|
|
48
|
+
| **Tiny footprint** | ~18 KB minified, zero dependencies. |
|
|
129
49
|
|
|
130
50
|
---
|
|
131
51
|
|
|
132
|
-
##
|
|
133
|
-
|
|
134
|
-
### Data Binding (`res`)
|
|
52
|
+
## Quick Tour
|
|
135
53
|
|
|
136
|
-
Bind
|
|
54
|
+
### Bind a variable
|
|
137
55
|
|
|
138
56
|
```html
|
|
139
|
-
<span res="
|
|
140
|
-
<div res="user.profile.name"></div> <!-- Nested object property -->
|
|
57
|
+
<h1>Hello, <span res="name"></span></h1>
|
|
141
58
|
```
|
|
142
59
|
|
|
143
|
-
|
|
60
|
+
```js
|
|
61
|
+
const res = new Resonant();
|
|
62
|
+
res.add('name', 'World');
|
|
63
|
+
|
|
64
|
+
name = 'ResonantJs'; // DOM updates instantly
|
|
65
|
+
```
|
|
144
66
|
|
|
145
|
-
Bind
|
|
67
|
+
### Bind an object
|
|
146
68
|
|
|
147
69
|
```html
|
|
148
70
|
<div res="user">
|
|
@@ -151,508 +73,246 @@ Bind to specific properties within objects:
|
|
|
151
73
|
</div>
|
|
152
74
|
```
|
|
153
75
|
|
|
154
|
-
|
|
76
|
+
```js
|
|
77
|
+
res.add('user', { name: 'Alice', email: 'alice@example.com' });
|
|
155
78
|
|
|
156
|
-
|
|
79
|
+
user.name = 'Bob'; // only the name span updates
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Render an array
|
|
83
|
+
|
|
84
|
+
Place `res` on a template element inside a list container. ResonantJs clones it once per item.
|
|
157
85
|
|
|
158
86
|
```html
|
|
159
87
|
<ul>
|
|
160
|
-
<li res="
|
|
88
|
+
<li res="tasks">
|
|
161
89
|
<span res-prop="title"></span>
|
|
162
|
-
<button res-onclick="
|
|
90
|
+
<button res-onclick-remove="id">x</button>
|
|
163
91
|
</li>
|
|
164
92
|
</ul>
|
|
165
93
|
```
|
|
166
94
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
95
|
+
```js
|
|
96
|
+
res.add('tasks', [
|
|
97
|
+
{ id: 1, title: 'Learn ResonantJs' },
|
|
98
|
+
{ id: 2, title: 'Ship a feature' }
|
|
99
|
+
]);
|
|
170
100
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div res-display="tasks.length > 0">You have tasks</div>
|
|
174
|
-
<div res-display="user.role === 'admin'">Admin Panel</div>
|
|
101
|
+
tasks.push({ id: 3, title: 'Profit' }); // new <li> appears
|
|
102
|
+
tasks[0].title = 'Done!'; // only that <li> re-renders
|
|
175
103
|
```
|
|
176
104
|
|
|
177
|
-
###
|
|
178
|
-
|
|
179
|
-
Apply conditional CSS classes and styles:
|
|
105
|
+
### Conditional display
|
|
180
106
|
|
|
181
107
|
```html
|
|
182
|
-
<div res-
|
|
183
|
-
<
|
|
108
|
+
<div res-display="user.isAdmin">Admin Panel</div>
|
|
109
|
+
<div res-display="tasks.length === 0">No tasks yet.</div>
|
|
184
110
|
```
|
|
185
111
|
|
|
186
|
-
###
|
|
187
|
-
|
|
188
|
-
Bind click events with context:
|
|
112
|
+
### Dynamic classes
|
|
189
113
|
|
|
190
114
|
```html
|
|
191
|
-
<
|
|
192
|
-
<button res-onclick-remove="true">Delete Item</button>
|
|
115
|
+
<span res-prop="title" res-style="done ? 'completed' : ''"></span>
|
|
193
116
|
```
|
|
194
117
|
|
|
195
|
-
### Computed
|
|
118
|
+
### Computed properties
|
|
196
119
|
|
|
197
|
-
|
|
120
|
+
Derived values that recalculate automatically when dependencies change. Chains work too.
|
|
198
121
|
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
resonant.add('lastName', 'Doe');
|
|
122
|
+
```js
|
|
123
|
+
res.add('price', 100);
|
|
124
|
+
res.add('taxRate', 0.08);
|
|
203
125
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return firstName + ' ' + lastName;
|
|
207
|
-
});
|
|
126
|
+
res.computed('tax', () => price * taxRate);
|
|
127
|
+
res.computed('total', () => price + tax); // chains: updates when tax updates
|
|
208
128
|
```
|
|
209
129
|
|
|
210
130
|
```html
|
|
211
|
-
|
|
131
|
+
Total: $<span res="total"></span>
|
|
212
132
|
```
|
|
213
133
|
|
|
214
|
-
###
|
|
215
|
-
|
|
216
|
-
Two-way data binding for form elements:
|
|
134
|
+
### Two-way input binding
|
|
217
135
|
|
|
218
136
|
```html
|
|
219
|
-
<input type="text" res="
|
|
137
|
+
<input type="text" res="name" />
|
|
220
138
|
<input type="checkbox" res="settings.darkMode" />
|
|
221
|
-
<select res="
|
|
222
|
-
<option value="us">United States</option>
|
|
223
|
-
<option value="uk">United Kingdom</option>
|
|
224
|
-
</select>
|
|
139
|
+
<select res="country">...</select>
|
|
225
140
|
```
|
|
226
141
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
## Best Practices and Tips
|
|
230
|
-
|
|
231
|
-
- **Name your state clearly**: Variables you `add` become globals on `window` (e.g., `tasks`, `user`). Avoid collisions with existing globals.
|
|
232
|
-
- **List templates**: In `res="items"` templates, you can reference current item fields directly (`completed`, `name`) or as `item.completed` — both work.
|
|
233
|
-
- **Array updates**: Prefer `items.set(i, value)` over direct index assignment for clarity; both are reactive.
|
|
234
|
-
- **Batch updates**: When replacing a whole list, use `items.update(newArray)` to emit a single coherent update.
|
|
235
|
-
- **Object binding**: Use `res-prop=""` to bind an entire object to a single element when you just want to print it.
|
|
236
|
-
- **Event handlers**: `res-onclick` handlers are looked up on `window`. If your handler accepts an argument, Resonant passes the current `item`.
|
|
237
|
-
- **Quick removal**: For quick removal buttons, use `res-onclick-remove="idProp"` to delete by a unique key on each item.
|
|
238
|
-
- **Computed properties**: Track dependencies automatically. Use plain variable names inside the function (e.g., `firstName`, `lastName`). They are read-only.
|
|
239
|
-
- **Conditional expressions**: Keep display and style expressions simple and fast.
|
|
240
|
-
|
|
241
|
-
### Performance Notes
|
|
242
|
-
|
|
243
|
-
- Resonant selectively re-renders only changed array items by tracking indices and stable object keys
|
|
244
|
-
- Deeply nested objects and arrays are proxied; nested edits still update only affected DOM segments
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## API & Attribute Reference
|
|
249
|
-
|
|
250
|
-
### HTML Attributes
|
|
251
|
-
|
|
252
|
-
- `res` — bind a variable or array/template root
|
|
253
|
-
- `res-prop` — bind an object property within a `res` context; empty value binds the whole item
|
|
254
|
-
- `res-display` — boolean expression to show/hide element
|
|
255
|
-
- `res-style` — expression returning a space-separated class string
|
|
256
|
-
- `res-onclick` — call a global function; if it declares a parameter, it receives the current item
|
|
257
|
-
- `res-onclick-remove` — remove from the parent array by matching the given property (e.g., `id`)
|
|
258
|
-
|
|
259
|
-
### JavaScript API
|
|
260
|
-
|
|
261
|
-
- `const resonant = new Resonant()`
|
|
262
|
-
- `resonant.add(name, value, persist?)`
|
|
263
|
-
- `resonant.addAll(objectMap)`
|
|
264
|
-
- `resonant.addCallback(name, (newValue, item, action) => void)`
|
|
265
|
-
- `resonant.computed(name, () => value)`
|
|
266
|
-
|
|
267
|
-
### Array Helpers
|
|
268
|
-
|
|
269
|
-
Reactive arrays include these methods:
|
|
142
|
+
### Persistence
|
|
270
143
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
### Callback Actions
|
|
275
|
-
|
|
276
|
-
- Scalars: `modified`
|
|
277
|
-
- Arrays: `added`, `removed`, `modified`, `updated`, `filtered`
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## Key Features
|
|
282
|
-
|
|
283
|
-
### Reactive Data Management
|
|
284
|
-
|
|
285
|
-
```javascript
|
|
286
|
-
const resonant = new Resonant();
|
|
287
|
-
|
|
288
|
-
// Add single variables
|
|
289
|
-
resonant.add('counter', 0);
|
|
290
|
-
resonant.add('user', { name: 'John', age: 30 });
|
|
291
|
-
|
|
292
|
-
// Batch initialization
|
|
293
|
-
resonant.addAll({
|
|
294
|
-
tasks: [],
|
|
295
|
-
settings: { theme: 'light' },
|
|
296
|
-
currentUser: { name: 'Alice', role: 'admin' }
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// Changes automatically update the UI
|
|
300
|
-
user.name = 'Jane'; // DOM updates instantly
|
|
301
|
-
tasks.push({ title: 'New task' }); // Array renders new item
|
|
144
|
+
```js
|
|
145
|
+
res.add('theme', 'light', true); // third arg = persist to localStorage
|
|
146
|
+
theme = 'dark'; // saved automatically
|
|
302
147
|
```
|
|
303
148
|
|
|
304
|
-
###
|
|
305
|
-
|
|
306
|
-
React to data changes with custom logic:
|
|
149
|
+
### Bind existing variables
|
|
307
150
|
|
|
308
|
-
|
|
309
|
-
resonant.addCallback('tasks', (newValue, item, action) => {
|
|
310
|
-
console.log(`Tasks ${action}:`, item);
|
|
311
|
-
updateTaskCounter();
|
|
312
|
-
saveToAPI();
|
|
313
|
-
});
|
|
151
|
+
Already have a variable on `window`? Register it without passing a value.
|
|
314
152
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
153
|
+
```js
|
|
154
|
+
window.username = 'Alice';
|
|
155
|
+
res.add('username'); // picks up 'Alice', makes it reactive
|
|
156
|
+
res.add('username', true); // same, but also persists to localStorage
|
|
320
157
|
```
|
|
321
158
|
|
|
322
|
-
###
|
|
323
|
-
|
|
324
|
-
Automatic localStorage integration:
|
|
325
|
-
|
|
326
|
-
```javascript
|
|
327
|
-
// Data persists across browser sessions
|
|
328
|
-
resonant.add('userPreferences', { theme: 'dark' }, true);
|
|
329
|
-
resonant.add('appState', { currentView: 'dashboard' }, true);
|
|
159
|
+
### Event handling
|
|
330
160
|
|
|
331
|
-
|
|
332
|
-
|
|
161
|
+
```html
|
|
162
|
+
<button res-onclick="editTask">Edit</button> <!-- receives the current item -->
|
|
163
|
+
<button res-onclick-remove="id">Delete</button> <!-- removes item by matching property -->
|
|
333
164
|
```
|
|
334
165
|
|
|
335
|
-
###
|
|
336
|
-
|
|
337
|
-
Reactive derived values that automatically recalculate:
|
|
338
|
-
|
|
339
|
-
```javascript
|
|
340
|
-
resonant.add('firstName', 'John');
|
|
341
|
-
resonant.add('lastName', 'Doe');
|
|
166
|
+
### Callbacks
|
|
342
167
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Cannot be set directly - read-only
|
|
349
|
-
// fullName = 'Something'; // Will log warning and be ignored
|
|
350
|
-
|
|
351
|
-
// Chain computed properties
|
|
352
|
-
resonant.computed('greeting', () => {
|
|
353
|
-
return 'Hello, ' + fullName + '!';
|
|
168
|
+
```js
|
|
169
|
+
res.addCallback('tasks', (value, item, action) => {
|
|
170
|
+
console.log(action, item); // 'added', 'removed', 'modified', etc.
|
|
354
171
|
});
|
|
355
172
|
```
|
|
356
173
|
|
|
357
|
-
### Array Operations
|
|
358
|
-
|
|
359
|
-
Full array reactivity with custom methods:
|
|
360
|
-
|
|
361
|
-
```javascript
|
|
362
|
-
// All operations trigger UI updates
|
|
363
|
-
items.push(newItem); // Add item
|
|
364
|
-
items.splice(index, 1); // Remove item
|
|
365
|
-
items.update([...newItems]); // Replace entire array
|
|
366
|
-
items.set(index, newValue); // Update specific index
|
|
367
|
-
items.delete(index); // Delete by index
|
|
368
|
-
items.filter(v => v > 0); // Non-mutating; still triggers a 'filtered' callback
|
|
369
|
-
items.filterInPlace(fn); // Mutating filter + rerender
|
|
370
|
-
items.forceUpdate(); // Force a rerender without changing contents
|
|
371
|
-
```
|
|
372
|
-
|
|
373
174
|
---
|
|
374
175
|
|
|
375
|
-
##
|
|
176
|
+
## Build a Todo App
|
|
376
177
|
|
|
377
|
-
|
|
178
|
+
Copy this into an `.html` file and open it in your browser.
|
|
378
179
|
|
|
379
180
|
```html
|
|
380
|
-
|
|
381
|
-
|
|
181
|
+
<!doctype html>
|
|
182
|
+
<html>
|
|
183
|
+
<head>
|
|
184
|
+
<style>.done { text-decoration: line-through; color: #999; }</style>
|
|
185
|
+
<script src="https://unpkg.com/resonantjs@latest/resonant.min.js"></script>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
<h1>Todos (<span res="tasks.length"></span>)</h1>
|
|
189
|
+
|
|
190
|
+
<input placeholder="Add a task..." res="newTask" />
|
|
382
191
|
<button onclick="addTask()">Add</button>
|
|
383
|
-
|
|
384
|
-
<select res="filter">
|
|
385
|
-
<option value="all">All Tasks</option>
|
|
386
|
-
<option value="active">Active</option>
|
|
387
|
-
<option value="completed">Completed</option>
|
|
388
|
-
</select>
|
|
389
|
-
|
|
192
|
+
|
|
390
193
|
<ul>
|
|
391
|
-
<li res="
|
|
392
|
-
|
|
393
|
-
<
|
|
394
|
-
<
|
|
395
|
-
<button res-onclick="deleteTask(item)">Delete</button>
|
|
194
|
+
<li res="tasks">
|
|
195
|
+
<input type="checkbox" res-prop="done" />
|
|
196
|
+
<span res-prop="name" res-style="done ? 'done' : ''"></span>
|
|
197
|
+
<button res-onclick="removeTask">x</button>
|
|
396
198
|
</li>
|
|
397
199
|
</ul>
|
|
398
|
-
</div>
|
|
399
200
|
|
|
400
|
-
<script>
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
newTask: '',
|
|
408
|
-
filter: 'all',
|
|
409
|
-
filteredTasks: []
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
function addTask() {
|
|
413
|
-
if (newTask.trim()) {
|
|
414
|
-
tasks.push({ title: newTask, completed: false });
|
|
415
|
-
newTask = '';
|
|
416
|
-
updateFilter();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function deleteTask(task) {
|
|
421
|
-
const index = tasks.indexOf(task);
|
|
422
|
-
tasks.splice(index, 1);
|
|
423
|
-
updateFilter();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function updateFilter() {
|
|
427
|
-
filteredTasks.splice(0);
|
|
428
|
-
tasks.forEach(task => filteredTasks.push(task));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
resonant.addCallback('filter', updateFilter);
|
|
432
|
-
resonant.addCallback('tasks', updateFilter);
|
|
433
|
-
updateFilter();
|
|
434
|
-
</script>
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Dashboard with Statistics
|
|
438
|
-
|
|
439
|
-
```html
|
|
440
|
-
<div class="dashboard">
|
|
441
|
-
<div class="stats">
|
|
442
|
-
<div class="stat-card">
|
|
443
|
-
<h3 res="stats.totalTasks"></h3>
|
|
444
|
-
<p>Total Tasks</p>
|
|
445
|
-
</div>
|
|
446
|
-
<div class="stat-card">
|
|
447
|
-
<h3 res="stats.completedTasks"></h3>
|
|
448
|
-
<p>Completed</p>
|
|
449
|
-
</div>
|
|
450
|
-
<div class="stat-card">
|
|
451
|
-
<h3 res="stats.completionRate"></h3>
|
|
452
|
-
<p>% Complete</p>
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
455
|
-
|
|
456
|
-
<div class="projects">
|
|
457
|
-
<div res="projects">
|
|
458
|
-
<div res-prop="" class="project-card">
|
|
459
|
-
<h3 res-prop="name"></h3>
|
|
460
|
-
<div class="progress-bar">
|
|
461
|
-
<div class="progress" res-style="'width: ' + progress + '%'"></div>
|
|
462
|
-
</div>
|
|
463
|
-
<div res-prop="tasks">
|
|
464
|
-
<div res-prop="" res-style="'task priority-' + priority">
|
|
465
|
-
<span res-prop="title"></span>
|
|
466
|
-
<span res-prop="assignee"></span>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
</div>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
472
|
-
</div>
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
---
|
|
476
|
-
|
|
477
|
-
## Advanced Patterns
|
|
478
|
-
|
|
479
|
-
### Nested Data Structures
|
|
480
|
-
|
|
481
|
-
Handle complex, deeply nested data:
|
|
482
|
-
|
|
483
|
-
```javascript
|
|
484
|
-
resonant.add('company', {
|
|
485
|
-
departments: [
|
|
486
|
-
{
|
|
487
|
-
name: 'Engineering',
|
|
488
|
-
teams: [
|
|
489
|
-
{
|
|
490
|
-
name: 'Frontend',
|
|
491
|
-
members: [
|
|
492
|
-
{ name: 'Alice', role: 'Senior Dev', skills: ['React', 'Vue'] },
|
|
493
|
-
{ name: 'Bob', role: 'Junior Dev', skills: ['HTML', 'CSS'] }
|
|
494
|
-
]
|
|
495
|
-
}
|
|
201
|
+
<script>
|
|
202
|
+
const res = new Resonant();
|
|
203
|
+
res.addAll({
|
|
204
|
+
newTask: '',
|
|
205
|
+
tasks: [
|
|
206
|
+
{ name: 'Learn ResonantJs', done: false },
|
|
207
|
+
{ name: 'Ship a feature', done: true }
|
|
496
208
|
]
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function addTask() {
|
|
212
|
+
const title = newTask.trim();
|
|
213
|
+
if (!title) return;
|
|
214
|
+
tasks.unshift({ name: title, done: false });
|
|
215
|
+
newTask = '';
|
|
497
216
|
}
|
|
498
|
-
]
|
|
499
|
-
});
|
|
500
217
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
218
|
+
function removeTask(item) {
|
|
219
|
+
const idx = tasks.indexOf(item);
|
|
220
|
+
if (idx !== -1) tasks.delete(idx);
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
</body>
|
|
224
|
+
</html>
|
|
504
225
|
```
|
|
505
226
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
Create reactive calculated values that automatically update when dependencies change:
|
|
227
|
+
---
|
|
509
228
|
|
|
510
|
-
|
|
511
|
-
const resonant = new Resonant();
|
|
512
|
-
resonant.add('tasks', [
|
|
513
|
-
{ title: 'Task 1', completed: true },
|
|
514
|
-
{ title: 'Task 2', completed: false },
|
|
515
|
-
{ title: 'Task 3', completed: true }
|
|
516
|
-
]);
|
|
229
|
+
## API Reference
|
|
517
230
|
|
|
518
|
-
|
|
519
|
-
resonant.computed('totalTasks', () => {
|
|
520
|
-
return tasks.length;
|
|
521
|
-
});
|
|
231
|
+
### JavaScript
|
|
522
232
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
233
|
+
| Method | Description |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `new Resonant()` | Create an instance |
|
|
236
|
+
| `.add(name, value?, persist?)` | Add a reactive variable. Omit `value` to bind an existing `window` variable. Pass `true` as second or third arg to persist to `localStorage`. |
|
|
237
|
+
| `.addAll({ name: value, ... })` | Add multiple variables at once |
|
|
238
|
+
| `.addCallback(name, fn)` | Listen for changes. `fn(currentValue, item, action)` |
|
|
239
|
+
| `.computed(name, fn)` | Define a read-only derived value |
|
|
526
240
|
|
|
527
|
-
|
|
528
|
-
return totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
529
|
-
});
|
|
241
|
+
### HTML Attributes
|
|
530
242
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
243
|
+
| Attribute | Description |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `res="varName"` | Bind element to a variable (scalar, object, or array template) |
|
|
246
|
+
| `res-prop="key"` | Bind to an object property within a `res` context |
|
|
247
|
+
| `res-display="expr"` | Show/hide element based on a JS expression |
|
|
248
|
+
| `res-style="expr"` | Apply CSS classes from a JS expression |
|
|
249
|
+
| `res-onclick="fnName"` | Call a global function on click; receives current item if in an array |
|
|
250
|
+
| `res-onclick-remove="prop"` | Remove the current item from its parent array by matching property |
|
|
537
251
|
|
|
538
|
-
|
|
539
|
-
return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
540
|
-
});
|
|
252
|
+
### Array Methods
|
|
541
253
|
|
|
542
|
-
|
|
543
|
-
return subtotal * taxRate;
|
|
544
|
-
});
|
|
254
|
+
Reactive arrays support all standard methods plus:
|
|
545
255
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
256
|
+
| Method | Description |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `.set(index, value)` | Update item at index |
|
|
259
|
+
| `.delete(index)` | Remove item at index |
|
|
260
|
+
| `.update(newArray)` | Replace entire array contents |
|
|
261
|
+
| `.filterInPlace(fn)` | Mutating filter |
|
|
262
|
+
| `.forceUpdate()` | Force re-render without changing data |
|
|
550
263
|
|
|
551
|
-
|
|
552
|
-
<!-- These automatically update when items change -->
|
|
553
|
-
<div>Subtotal: $<span res="subtotal"></span></div>
|
|
554
|
-
<div>Tax: $<span res="tax"></span></div>
|
|
555
|
-
<div>Total: $<span res="total"></span></div>
|
|
556
|
-
```
|
|
264
|
+
### Callback Actions
|
|
557
265
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
Organize code into reusable patterns:
|
|
561
|
-
|
|
562
|
-
```javascript
|
|
563
|
-
function createTaskManager(containerId) {
|
|
564
|
-
const resonant = new Resonant();
|
|
565
|
-
|
|
566
|
-
resonant.addAll({
|
|
567
|
-
tasks: [],
|
|
568
|
-
filter: 'all',
|
|
569
|
-
newTask: ''
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
resonant.addCallback('tasks', updateStats);
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
addTask: () => { /* implementation */ },
|
|
576
|
-
removeTask: (task) => { /* implementation */ },
|
|
577
|
-
setFilter: (filter) => { /* implementation */ }
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
```
|
|
266
|
+
`added` `removed` `modified` `updated` `filtered`
|
|
581
267
|
|
|
582
268
|
---
|
|
583
269
|
|
|
584
|
-
##
|
|
585
|
-
|
|
586
|
-
Explore our comprehensive examples:
|
|
270
|
+
## Performance
|
|
587
271
|
|
|
588
|
-
- **
|
|
589
|
-
- **
|
|
590
|
-
- **
|
|
591
|
-
- **
|
|
592
|
-
|
|
593
|
-
Each example demonstrates different aspects of ResonantJs and can serve as starting points for your projects.
|
|
272
|
+
- **Selective array re-rendering** -- when a property on one array item changes, only that item's DOM subtree is updated. Siblings are untouched, including their `res-display` and `res-style` evaluations.
|
|
273
|
+
- **Batched updates** -- rapid changes within the same tick are coalesced into a single DOM update.
|
|
274
|
+
- **Computed property chains** -- cascading computed properties resolve in dependency order within a single pass.
|
|
275
|
+
- **Stable keys** -- array items are tracked by stable keys for efficient reuse during re-renders.
|
|
594
276
|
|
|
595
277
|
---
|
|
596
278
|
|
|
597
|
-
##
|
|
279
|
+
## Browser Support
|
|
598
280
|
|
|
599
|
-
|
|
281
|
+
Chrome 60+ / Firefox 55+ / Safari 12+ / Edge 79+ / Mobile browsers
|
|
600
282
|
|
|
601
|
-
|
|
602
|
-
- **Efficient diffing**: Smart change detection for nested objects
|
|
603
|
-
- **Lazy evaluation**: Conditional expressions only run when dependencies change
|
|
604
|
-
- **Memory efficient**: Automatic cleanup of unused observers
|
|
283
|
+
---
|
|
605
284
|
|
|
606
|
-
|
|
285
|
+
## Examples
|
|
607
286
|
|
|
608
|
-
-
|
|
609
|
-
-
|
|
610
|
-
-
|
|
611
|
-
-
|
|
612
|
-
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
287
|
+
- [Basic Counter](./examples/example-basic.html)
|
|
288
|
+
- [Task Manager](./examples/example-taskmanager.html)
|
|
289
|
+
- [Nested Data (Houses)](./examples/example-houses.html)
|
|
290
|
+
- [Tests Showcase](./examples/tests.html)
|
|
613
291
|
|
|
614
292
|
---
|
|
615
293
|
|
|
616
|
-
##
|
|
617
|
-
|
|
618
|
-
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
619
|
-
|
|
620
|
-
### Development Setup
|
|
294
|
+
## Development
|
|
621
295
|
|
|
622
296
|
```bash
|
|
623
297
|
git clone https://github.com/amurgola/ResonantJs.git
|
|
624
298
|
cd ResonantJs
|
|
625
299
|
npm install
|
|
626
|
-
npm test
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
### Running Tests
|
|
630
|
-
|
|
631
|
-
```bash
|
|
632
|
-
npm test # Run all tests
|
|
633
|
-
npm test -- test/specific.test.js # Run specific test file
|
|
300
|
+
npm test # run all tests
|
|
301
|
+
npm run build # run tests + minify
|
|
634
302
|
```
|
|
635
303
|
|
|
636
304
|
---
|
|
637
305
|
|
|
638
306
|
## License
|
|
639
307
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
---
|
|
643
|
-
|
|
644
|
-
## Support & Community
|
|
645
|
-
|
|
646
|
-
- **Issues**: [GitHub Issues](https://github.com/amurgola/ResonantJs/issues)
|
|
647
|
-
- **Discussions**: [GitHub Discussions](https://github.com/amurgola/ResonantJs/discussions)
|
|
648
|
-
- **Documentation**: [Full API Documentation](https://github.com/amurgola/ResonantJs/wiki)
|
|
308
|
+
MIT -- see [LICENSE](LICENSE).
|
|
649
309
|
|
|
650
310
|
---
|
|
651
311
|
|
|
652
312
|
<div align="center">
|
|
653
313
|
|
|
654
|
-
**[
|
|
314
|
+
**[GitHub](https://github.com/amurgola/ResonantJs)** · **[npm](https://www.npmjs.com/package/resonantjs)** · **[Issues](https://github.com/amurgola/ResonantJs/issues)**
|
|
655
315
|
|
|
656
|
-
*Built
|
|
316
|
+
*Built by [Andrew Paul Murgola](https://github.com/amurgola)*
|
|
657
317
|
|
|
658
318
|
</div>
|