resonantjs 1.1.1 → 1.1.3

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.
@@ -6,7 +6,7 @@
6
6
  rel="stylesheet"
7
7
  href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css"
8
8
  />
9
- <script src="https://unpkg.com/resonantjs@latest/resonant.min.js"></script>
9
+ <script src="../resonant.js"></script>
10
10
  <style>
11
11
  section.grid {
12
12
  padding: 1rem;
@@ -40,6 +40,17 @@
40
40
  <button onclick="counter++">Increment Counter</button>
41
41
  </section>
42
42
 
43
+ <section class="grid">
44
+ <code class="code">
45
+ resonantJs.add("counter", <span res="counter">0</span>);
46
+ </code>
47
+ <code class="code">
48
+ Counter: &lt;span res="counter"&gt;&lt;/span&gt;<br/><br/>
49
+ &lt;button onclick="counter++"&gt;Increment Counter&lt;/button&gt;<br/><br/>
50
+ &lt;div res-display="counter &lt; 10"&gt;contents&lt;/div&gt;<br/>
51
+ </code>
52
+ </section>
53
+
43
54
  <!-- Demonstrate object property binding -->
44
55
  <section class="grid" res="user">
45
56
  <div>
@@ -67,6 +78,26 @@
67
78
  </div>
68
79
  </section>
69
80
 
81
+ <section class="grid" res="user">
82
+ <code class="code">
83
+ resonantJs.add({<br/>
84
+ &nbsp;&nbsp;user: {<br/>
85
+ &nbsp;&nbsp;&nbsp;&nbsp;firstname: "<span res-prop="firstname"></span>",<br/>
86
+ &nbsp;&nbsp;&nbsp;&nbsp;lastname: "<span res-prop="lastname"></span>"<br/>
87
+ &nbsp;&nbsp;}<br/>
88
+ });
89
+ </code>
90
+ <code class="code">
91
+ &lt;div res="user"&gt;<br/>
92
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;input type="text" res-prop="firstname" /&gt;<br/>
93
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;input type="text" res-prop="lastname" /&gt;<br/>
94
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;span res-prop="firstname"&gt;&lt;/span&gt;<br/>
95
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;span res-prop="lastname"&gt;&lt;/span&gt;<br/>
96
+ &lt;/div&gt;
97
+ <br/><br/>
98
+ </code>
99
+ </section>
100
+
70
101
  <!-- Demonstrate dynamic list rendering -->
71
102
  <section class="grid">
72
103
  <div>
@@ -77,8 +108,84 @@
77
108
  </li>
78
109
  </ul>
79
110
  </div>
111
+ <div>
112
+ <button onclick="addProjectMember()">Add</button>
113
+ <button onclick="toggleProjectMemberName()">Toggle Alice's Name</button>
114
+ </div>
80
115
 
81
- <button onclick="addProjectMember()">Add Project Member</button>
116
+ </section>
117
+
118
+ <section class="grid">
119
+ <code class="code">
120
+ resonantJs.add({<br/>
121
+ &nbsp;&nbsp;projectTeam: [<br/>
122
+ &nbsp;&nbsp;&nbsp;&nbsp;{ name: "Alice", role: "Developer" },<br/>
123
+ &nbsp;&nbsp;&nbsp;&nbsp;{ name: "Bob", role: "Designer" }<br/>
124
+ &nbsp;&nbsp;]<br/>
125
+ });<br/><br/>
126
+
127
+ function addProjectMember() {<br/>
128
+ &nbsp;&nbsp;const newMember = { name: "Charlie", role: "Product Manager" };<br/>
129
+ &nbsp;&nbsp;projectTeam.push(newMember);<br/>
130
+ }
131
+ </code>
132
+ <code class="code">
133
+ &lt;ul res="projectTeam"&gt;<br/>
134
+ &nbsp;&nbsp;&lt;li&gt;<br/>
135
+ &nbsp;&nbsp;&nbsp;&nbsp;&lt;span res-prop="name"&gt;&lt;/span&gt - &lt;span res-prop="role"&gt;&lt;/span&gt<br/>
136
+ &nbsp;&nbsp;&lt;/li&gt;<br/>
137
+ &lt;/ul&gt;<br/><br/>
138
+ &lt;button onclick="addProjectMember()"&gt;Add Project Member&lt;/button&gt;
139
+ </code>
140
+ </section>
141
+ <section>
142
+ <h1>Summary</h1>
143
+ <p>
144
+ Resonant.js is a simple library that allows you to bind JavaScript variables to HTML elements.
145
+ This allows you to create dynamic web applications without the need for complex frameworks.
146
+ </p>
147
+ <h2>HTML properties</h2>
148
+ <table>
149
+ <tr>
150
+ <td>res</td>
151
+ <td>Bind an object to a section of HTML</td>
152
+ </tr>
153
+ <tr>
154
+ <td>res-prop</td>
155
+ <td>Bind an object property to an HTML element, used when the defined variable is an object array</td>
156
+ </tr>
157
+ <tr>
158
+ <td>res-display</td>
159
+ <td>Conditionally display an element based on a JavaScript expression</td>
160
+ </tr>
161
+ <tr>
162
+ <td>res-style, res-styles</td>
163
+ <td>Bind a style or styles object to an HTML element conditionally</td>
164
+ </tr>
165
+ <tr>
166
+ <td>res-onclick, res-onclick-remove</td>
167
+ <td>Bind an onclick event to an HTML element</td>
168
+ </tr>
169
+ </table>
170
+ <h2>Javascript Functions</h2>
171
+ <table>
172
+ <tr>
173
+ <td>const resonantJs = new Resonant();</td>
174
+ <td>Initialize the Resonant object</td>
175
+ </tr>
176
+ <tr>
177
+ <td>resonantJs.add("variableName", optionalBooleanForPersistence)</td>
178
+ <td>Bind a variable to the Resonant object</td>
179
+ </tr>
180
+ <tr>
181
+ <td>resonantJs.addAll()</td>
182
+ <td>Bind multiple variables to the Resonant object at once</td>
183
+ </tr>
184
+ <tr>
185
+ <td>resonantJs.addCallback("variableName", (objectReturned) => {})</td>
186
+ <td>Bind a callback function to a variable</td>
187
+ </tr>
188
+ </table>
82
189
  </section>
83
190
  </main>
84
191
 
@@ -92,8 +199,7 @@
92
199
  resonantJs.addAll({
93
200
  user: {
94
201
  firstname: "John",
95
- lastname: "Doe",
96
- email: ""
202
+ lastname: "Doe"
97
203
  },
98
204
  projectTeam: [
99
205
  { name: "Alice", role: "Developer" },
@@ -116,6 +222,12 @@
116
222
  const newMember = { name: "Charlie", role: "Product Manager" };
117
223
  projectTeam.push(newMember);
118
224
  }
225
+
226
+ //Silly function to demonstrate dynamic list rerendering
227
+ function toggleProjectMemberName() {
228
+ const alice = projectTeam[0];
229
+ alice.name = alice.name === "Alice" ? "Alicia" : "Alice";
230
+ }
119
231
  </script>
120
232
  </body>
121
233
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resonantjs",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "A lightweight JavaScript framework that enables reactive data-binding for building dynamic and responsive web applications. It simplifies creating interactive UIs by automatically updating the DOM when your data changes.",
5
5
  "main": "resonant.js",
6
6
  "repository": {
package/resonant.js CHANGED
@@ -1,6 +1,6 @@
1
1
  class ObservableArray extends Array {
2
2
  constructor(variableName, resonantInstance, ...args) {
3
- if(resonantInstance === undefined) {
3
+ if (resonantInstance === undefined) {
4
4
  return super(...args);
5
5
  }
6
6
  super(...args);
@@ -13,6 +13,7 @@ class ObservableArray extends Array {
13
13
 
14
14
  this.forEach((item, index) => {
15
15
  if (typeof item === 'object') {
16
+ this._setKeyForObjectAndChildObjects(item, index);
16
17
  this[index] = this._createProxy(item, index);
17
18
  }
18
19
  });
@@ -22,12 +23,53 @@ class ObservableArray extends Array {
22
23
  }
23
24
  }
24
25
 
26
+ _setKeyForObjectAndChildObjects(obj, index) {
27
+ if (typeof obj === 'object') {
28
+ obj.key = index + '-' + this._generateKeyByContentCheckSum(obj);
29
+ Object.keys(obj).forEach(key => {
30
+ this._setKeyForObjectAndChildObjects(obj[key], index);
31
+ });
32
+ }
33
+ }
34
+
35
+ _generateKeyByContentCheckSum(obj) {
36
+ const removeKeysRecursively = (obj) => {
37
+ if (!obj || typeof obj !== 'object') return obj;
38
+
39
+ if (Array.isArray(obj)) {
40
+ return obj.map(item => removeKeysRecursively(item));
41
+ }
42
+
43
+ const newObj = {...obj};
44
+ delete newObj.key;
45
+
46
+ Object.keys(newObj).forEach(key => {
47
+ if (typeof newObj[key] === 'object' && newObj[key] !== null) {
48
+ newObj[key] = removeKeysRecursively(newObj[key]);
49
+ }
50
+ });
51
+
52
+ return newObj;
53
+ };
54
+
55
+ const objForHash = removeKeysRecursively(obj);
56
+ const str = JSON.stringify(objForHash);
57
+ let hash = 0;
58
+ for (let i = 0; i < str.length; i++) {
59
+ const char = str.charCodeAt(i);
60
+ hash = ((hash << 5) - hash) + char;
61
+ hash = hash & hash;
62
+ }
63
+ return Math.abs(hash).toString(36);
64
+ }
65
+
25
66
  _createProxy(item, index) {
26
67
  return new Proxy(item, {
27
68
  set: (target, property, value) => {
28
69
  if (target[property] !== value) {
29
70
  const oldValue = target[property];
30
71
  target[property] = value;
72
+ this._setKeyForObjectAndChildObjects(item, index);
31
73
  this.resonantInstance._queueUpdate(this.variableName, 'modified', target, property, oldValue, index);
32
74
  }
33
75
  return true;
@@ -35,8 +77,11 @@ class ObservableArray extends Array {
35
77
  });
36
78
  }
37
79
 
38
- //temp fix for issues
39
80
  forceUpdate() {
81
+ this.forEach((item, index) => {
82
+ console.log('forceUpdate', item, index);
83
+ this._setKeyForObjectAndChildObjects(item, index);
84
+ });
40
85
  this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
41
86
  this.resonantInstance._queueUpdate(this.variableName, 'modified', this.slice());
42
87
  }
@@ -49,6 +94,7 @@ class ObservableArray extends Array {
49
94
  push(...args) {
50
95
  args = args.map((item, index) => {
51
96
  if (typeof item === 'object') {
97
+ this._setKeyForObjectAndChildObjects(item, this.length + index);
52
98
  return this._createProxy(item, this.length + index);
53
99
  }
54
100
  return item;
@@ -102,9 +148,9 @@ class ObservableArray extends Array {
102
148
  }
103
149
 
104
150
  this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
105
-
106
151
  const oldValue = this[index];
107
152
  this[index] = value;
153
+
108
154
  this.resonantInstance._queueUpdate(this.variableName, action, this[index], index, oldValue);
109
155
  }
110
156
  return true;
@@ -114,9 +160,7 @@ class ObservableArray extends Array {
114
160
  const oldValue = this[index];
115
161
  this.isDeleting = true;
116
162
  this.splice(index, 1);
117
-
118
163
  this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
119
-
120
164
  this.resonantInstance._queueUpdate(this.variableName, 'removed', null, index, oldValue);
121
165
  this.isDeleting = false;
122
166
  return true;
@@ -126,7 +170,6 @@ class ObservableArray extends Array {
126
170
  if(this.resonantInstance === undefined || actuallyFilter === false) {
127
171
  return super.filter(filter);
128
172
  }
129
-
130
173
  const result = super.filter(filter);
131
174
  this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
132
175
  this.resonantInstance._queueUpdate(this.variableName, 'filtered');
@@ -142,12 +185,28 @@ class Resonant {
142
185
  this.arrayDataChangeDetection = {};
143
186
  }
144
187
 
188
+ _handleInputElement(element, value, onChangeCallback) {
189
+ if (element.type === 'checkbox') {
190
+ element.checked = value;
191
+ if (!element.hasAttribute('data-resonant-bound')) {
192
+ element.onchange = () => onChangeCallback(element.checked);
193
+ element.setAttribute('data-resonant-bound', 'true');
194
+ }
195
+ } else {
196
+ element.value = value;
197
+ if (!element.hasAttribute('data-resonant-bound')) {
198
+ element.oninput = () => onChangeCallback(element.value);
199
+ element.setAttribute('data-resonant-bound', 'true');
200
+ }
201
+ }
202
+ }
203
+
145
204
  add(variableName, value, persist) {
146
205
  value = this.persist(variableName, value, persist);
147
206
  if (Array.isArray(value)) {
148
207
  this.data[variableName] = new ObservableArray(variableName, this, ...value);
149
208
  this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
150
- } else if (typeof value === 'object') {
209
+ } else if (typeof value === 'object' && value !== null) {
151
210
  this.data[variableName] = this._createObject(variableName, value);
152
211
  } else {
153
212
  this.data[variableName] = value;
@@ -162,8 +221,8 @@ class Resonant {
162
221
  return value;
163
222
  }
164
223
  var found = localStorage.getItem('res_' + variableName);
165
- if (found !== null && found !== undefined){
166
- return JSON.parse(localStorage.getItem('res_' + variableName));
224
+ if (found !== null && found !== undefined) {
225
+ return JSON.parse(found);
167
226
  } else {
168
227
  localStorage.setItem('res_' + variableName, JSON.stringify(value));
169
228
  return value;
@@ -182,6 +241,10 @@ class Resonant {
182
241
  });
183
242
  }
184
243
 
244
+ _resolveValue(instance, key, override = null) {
245
+ return override ?? instance[key];
246
+ }
247
+
185
248
  _createObject(variableName, obj) {
186
249
  obj[Symbol('isProxy')] = true;
187
250
  return new Proxy(obj, {
@@ -196,15 +259,56 @@ class Resonant {
196
259
  });
197
260
  }
198
261
 
262
+ _evaluateDisplayCondition(element, item, condition) {
263
+ try {
264
+ const show = new Function('item', `return ${condition}`)(item);
265
+ element.style.display = show ? '' : 'none';
266
+ } catch (e) {
267
+ console.error(`Error evaluating display condition: ${condition}`, e);
268
+ }
269
+ }
270
+
271
+ _handleDisplayElements(parentElement, instance) {
272
+ const displayElements = parentElement.querySelectorAll('[res-display]');
273
+ displayElements.forEach(displayEl => {
274
+ const condition = displayEl.getAttribute('res-display') || '';
275
+ this._evaluateDisplayCondition(displayEl, instance, condition);
276
+ });
277
+ }
278
+
279
+ _bindClickEvents(parentElement, instance, arrayValue) {
280
+ const onclickElements = parentElement.querySelectorAll('[res-onclick], [res-onclick-remove]');
281
+ onclickElements.forEach(onclickEl => {
282
+ const functionName = onclickEl.getAttribute('res-onclick');
283
+ const removeKey = onclickEl.getAttribute('res-onclick-remove');
284
+
285
+ if (functionName) {
286
+ onclickEl.onclick = () => {
287
+ const func = new Function('item', `return ${functionName}(item)`);
288
+ func(instance);
289
+ };
290
+ }
291
+ if (removeKey) {
292
+ onclickEl.onclick = () => {
293
+ if (arrayValue) {
294
+ const removeIdx = arrayValue.findIndex(t => t[removeKey] === instance[removeKey]);
295
+ if (removeIdx !== -1) {
296
+ arrayValue.splice(removeIdx, 1);
297
+ }
298
+ }
299
+ };
300
+ }
301
+ });
302
+ }
303
+
199
304
  _defineProperty(variableName) {
200
305
  Object.defineProperty(window, variableName, {
201
306
  get: () => this.data[variableName],
202
307
  set: (newValue) => {
203
308
  if (Array.isArray(newValue)) {
204
309
  this.data[variableName] = new ObservableArray(variableName, this, ...newValue);
205
- this.arrayDataChangeDetection[variableName] = this.data[variableName].slice(); // Create a copy for change detection
206
-
207
- } else if (typeof newValue === 'object') {
310
+ this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
311
+ } else if (typeof newValue === 'object' && newValue !== null) {
208
312
  this.data[variableName] = this._createObject(variableName, newValue);
209
313
  } else {
210
314
  this.data[variableName] = newValue;
@@ -212,7 +316,8 @@ class Resonant {
212
316
  this.updateElement(variableName);
213
317
  this.updateDisplayConditionalsFor(variableName);
214
318
  this.updateStylesFor(variableName);
215
- if (!Array.isArray(newValue) && typeof newValue !== 'object') {
319
+
320
+ if (!Array.isArray(newValue) && (typeof newValue !== 'object' || newValue === null)) {
216
321
  this._queueUpdate(variableName, 'modified', this.data[variableName]);
217
322
  }
218
323
  }
@@ -223,7 +328,6 @@ class Resonant {
223
328
  if (!this.pendingUpdates.has(variableName)) {
224
329
  this.pendingUpdates.set(variableName, []);
225
330
  }
226
-
227
331
  this.pendingUpdates.get(variableName).push({ action, item, property, oldValue });
228
332
 
229
333
  if (this.pendingUpdates.get(variableName).length === 1) {
@@ -232,16 +336,19 @@ class Resonant {
232
336
  this.updatePersistantData(variableName);
233
337
  this.pendingUpdates.delete(variableName);
234
338
 
235
- updates = updates.filter((v, i, a) => a.findIndex(t => (t.property === v.property && t.action === v.action)) === i);
339
+ updates = updates.filter((v, i, a) =>
340
+ a.findIndex(t => (t.property === v.property && t.action === v.action)) === i
341
+ );
342
+
236
343
  updates.forEach(update => {
237
344
  this._triggerCallbacks(variableName, update);
238
345
  });
346
+
239
347
  this.updateElement(variableName);
240
348
  this.updateDisplayConditionalsFor(variableName);
241
349
  this.updateStylesFor(variableName);
242
350
  }, 0);
243
351
  }
244
-
245
352
  }
246
353
 
247
354
  _triggerCallbacks(variableName, callbackData) {
@@ -258,55 +365,27 @@ class Resonant {
258
365
  const value = this.data[variableName];
259
366
 
260
367
  elements.forEach(element => {
261
- element.value = value;
262
368
  if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
263
- if (!element.hasAttribute('data-resonant-bound')) {
264
- element.oninput = () => {
265
- this.data[variableName] = element.value;
266
- this._queueUpdate(variableName, 'modified', this.data[variableName]);
267
- };
268
- element.setAttribute('data-resonant-bound', 'true');
269
- }
270
- } else if (Array.isArray(value)) {
271
- element.querySelectorAll(`[res="${variableName}"][res-rendered=true]`).forEach(el => el.remove());
369
+ this._handleInputElement(element, value, (newValue) => {
370
+ this.data[variableName] = newValue;
371
+ this._queueUpdate(variableName, 'modified', this.data[variableName]);
372
+ });
373
+ }
374
+ else if (Array.isArray(value)) {
375
+ element.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
272
376
  this._renderArray(variableName, element);
273
- } else if (typeof value === 'object') {
274
- const subElements = element.querySelectorAll(`[res-prop]`);
377
+ }
378
+ else if (typeof value === 'object' && value !== null) {
379
+ const subElements = element.querySelectorAll('[res-prop]');
275
380
  subElements.forEach(subEl => {
276
381
  const key = subEl.getAttribute('res-prop');
277
382
  if (key && key in value) {
278
- if (!subEl.hasAttribute('data-resonant-bound')) {
279
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
280
- if (subEl.type === 'checkbox') {
281
- subEl.checked = value[key];
282
- subEl.onchange = () => {
283
- this.data[variableName][key] = subEl.checked;
284
- };
285
- } else {
286
- subEl.value = value[key];
287
- subEl.oninput = () => {
288
- this.data[variableName][key] = subEl.value;
289
- };
290
- }
291
- } else {
292
- subEl.innerHTML = value[key];
293
- }
294
- subEl.setAttribute('data-resonant-bound', 'true');
295
- } else {
296
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
297
- if (subEl.type === 'checkbox') {
298
- subEl.checked = value[key];
299
- } else {
300
- subEl.value = value[key];
301
- }
302
- } else {
303
- subEl.innerHTML = value[key];
304
- }
305
- }
383
+ this._renderObjectProperty(subEl, value[key], variableName, key);
306
384
  }
307
385
  });
308
- } else {
309
- element.innerHTML = value;
386
+ }
387
+ else {
388
+ element.innerHTML = value ?? '';
310
389
  }
311
390
  });
312
391
 
@@ -314,25 +393,151 @@ class Resonant {
314
393
  this.updateStylesFor(variableName);
315
394
  }
316
395
 
396
+ _renderObjectProperty(subEl, propValue, parentVarName, key) {
397
+ if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
398
+ !Array.isArray(propValue) &&
399
+ typeof propValue !== 'object') {
400
+ this._handleInputElement(subEl, propValue, (newValue) => {
401
+ window[parentVarName][key] = newValue;
402
+ });
403
+ }
404
+ else if (Array.isArray(propValue)) {
405
+ this._renderNestedArray(subEl, propValue);
406
+ }
407
+ else if (typeof propValue === 'object' && propValue !== null) {
408
+ const nestedElements = subEl.querySelectorAll('[res-prop]');
409
+ nestedElements.forEach(nestedEl => {
410
+ const nestedKey = nestedEl.getAttribute('res-prop');
411
+ if (nestedKey && nestedKey in propValue) {
412
+ this._renderObjectProperty(nestedEl, propValue[nestedKey], parentVarName, nestedKey);
413
+ }
414
+ });
415
+ }
416
+ else {
417
+ subEl.innerHTML = propValue ?? '';
418
+ }
419
+ }
420
+
421
+ _renderArray(variableName, el) {
422
+ const container = el.hasAttribute('res') && el.parentElement ? el.parentElement : el;
423
+
424
+ let template;
425
+ if (!window[variableName + "_template"]) {
426
+ template = el.cloneNode(true);
427
+ window[variableName + "_template"] = template;
428
+ el.style.display = 'none';
429
+ el.setAttribute('res-template', 'true');
430
+ } else {
431
+ template = window[variableName + "_template"];
432
+ }
433
+
434
+ const existingElements = new Map();
435
+ container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(element => {
436
+ const key = element.getAttribute('res-key');
437
+ if (key) {
438
+ existingElements.set(key, element);
439
+ }
440
+ });
441
+
442
+ container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
443
+
444
+ this.data[variableName].forEach((instance, index) => {
445
+ const elementKey = instance.key;
446
+ let elementToUse;
447
+
448
+ if (existingElements.has(elementKey)) {
449
+ elementToUse = existingElements.get(elementKey);
450
+ existingElements.delete(elementKey);
451
+ elementToUse.setAttribute("res-index", index);
452
+ } else {
453
+ elementToUse = template.cloneNode(true);
454
+ elementToUse.removeAttribute('res-template');
455
+ elementToUse.style.display = '';
456
+ elementToUse.setAttribute("res-rendered", "true");
457
+ elementToUse.setAttribute("res-key", elementKey);
458
+ elementToUse.setAttribute("res-index", index);
459
+
460
+ for (let key in instance) {
461
+ let overrideInstanceValue = null;
462
+ let subEl = elementToUse.querySelector(`[res-prop="${key}"]`);
463
+ if (!subEl) {
464
+ subEl = elementToUse.querySelector('[res-prop=""]');
465
+ overrideInstanceValue = instance;
466
+ }
467
+ if (subEl) {
468
+ const value = this._resolveValue(instance, key, overrideInstanceValue);
469
+
470
+ if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
471
+ !Array.isArray(value) &&
472
+ typeof value !== 'object') {
473
+ this._handleInputElement(
474
+ subEl,
475
+ value,
476
+ (newValue) => {
477
+ instance[key] = newValue;
478
+ this._queueUpdate(variableName, 'modified', instance, key, value);
479
+ }
480
+ );
481
+ }
482
+ else if (Array.isArray(value)) {
483
+ this._renderNestedArray(subEl, value);
484
+ }
485
+ else if (typeof value === 'object' && value !== null) {
486
+ const nestedElements = subEl.querySelectorAll('[res-prop]');
487
+ nestedElements.forEach(nestedEl => {
488
+ const nestedKey = nestedEl.getAttribute('res-prop');
489
+ if (nestedKey && nestedKey in value) {
490
+ this._renderObjectProperty(nestedEl, value[nestedKey], variableName, nestedKey);
491
+ }
492
+ });
493
+ }
494
+ else {
495
+ subEl.innerHTML = value ?? '';
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ this._handleDisplayElements(elementToUse, instance);
502
+ this._bindClickEvents(elementToUse, instance, this.data[variableName]);
503
+
504
+ container.appendChild(elementToUse);
505
+ });
506
+ }
507
+ _renderNestedArray(subEl, arrayValue) {
508
+ const template = subEl.cloneNode(true);
509
+ subEl.innerHTML = '';
510
+
511
+ subEl.removeAttribute('res-prop');
512
+
513
+ arrayValue.forEach((item, idx) => {
514
+ const cloned = template.cloneNode(true);
515
+ cloned.setAttribute('res-rendered', 'true');
516
+
517
+ const nestedEls = cloned.querySelectorAll('[res-prop]');
518
+ nestedEls.forEach(nestedEl => {
519
+ const nestedKey = nestedEl.getAttribute('res-prop');
520
+ if (nestedKey && nestedKey in item) {
521
+ this._renderObjectProperty(nestedEl, item[nestedKey], null, nestedKey);
522
+ }
523
+ });
524
+
525
+ this._handleDisplayElements(cloned, item);
526
+ this._bindClickEvents(cloned, item, arrayValue);
527
+
528
+ subEl.appendChild(cloned);
529
+ });
530
+ }
317
531
  updateDisplayConditionalsFor(variableName) {
318
532
  const conditionalElements = document.querySelectorAll(`[res-display*="${variableName}"]`);
319
533
  conditionalElements.forEach(conditionalElement => {
320
534
  const condition = conditionalElement.getAttribute('res-display');
321
- try {
322
- if (eval(condition)) {
323
- conditionalElement.style.display = '';
324
- } else {
325
- conditionalElement.style.display = 'none';
326
- }
327
- } catch (e) {
328
- console.error(`Error evaluating condition for ${variableName}: ${condition}`, e);
329
- }
535
+ this._evaluateDisplayCondition(conditionalElement, this.data[variableName], condition);
330
536
  });
331
537
  }
332
538
 
333
539
  updateStylesFor(variableName) {
334
540
  const styleElements = document.querySelectorAll(`[res-style*="${variableName}"]`);
335
-
336
541
  styleElements.forEach(styleElement => {
337
542
  let styleCondition = styleElement.getAttribute('res-style');
338
543
  try {
@@ -355,23 +560,21 @@ class Resonant {
355
560
  const item = this.data[variableName][index];
356
561
  styleCondition = styleCondition.replace(new RegExp(`\\b${variableName}\\b`, 'g'), 'item');
357
562
  const styleClass = new Function('item', `return ${styleCondition}`)(item);
358
-
359
563
  if (styleClass) {
360
564
  styleElement.classList.add(styleClass);
361
565
  } else {
362
- var elementHasStyle = styleElement.classList.contains(styleClass);
566
+ const elementHasStyle = styleElement.classList.contains(styleClass);
363
567
  if (elementHasStyle) {
364
568
  styleElement.classList.remove(styleClass);
365
569
  }
366
570
  }
367
571
  } else {
368
572
  const styleClass = eval(styleCondition);
369
-
370
573
  if (styleClass) {
371
574
  styleElement.classList.add(styleClass);
372
575
  styleElement.setAttribute('res-styles', styleClass);
373
576
  } else {
374
- var elementHasStyle = styleElement.classList.contains(styleClass);
577
+ const elementHasStyle = styleElement.classList.contains(styleClass);
375
578
  if (elementHasStyle) {
376
579
  styleElement.classList.remove(styleClass);
377
580
  }
@@ -383,86 +586,6 @@ class Resonant {
383
586
  });
384
587
  }
385
588
 
386
- _renderArray(variableName, el) {
387
- let template = el.cloneNode(true);
388
- el.innerHTML = '';
389
-
390
- if (!window[variableName + "_template"]) {
391
- window[variableName + "_template"] = template;
392
- } else {
393
- template = window[variableName + "_template"];
394
- }
395
-
396
- this.data[variableName].forEach((instance, index) => {
397
- const clonedEl = template.cloneNode(true);
398
- clonedEl.setAttribute("res-index", index);
399
- for (let key in instance) {
400
- let overrideInstanceValue = null;
401
- let subEl = clonedEl.querySelector(`[res-prop="${key}"]`);
402
- if(!subEl) {
403
- subEl = clonedEl.querySelector(`[res-prop=""]`);
404
- overrideInstanceValue = instance;
405
- }
406
- if (subEl) {
407
- if (!subEl.hasAttribute('data-resonant-bound')) {
408
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
409
- if (subEl.type === 'checkbox') {
410
- subEl.checked = overrideInstanceValue ?? instance[key] ;
411
- subEl.onchange = () => {
412
- instance[key] = subEl.checked;
413
- this._queueUpdate(variableName, 'modified', instance, key, overrideInstanceValue ?? instance[key]);
414
- };
415
- } else {
416
- subEl.value = overrideInstanceValue ?? instance[key];
417
- subEl.oninput = () => {
418
- instance[key] = subEl.value;
419
- this._queueUpdate(variableName, 'modified', instance, key, overrideInstanceValue ?? instance[key]);
420
- };
421
- }
422
- } else {
423
- subEl.innerHTML = overrideInstanceValue ?? instance[key];
424
- }
425
- subEl.setAttribute('data-resonant-bound', 'true');
426
- } else {
427
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
428
- if (subEl.type === 'checkbox') {
429
- subEl.checked = overrideInstanceValue ?? instance[key];
430
- } else {
431
- subEl.value = overrideInstanceValue ?? instance[key];
432
- }
433
- } else {
434
- subEl.innerHTML = overrideInstanceValue ?? instance[key];
435
- }
436
- }
437
- }
438
- }
439
-
440
- const onclickElements = clonedEl.querySelectorAll('[res-onclick], [res-onclick-remove]');
441
- onclickElements.forEach(onclickEl => {
442
- const functionName = onclickEl.getAttribute('res-onclick');
443
- const removeKey = onclickEl.getAttribute('res-onclick-remove');
444
- if (functionName) {
445
- onclickEl.onclick = () => {
446
- const func = new Function('item', `return ${functionName}(item)`);
447
- func(instance);
448
- };
449
- }
450
-
451
- if (removeKey) {
452
- onclickEl.onclick = () => {
453
- const index = this.data[variableName].findIndex(t => t[removeKey] === instance[removeKey]);
454
- if (index !== -1) {
455
- this.data[variableName].splice(index, 1);
456
- }
457
- };
458
- }
459
- });
460
-
461
- clonedEl.setAttribute("res-rendered", true);
462
- el.appendChild(clonedEl);
463
- });
464
- }
465
-
466
589
  addCallback(variableName, method) {
467
590
  if (!this.callbacks[variableName]) {
468
591
  this.callbacks[variableName] = [];
package/resonant.min.js CHANGED
@@ -1 +1 @@
1
- class ObservableArray extends Array{constructor(e,t,...a){if(void 0===t)return super(...a);super(...a);var s=void 0===t.data[e];this.variableName=e,this.resonantInstance=t,this.isDeleting=!1,this.forEach(((e,t)=>{"object"==typeof e&&(this[t]=this._createProxy(e,t))})),s||this.forceUpdate()}_createProxy(e,t){return new Proxy(e,{set:(e,a,s)=>{if(e[a]!==s){const i=e[a];e[a]=s,this.resonantInstance._queueUpdate(this.variableName,"modified",e,a,i,t)}return!0}})}forceUpdate(){this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"modified",this.slice())}update(e){window[this.variableName]=e,this.resonantInstance._queueUpdate(this.variableName,"updated",e)}push(...e){e=e.map(((e,t)=>"object"==typeof e?this._createProxy(e,this.length+t):e));const t=super.push(...e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),e.forEach(((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"added",e,this.length-1-t)})),t}splice(e,t,...a){a=a.map(((t,a)=>"object"==typeof t?this._createProxy(t,e+a):t));const s=super.splice(e,t,...a);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),t>0&&s.forEach(((t,a)=>{this.resonantInstance._queueUpdate(this.variableName,"removed",t,e+a)})),a.length>0&&a.forEach(((t,a)=>{this.resonantInstance._queueUpdate(this.variableName,"added",t,e+a)})),s}set(e,t){if(this[e]!==t){if(this.isDeleting)return!0;const a=this.resonantInstance.arrayDataChangeDetection[this.variableName];let s="modified";(e>=a.length||void 0===a[e])&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();const i=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,i)}return!0}delete(e){const t=this[e];return this.isDeleting=!0,this.splice(e,1),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"removed",null,e,t),this.isDeleting=!1,!0}filter(e,t=!0){if(void 0===this.resonantInstance||!1===t)return super.filter(e);const a=super.filter(e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"filtered"),a}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}add(e,t,a){t=this.persist(e,t,a),Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):this.data[e]="object"==typeof t?this._createObject(e,t):t,this._defineProperty(e),this.updateElement(e)}persist(e,t,a){if(void 0===a||!a)return t;var s=localStorage.getItem("res_"+e);return null!=s?JSON.parse(localStorage.getItem("res_"+e)):(localStorage.setItem("res_"+e,JSON.stringify(t)),t)}updatePersistantData(e){localStorage.getItem("res_"+e)&&localStorage.setItem("res_"+e,JSON.stringify(this.data[e]))}addAll(e){Object.entries(e).forEach((([e,t])=>{this.add(e,t)}))}_createObject(e,t){return t[Symbol("isProxy")]=!0,new Proxy(t,{set:(t,a,s)=>{if(t[a]!==s){const i=t[a];t[a]=s,this._queueUpdate(e,"modified",t,a,i)}return!0}})}_defineProperty(e){Object.defineProperty(window,e,{get:()=>this.data[e],set:t=>{Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):this.data[e]="object"==typeof t?this._createObject(e,t):t,this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e),Array.isArray(t)||"object"==typeof t||this._queueUpdate(e,"modified",this.data[e])}})}_queueUpdate(e,t,a,s,i){this.pendingUpdates.has(e)||this.pendingUpdates.set(e,[]),this.pendingUpdates.get(e).push({action:t,item:a,property:s,oldValue:i}),1===this.pendingUpdates.get(e).length&&setTimeout((()=>{let t=this.pendingUpdates.get(e);this.updatePersistantData(e),this.pendingUpdates.delete(e),t=t.filter(((e,t,a)=>a.findIndex((t=>t.property===e.property&&t.action===e.action))===t)),t.forEach((t=>{this._triggerCallbacks(e,t)})),this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}),0)}_triggerCallbacks(e,t){this.callbacks[e]&&this.callbacks[e].forEach((a=>{const s=t.item||t.oldValue;a(this.data[e],s,t.action)}))}updateElement(e){const t=document.querySelectorAll(`[res="${e}"]`),a=this.data[e];t.forEach((t=>{if(t.value=a,"INPUT"===t.tagName||"TEXTAREA"===t.tagName)t.hasAttribute("data-resonant-bound")||(t.oninput=()=>{this.data[e]=t.value,this._queueUpdate(e,"modified",this.data[e])},t.setAttribute("data-resonant-bound","true"));else if(Array.isArray(a))t.querySelectorAll(`[res="${e}"][res-rendered=true]`).forEach((e=>e.remove())),this._renderArray(e,t);else if("object"==typeof a){t.querySelectorAll("[res-prop]").forEach((t=>{const s=t.getAttribute("res-prop");s&&s in a&&(t.hasAttribute("data-resonant-bound")?"INPUT"===t.tagName||"TEXTAREA"===t.tagName?"checkbox"===t.type?t.checked=a[s]:t.value=a[s]:t.innerHTML=a[s]:("INPUT"===t.tagName||"TEXTAREA"===t.tagName?"checkbox"===t.type?(t.checked=a[s],t.onchange=()=>{this.data[e][s]=t.checked}):(t.value=a[s],t.oninput=()=>{this.data[e][s]=t.value}):t.innerHTML=a[s],t.setAttribute("data-resonant-bound","true")))}))}else t.innerHTML=a})),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}updateDisplayConditionalsFor(variableName){const conditionalElements=document.querySelectorAll(`[res-display*="${variableName}"]`);conditionalElements.forEach((conditionalElement=>{const condition=conditionalElement.getAttribute("res-display");try{eval(condition)?conditionalElement.style.display="":conditionalElement.style.display="none"}catch(e){console.error(`Error evaluating condition for ${variableName}: ${condition}`,e)}}))}updateStylesFor(variableName){const styleElements=document.querySelectorAll(`[res-style*="${variableName}"]`);styleElements.forEach((styleElement=>{let styleCondition=styleElement.getAttribute("res-style");try{let parent=styleElement,index=null;for(;parent&&!index;)index=parent.getAttribute("res-index"),parent=parent.parentElement;let resStyles=styleElement.getAttribute("res-styles");if(resStyles){let e=resStyles.split(" ");e.forEach((e=>{styleElement.classList.remove(e)}))}if(null!==index){const e=this.data[variableName][index];styleCondition=styleCondition.replace(new RegExp(`\\b${variableName}\\b`,"g"),"item");const t=new Function("item",`return ${styleCondition}`)(e);if(t)styleElement.classList.add(t);else{var elementHasStyle=styleElement.classList.contains(t);elementHasStyle&&styleElement.classList.remove(t)}}else{const styleClass=eval(styleCondition);if(styleClass)styleElement.classList.add(styleClass),styleElement.setAttribute("res-styles",styleClass);else{var elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}}catch(e){console.error(`Error evaluating style for ${variableName}: ${styleCondition}`,e)}}))}_renderArray(e,t){let a=t.cloneNode(!0);t.innerHTML="",window[e+"_template"]?a=window[e+"_template"]:window[e+"_template"]=a,this.data[e].forEach(((s,i)=>{const n=a.cloneNode(!0);n.setAttribute("res-index",i);for(let t in s){let a=null,i=n.querySelector(`[res-prop="${t}"]`);i||(i=n.querySelector('[res-prop=""]'),a=s),i&&(i.hasAttribute("data-resonant-bound")?"INPUT"===i.tagName||"TEXTAREA"===i.tagName?"checkbox"===i.type?i.checked=a??s[t]:i.value=a??s[t]:i.innerHTML=a??s[t]:("INPUT"===i.tagName||"TEXTAREA"===i.tagName?"checkbox"===i.type?(i.checked=a??s[t],i.onchange=()=>{s[t]=i.checked,this._queueUpdate(e,"modified",s,t,a??s[t])}):(i.value=a??s[t],i.oninput=()=>{s[t]=i.value,this._queueUpdate(e,"modified",s,t,a??s[t])}):i.innerHTML=a??s[t],i.setAttribute("data-resonant-bound","true")))}n.querySelectorAll("[res-onclick], [res-onclick-remove]").forEach((t=>{const a=t.getAttribute("res-onclick"),i=t.getAttribute("res-onclick-remove");a&&(t.onclick=()=>{new Function("item",`return ${a}(item)`)(s)}),i&&(t.onclick=()=>{const t=this.data[e].findIndex((e=>e[i]===s[i]));-1!==t&&this.data[e].splice(t,1)})})),n.setAttribute("res-rendered",!0),t.appendChild(n)}))}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}
1
+ class ObservableArray extends Array{constructor(e,t,...r){if(void 0===t)return super(...r);super(...r);var s=void 0===t.data[e];this.variableName=e,this.resonantInstance=t,this.isDeleting=!1,this.forEach((e,t)=>{"object"==typeof e&&(this._setKeyForObjectAndChildObjects(e,t),this[t]=this._createProxy(e,t))}),s||this.forceUpdate()}_setKeyForObjectAndChildObjects(e,t){"object"==typeof e&&(e.key=t+"-"+this._generateKeyByContentCheckSum(e),Object.keys(e).forEach(r=>{this._setKeyForObjectAndChildObjects(e[r],t)}))}_generateKeyByContentCheckSum(e){let t=e=>{if(!e||"object"!=typeof e)return e;if(Array.isArray(e))return e.map(e=>t(e));let r={...e};return delete r.key,Object.keys(r).forEach(e=>{"object"==typeof r[e]&&null!==r[e]&&(r[e]=t(r[e]))}),r},r=t(e),s=JSON.stringify(r),a=0;for(let i=0;i<s.length;i++){let n=s.charCodeAt(i);a=(a<<5)-a+n,a&=a}return Math.abs(a).toString(36)}_createProxy(e,t){return new Proxy(e,{set:(r,s,a)=>{if(r[s]!==a){let i=r[s];r[s]=a,this._setKeyForObjectAndChildObjects(e,t),this.resonantInstance._queueUpdate(this.variableName,"modified",r,s,i,t)}return!0}})}forceUpdate(){this.forEach((e,t)=>{console.log("forceUpdate",e,t),this._setKeyForObjectAndChildObjects(e,t)}),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"modified",this.slice())}update(e){window[this.variableName]=e,this.resonantInstance._queueUpdate(this.variableName,"updated",e)}push(...e){e=e.map((e,t)=>"object"==typeof e?(this._setKeyForObjectAndChildObjects(e,this.length+t),this._createProxy(e,this.length+t)):e);let t=super.push(...e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),e.forEach((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"added",e,this.length-1-t)}),t}splice(e,t,...r){r=r.map((t,r)=>"object"==typeof t?this._createProxy(t,e+r):t);let s=super.splice(e,t,...r);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),t>0&&s.forEach((t,r)=>{this.resonantInstance._queueUpdate(this.variableName,"removed",t,e+r)}),r.length>0&&r.forEach((t,r)=>{this.resonantInstance._queueUpdate(this.variableName,"added",t,e+r)}),s}set(e,t){if(this[e]!==t){if(this.isDeleting)return!0;let r=this.resonantInstance.arrayDataChangeDetection[this.variableName],s="modified";e>=r.length?s="added":void 0===r[e]&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();let a=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,a)}return!0}delete(e){let t=this[e];return this.isDeleting=!0,this.splice(e,1),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"removed",null,e,t),this.isDeleting=!1,!0}filter(e,t=!0){if(void 0===this.resonantInstance||!1===t)return super.filter(e);let r=super.filter(e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"filtered"),r}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}_handleInputElement(e,t,r){"checkbox"===e.type?(e.checked=t,e.hasAttribute("data-resonant-bound")||(e.onchange=()=>r(e.checked),e.setAttribute("data-resonant-bound","true"))):(e.value=t,e.hasAttribute("data-resonant-bound")||(e.oninput=()=>r(e.value),e.setAttribute("data-resonant-bound","true")))}add(e,t,r){Array.isArray(t=this.persist(e,t,r))?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):"object"==typeof t&&null!==t?this.data[e]=this._createObject(e,t):this.data[e]=t,this._defineProperty(e),this.updateElement(e)}persist(e,t,r){if(void 0===r||!r)return t;var s=localStorage.getItem("res_"+e);return null!=s?JSON.parse(s):(localStorage.setItem("res_"+e,JSON.stringify(t)),t)}updatePersistantData(e){localStorage.getItem("res_"+e)&&localStorage.setItem("res_"+e,JSON.stringify(this.data[e]))}addAll(e){Object.entries(e).forEach(([e,t])=>{this.add(e,t)})}_resolveValue(e,t,r=null){return r??e[t]}_createObject(e,t){return t[Symbol("isProxy")]=!0,new Proxy(t,{set:(t,r,s)=>{if(t[r]!==s){let a=t[r];t[r]=s,this._queueUpdate(e,"modified",t,r,a)}return!0}})}_evaluateDisplayCondition(e,t,r){try{let s=Function("item",`return ${r}`)(t);e.style.display=s?"":"none"}catch(a){console.error(`Error evaluating display condition: ${r}`,a)}}_handleDisplayElements(e,t){let r=e.querySelectorAll("[res-display]");r.forEach(e=>{let r=e.getAttribute("res-display")||"";this._evaluateDisplayCondition(e,t,r)})}_bindClickEvents(e,t,r){let s=e.querySelectorAll("[res-onclick], [res-onclick-remove]");s.forEach(e=>{let s=e.getAttribute("res-onclick"),a=e.getAttribute("res-onclick-remove");s&&(e.onclick=()=>{let e=Function("item",`return ${s}(item)`);e(t)}),a&&(e.onclick=()=>{if(r){let e=r.findIndex(e=>e[a]===t[a]);-1!==e&&r.splice(e,1)}})})}_defineProperty(e){Object.defineProperty(window,e,{get:()=>this.data[e],set:t=>{Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):"object"==typeof t&&null!==t?this.data[e]=this._createObject(e,t):this.data[e]=t,this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e),Array.isArray(t)||"object"==typeof t&&null!==t||this._queueUpdate(e,"modified",this.data[e])}})}_queueUpdate(e,t,r,s,a){this.pendingUpdates.has(e)||this.pendingUpdates.set(e,[]),this.pendingUpdates.get(e).push({action:t,item:r,property:s,oldValue:a}),1===this.pendingUpdates.get(e).length&&setTimeout(()=>{let t=this.pendingUpdates.get(e);this.updatePersistantData(e),this.pendingUpdates.delete(e),(t=t.filter((e,t,r)=>r.findIndex(t=>t.property===e.property&&t.action===e.action)===t)).forEach(t=>{this._triggerCallbacks(e,t)}),this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)},0)}_triggerCallbacks(e,t){this.callbacks[e]&&this.callbacks[e].forEach(r=>{let s=t.item||t.oldValue;r(this.data[e],s,t.action)})}updateElement(e){let t=document.querySelectorAll(`[res="${e}"]`),r=this.data[e];t.forEach(t=>{if("INPUT"===t.tagName||"TEXTAREA"===t.tagName)this._handleInputElement(t,r,t=>{this.data[e]=t,this._queueUpdate(e,"modified",this.data[e])});else if(Array.isArray(r))t.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>e.remove()),this._renderArray(e,t);else if("object"==typeof r&&null!==r){let s=t.querySelectorAll("[res-prop]");s.forEach(t=>{let s=t.getAttribute("res-prop");s&&s in r&&this._renderObjectProperty(t,r[s],e,s)})}else t.innerHTML=r??""}),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}_renderObjectProperty(e,t,r,s){if("INPUT"!==e.tagName&&"TEXTAREA"!==e.tagName||Array.isArray(t)||"object"==typeof t){if(Array.isArray(t))this._renderNestedArray(e,t);else if("object"==typeof t&&null!==t){let a=e.querySelectorAll("[res-prop]");a.forEach(e=>{let s=e.getAttribute("res-prop");s&&s in t&&this._renderObjectProperty(e,t[s],r,s)})}else e.innerHTML=t??""}else this._handleInputElement(e,t,e=>{window[r][s]=e})}_renderArray(e,t){let r=t.hasAttribute("res")&&t.parentElement?t.parentElement:t,s;window[e+"_template"]?s=window[e+"_template"]:(s=t.cloneNode(!0),window[e+"_template"]=s,t.style.display="none",t.setAttribute("res-template","true"));let a=new Map;r.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>{let t=e.getAttribute("res-key");t&&a.set(t,e)}),r.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>e.remove()),this.data[e].forEach((t,i)=>{let n=t.key,l;if(a.has(n))l=a.get(n),a.delete(n),l.setAttribute("res-index",i);else for(let o in(l=s.cloneNode(!0)).removeAttribute("res-template"),l.style.display="",l.setAttribute("res-rendered","true"),l.setAttribute("res-key",n),l.setAttribute("res-index",i),t){let h=null,d=l.querySelector(`[res-prop="${o}"]`);if(d||(d=l.querySelector('[res-prop=""]'),h=t),d){let c=this._resolveValue(t,o,h);if("INPUT"!==d.tagName&&"TEXTAREA"!==d.tagName||Array.isArray(c)||"object"==typeof c){if(Array.isArray(c))this._renderNestedArray(d,c);else if("object"==typeof c&&null!==c){let u=d.querySelectorAll("[res-prop]");u.forEach(t=>{let r=t.getAttribute("res-prop");r&&r in c&&this._renderObjectProperty(t,c[r],e,r)})}else d.innerHTML=c??""}else this._handleInputElement(d,c,r=>{t[o]=r,this._queueUpdate(e,"modified",t,o,c)})}}this._handleDisplayElements(l,t),this._bindClickEvents(l,t,this.data[e]),r.appendChild(l)})}_renderNestedArray(e,t){let r=e.cloneNode(!0);e.innerHTML="",e.removeAttribute("res-prop"),t.forEach((s,a)=>{let i=r.cloneNode(!0);i.setAttribute("res-rendered","true");let n=i.querySelectorAll("[res-prop]");n.forEach(e=>{let t=e.getAttribute("res-prop");t&&t in s&&this._renderObjectProperty(e,s[t],null,t)}),this._handleDisplayElements(i,s),this._bindClickEvents(i,s,t),e.appendChild(i)})}updateDisplayConditionalsFor(e){let t=document.querySelectorAll(`[res-display*="${e}"]`);t.forEach(t=>{let r=t.getAttribute("res-display");this._evaluateDisplayCondition(t,this.data[e],r)})}updateStylesFor(variableName){let styleElements=document.querySelectorAll(`[res-style*="${variableName}"]`);styleElements.forEach(styleElement=>{let styleCondition=styleElement.getAttribute("res-style");try{let parent=styleElement,index=null;for(;parent&&!index;)index=parent.getAttribute("res-index"),parent=parent.parentElement;let resStyles=styleElement.getAttribute("res-styles");if(resStyles&&resStyles.split(" ").forEach(e=>{styleElement.classList.remove(e)}),null!==index){let item=this.data[variableName][index];styleCondition=styleCondition.replace(RegExp(`\\b${variableName}\\b`,"g"),"item");let styleClass=Function("item",`return ${styleCondition}`)(item);if(styleClass)styleElement.classList.add(styleClass);else{let elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}else{let styleClass=eval(styleCondition);if(styleClass)styleElement.classList.add(styleClass),styleElement.setAttribute("res-styles",styleClass);else{let elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}}catch(e){console.error(`Error evaluating style for ${variableName}: ${styleCondition}`,e)}})}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}