resonantjs 1.1.2 → 1.1.4

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.
@@ -102,8 +102,8 @@
102
102
  <section class="grid">
103
103
  <div>
104
104
  <h2>Project Members</h2>
105
- <ul res="projectTeam">
106
- <li>
105
+ <ul>
106
+ <li res="projectTeam">
107
107
  <span res-prop="name"></span> - <span res-prop="role"></span>
108
108
  </li>
109
109
  </ul>
@@ -207,6 +207,8 @@
207
207
  ]
208
208
  });
209
209
 
210
+
211
+
210
212
  // Chain together callbacks
211
213
  resonantJs.addCallback("user", (user) => {
212
214
  console.log(`User updated: ${user.firstname} ${user.lastname}`);
@@ -0,0 +1,452 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Resonant.js Practical Demo</title>
5
+ <link
6
+ rel="stylesheet"
7
+ href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css"
8
+ />
9
+ <script src="../resonant.js"></script>
10
+ <style>
11
+ .success {
12
+ background-color: #0172ad;
13
+ }
14
+ .fail {
15
+ background-color: #883935;
16
+ }
17
+ section.grid {
18
+ padding: 1rem;
19
+ margin-bottom: 2rem;
20
+ border: 1px solid #e0e0e0;
21
+ border-radius: 0.5rem;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+
27
+ <main class="container">
28
+ <h1>Variable/Object/Array update tests</h1>
29
+ <section class="grid">
30
+ <div>
31
+ <h2>Basic Variable: <span res="basicVariable"></span></h2>
32
+ <div res-display="basicVariable == 'Initial'">
33
+ Has not been updated yet
34
+ </div>
35
+ <div res-display="basicVariable != 'Initial'">
36
+ Has been updated
37
+ </div>
38
+ </div>
39
+ <div>
40
+ <button class="runTest" data-type="basicVariable">Run Test</button>
41
+ </div>
42
+ </section>
43
+ <section class="grid">
44
+ <div res="simpleObject">
45
+ <h2>Simple Object: </h2>
46
+ First Name: <span res-prop="firstName"></span>
47
+ Last Name: <span res-prop="lastName"></span>
48
+
49
+ <div res-display="firstName == 'John'">
50
+ Has not been updated yet
51
+ </div>
52
+ <div res-display="firstName != 'John'">
53
+ Has been updated
54
+ </div>
55
+ </div>
56
+ <div>
57
+ <button class="runTest" data-type="simpleObject">Run Test</button>
58
+ </div>
59
+ </section>
60
+ <section class="grid">
61
+ <div>
62
+ <h2>Simple Array Update: </h2>
63
+ <ul>
64
+ <li res="simpleArrayUpdate">
65
+ <span res-prop="firstName"></span> <span res-prop="lastName"></span>
66
+ </li>
67
+ </ul>
68
+
69
+ <div res-display="simpleArrayUpdate[0].firstName == 'John'">
70
+ Has not been updated yet
71
+ </div>
72
+ <div res-display="simpleArrayUpdate[0].firstName != 'John'">
73
+ Has been updated
74
+ </div>
75
+ </div>
76
+ <div>
77
+ <button class="runTest" data-type="simpleArrayUpdate">Run Test</button>
78
+ </div>
79
+ </section>
80
+ <section class="grid">
81
+ <div>
82
+ <h2>Simple Array Add: </h2>
83
+ <ul>
84
+ <li res="simpleArrayAdd">
85
+ <span res-prop="firstName"></span> <span res-prop="lastName"></span>
86
+ </li>
87
+ </ul>
88
+
89
+ <div res-display="simpleArrayAdd[2] === undefined">
90
+ Has not been updated yet
91
+ </div>
92
+ <div res-display="simpleArrayAdd[2] !== undefined">
93
+ Has been updated
94
+ </div>
95
+ </div>
96
+ <div>
97
+ <button class="runTest" data-type="simpleArrayAdd">Run Test</button>
98
+ </div>
99
+ </section>
100
+ <section class="grid">
101
+ <div>
102
+ <h2>Simple Array Remove: </h2>
103
+ <ul>
104
+ <li res="simpleArrayRemove">
105
+ <span res-prop="firstName"></span> <span res-prop="lastName"></span>
106
+ </li>
107
+ </ul>
108
+
109
+ <div res-display="simpleArrayRemove[0].firstName == 'John'">
110
+ Has not been updated yet
111
+ </div>
112
+ <div res-display="simpleArrayRemove[0].firstName != 'John'">
113
+ Has been updated
114
+ </div>
115
+ </div>
116
+ <div>
117
+ <button class="runTest" data-type="simpleArrayRemove">Run Test</button>
118
+ </div>
119
+ </section>
120
+ <section class="grid">
121
+ <div>
122
+ <h2>Complex Array With Children Update: </h2>
123
+ <hr>
124
+ <div>
125
+ <div res="complexArrayWithChildrenUpdate">
126
+ <h3><span res-prop="firstName"></span> <span res-prop="lastName"></span></h3>
127
+
128
+ Phone Numbers:
129
+ <ul>
130
+ <li res-prop="phoneNumbers">
131
+ </li>
132
+ </ul>
133
+ Addresses
134
+ <ul>
135
+ <li res-prop="addresses">
136
+ <span res-prop="city"></span>,
137
+ <span res-prop="state"></span>
138
+ </li>
139
+ </ul>
140
+ <hr>
141
+ </div>
142
+ </div>
143
+
144
+
145
+ <div res-display="complexArrayWithChildrenUpdate[0].firstName == 'John'">
146
+ Has not been updated yet
147
+ </div>
148
+ <div res-display="complexArrayWithChildrenUpdate[0].firstName != 'John'">
149
+ Has been updated
150
+ </div>
151
+ </div>
152
+ <div>
153
+ <button class="runTest" data-type="complexArrayWithChildrenUpdate">Run Test</button>
154
+ </div>
155
+ </section>
156
+ <section class="grid">
157
+ <div>
158
+ <h2>Complex Array With Children Display Update: </h2>
159
+ <hr>
160
+ <div>
161
+ <div res="complexArrayWithChildrenDisplayUpdate">
162
+ <h3><span res-prop="firstName"></span> <span res-prop="lastName"></span></h3>
163
+
164
+ <div res-display="attributes.showOnPage">
165
+ Show on page
166
+ </div>
167
+ <hr>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ <div>
172
+ <button class="runTest" data-type="complexArrayWithChildrenDisplayUpdate">Run Test</button>
173
+ </div>
174
+ </section>
175
+ <section class="grid">
176
+ <div>
177
+ <h2>Complex Array With Arrays Display Test: </h2>
178
+ <hr>
179
+ <div>
180
+ <div res="complexArrayWithArrayDisplayUpdate">
181
+ <h3>
182
+ <span res-prop="firstName"></span>
183
+ <span res-prop="lastName"></span>
184
+ </h3>
185
+
186
+ <div res-prop="attributes">
187
+ <div res-prop="name">
188
+ </div>
189
+ <div res-display="showOnPage">
190
+ Show on page
191
+ </div>
192
+ </div>
193
+ <hr>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ <div>
198
+ <button class="runTest" data-type="complexArrayWithArrayDisplayUpdate">Run Test</button>
199
+ </div>
200
+ </section>
201
+ <button onclick="runAllTests()">Run All Tests</button>
202
+ </main>
203
+
204
+ <script>
205
+ const resonantJs = new Resonant();
206
+ resonantJs.add("basicVariable", "Initial");
207
+ resonantJs.add("simpleObject", {
208
+ firstName: "John",
209
+ lastName: "Doe"
210
+ });
211
+ resonantJs.add("simpleArrayUpdate", [
212
+ {
213
+ firstName: "John",
214
+ lastName: "Doe"
215
+ },
216
+ {
217
+ firstName: "Jane",
218
+ lastName: "Doe"
219
+ }
220
+ ]);
221
+ resonantJs.add("simpleArrayAdd", [
222
+ {
223
+ firstName: "John",
224
+ lastName: "Doe"
225
+ },
226
+ {
227
+ firstName: "Jane",
228
+ lastName: "Doe"
229
+ }
230
+ ]);
231
+ resonantJs.add("simpleArrayRemove", [
232
+ {
233
+ firstName: "John",
234
+ lastName: "Doe"
235
+ },
236
+ {
237
+ firstName: "Jane",
238
+ lastName: "Doe"
239
+ }
240
+ ]);
241
+ resonantJs.add("complexArrayWithChildrenUpdate", [
242
+ {
243
+ firstName: "Michael",
244
+ lastName: "Smith",
245
+ phoneNumbers: [
246
+ "555-123-4567",
247
+ "555-987-6543"
248
+ ],
249
+ addresses: [
250
+ {
251
+ city: "Chicago",
252
+ state: "IL"
253
+ },
254
+ {
255
+ city: "Seattle",
256
+ state: "WA"
257
+ },
258
+ {
259
+ city: "New York",
260
+ state: "NY"
261
+ }
262
+ ]
263
+ },
264
+ {
265
+ firstName: "Sarah",
266
+ lastName: "Johnson",
267
+ phoneNumbers: [
268
+ "555-234-5678",
269
+ "555-876-5432",
270
+ "555-345-6789"
271
+ ],
272
+ addresses: [
273
+ {
274
+ city: "Boston",
275
+ state: "MA"
276
+ },
277
+ {
278
+ city: "Austin",
279
+ state: "TX"
280
+ }
281
+ ]
282
+ }
283
+ ]);
284
+ resonantJs.add("complexArrayWithChildrenDisplayUpdate", [
285
+ {
286
+ firstName: "Michael",
287
+ lastName: "Smith",
288
+ attributes:
289
+ {
290
+ showOnPage: true
291
+ }
292
+ },
293
+ {
294
+ firstName: "Sarah",
295
+ lastName: "Johnson",
296
+ attributes:
297
+ {
298
+ showOnPage: true
299
+ }
300
+ }
301
+ ]);
302
+ resonantJs.add("complexArrayWithArrayDisplayUpdate", [
303
+ {
304
+ firstName: "Michael",
305
+ lastName: "Smith",
306
+ attributes: [
307
+ {
308
+ name: "Personal",
309
+ showOnPage: true
310
+ },
311
+ {
312
+ name: "Not Personal",
313
+ showOnPage: true
314
+ }]
315
+ }
316
+ ]);
317
+
318
+ function runAllTests() {
319
+ let tests = document.querySelectorAll(".runTest");
320
+ tests.forEach(test => {
321
+ test.click();
322
+ });
323
+ }
324
+
325
+ function runTest() {
326
+
327
+ //hide button
328
+ this.style.display = "none";
329
+
330
+ let parentElement = this.parentElement.parentElement;
331
+ let dataAttribute = this.getAttribute("data-type");
332
+ let expectedText = "";
333
+
334
+ console.log("Parent element inner text: ", parentElement.innerText);
335
+
336
+ if(dataAttribute === "basicVariable") {
337
+ basicVariable = "Updated";
338
+
339
+ expectedText = `Basic Variable: Updated
340
+ Has been updated`;
341
+ }
342
+
343
+ if(dataAttribute === "simpleObject") {
344
+ simpleObject.firstName = "Jane";
345
+ simpleObject.lastName = "Doe";
346
+
347
+ expectedText = `Simple Object:
348
+ First Name: Jane
349
+ Last Name: Doe
350
+ Has been updated`;
351
+ }
352
+
353
+ if(dataAttribute === "simpleArrayUpdate") {
354
+ simpleArrayUpdate[0].firstName = "Josh";
355
+ simpleArrayUpdate[0].lastName = "Forger";
356
+ simpleArrayUpdate[1].firstName = "Matilda";
357
+ simpleArrayUpdate[1].lastName = "Swinson";
358
+
359
+ expectedText = `Simple Array Update:
360
+ Josh Forger
361
+ Matilda Swinson
362
+ Has been updated`;
363
+ }
364
+
365
+ if(dataAttribute === "simpleArrayAdd") {
366
+ simpleArrayAdd.push({
367
+ firstName: "Josh",
368
+ lastName: "Forger"
369
+ });
370
+
371
+ expectedText = `Simple Array Add:
372
+ John Doe
373
+ Jane Doe
374
+ Josh Forger
375
+ Has been updated`;
376
+ }
377
+
378
+ if(dataAttribute === "simpleArrayRemove") {
379
+ simpleArrayRemove.splice(0, 1);
380
+
381
+ expectedText = `Simple Array Remove:
382
+ Jane Doe
383
+ Has been updated`;
384
+ }
385
+
386
+ if(dataAttribute === "complexArrayWithChildrenUpdate") {
387
+ complexArrayWithChildrenUpdate[0].firstName = "Josh";
388
+ complexArrayWithChildrenUpdate[0].lastName = "Forger";
389
+ complexArrayWithChildrenUpdate[0].phoneNumbers[0] = "123-123-1234";
390
+ complexArrayWithChildrenUpdate[0].addresses[1].city = "Hudson";
391
+ complexArrayWithChildrenUpdate[0].addresses[1].state = "OH";
392
+
393
+ expectedText = `Complex Array With Children Update:
394
+ Josh Forger
395
+ Phone Numbers:
396
+ 123-123-1234
397
+ 555-987-6543
398
+ Addresses
399
+ Chicago, IL
400
+ Hudson, OH
401
+ New York, NY
402
+ Sarah Johnson
403
+ Phone Numbers:
404
+ 555-234-5678
405
+ 555-876-5432
406
+ 555-345-6789
407
+ Addresses
408
+ Boston, MA
409
+ Austin, TX
410
+ Has been updated
411
+ `;
412
+ }
413
+
414
+ if(dataAttribute === "complexArrayWithChildrenDisplayUpdate") {
415
+ complexArrayWithChildrenDisplayUpdate[0].attributes.showOnPage = false;
416
+ // requires a force update unfortunately
417
+ complexArrayWithChildrenDisplayUpdate.forceUpdate();
418
+
419
+ expectedText = `Complex Array With Children Display Update:
420
+ Michael Smith
421
+ Sarah Johnson
422
+ Show on page
423
+ `;
424
+ }
425
+
426
+ setTimeout(() => {
427
+ visibleTextCompare(parentElement,expectedText);
428
+ }, 1000)
429
+ }
430
+
431
+ document.querySelectorAll(".runTest").forEach(test => {
432
+ test.addEventListener("click", runTest);
433
+ });
434
+
435
+ function visibleTextCompare(element, expectedText) {
436
+ let visibleText = element.innerText;
437
+
438
+ visibleText = visibleText.replace(/\s/g, "").replace("RunTest", "");
439
+ expectedText = expectedText.replace(/\s/g, "").replace("RunTest", "");
440
+
441
+ let result = visibleText === expectedText;
442
+
443
+ if(!result) {
444
+ console.log("Visible text: ", visibleText);
445
+ console.log("Expected text: ", expectedText);
446
+ }
447
+
448
+ element.classList.add(result ? "success" : "fail");
449
+ }
450
+ </script>
451
+ </body>
452
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resonantjs",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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
@@ -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;
@@ -36,6 +78,9 @@ class ObservableArray extends Array {
36
78
  }
37
79
 
38
80
  forceUpdate() {
81
+ this.forEach((item, index) => {
82
+ this._setKeyForObjectAndChildObjects(item, index);
83
+ });
39
84
  this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
40
85
  this.resonantInstance._queueUpdate(this.variableName, 'modified', this.slice());
41
86
  }
@@ -48,6 +93,7 @@ class ObservableArray extends Array {
48
93
  push(...args) {
49
94
  args = args.map((item, index) => {
50
95
  if (typeof item === 'object') {
96
+ this._setKeyForObjectAndChildObjects(item, this.length + index);
51
97
  return this._createProxy(item, this.length + index);
52
98
  }
53
99
  return item;
@@ -212,10 +258,60 @@ class Resonant {
212
258
  });
213
259
  }
214
260
 
215
- _evaluateDisplayCondition(element, item, condition) {
261
+ _evaluateDisplayCondition(element, instance, condition) {
216
262
  try {
217
- const show = new Function('item', `return ${condition}`)(item);
218
- element.style.display = show ? '' : 'none';
263
+ let parent = element.parentElement;
264
+ let parentResName = null;
265
+ let arrayIndex = null;
266
+
267
+ while (parent && !parentResName) {
268
+ parentResName = parent.getAttribute('res');
269
+ if (!arrayIndex) {
270
+ arrayIndex = parent.getAttribute('res-index');
271
+ }
272
+ parent = parent.parentElement;
273
+ }
274
+
275
+ // For array items, use the specific array element as instance
276
+ if (parentResName && arrayIndex !== null && Array.isArray(this.data[parentResName])) {
277
+ instance = this.data[parentResName][arrayIndex];
278
+ }
279
+
280
+ // For object properties without parent reference
281
+ if (parentResName && !condition.includes(parentResName + '.')) {
282
+ const propNames = condition.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) || [];
283
+ propNames.forEach(prop => {
284
+ if (prop === '==' || prop === '!=' || !(prop in instance)) return;
285
+
286
+ const regex = new RegExp(`\\b${prop}\\b`, 'g');
287
+ const propValue = instance[prop];
288
+
289
+ if (propValue === undefined) {
290
+ // Keep as is - property doesn't exist
291
+ return;
292
+ }
293
+
294
+ // Determine correct property access path
295
+ if (instance['item.' + prop] !== undefined) {
296
+ condition = condition.replace(regex, `item.${prop}`);
297
+ } else if (typeof propValue === 'object') {
298
+ condition = condition.replace(regex, `${parentResName}.${prop}`);
299
+ }
300
+ });
301
+ }
302
+
303
+ let show = false;
304
+ try{
305
+ show = new Function('item', `return ${condition}`)(instance);
306
+ } catch(e) {
307
+
308
+ const firstPeriodIndex = condition.indexOf('.');
309
+ const conditionAfterFirstPeriod = condition.substring(firstPeriodIndex + 1);
310
+ show = new Function('item', `return item.${conditionAfterFirstPeriod}`)(instance);
311
+
312
+ }
313
+
314
+ element.style.display = show ? 'inherit' : 'none';
219
315
  } catch (e) {
220
316
  console.error(`Error evaluating display condition: ${condition}`, e);
221
317
  }
@@ -314,9 +410,9 @@ class Resonant {
314
410
  }
315
411
 
316
412
  updateElement(variableName) {
413
+
317
414
  const elements = document.querySelectorAll(`[res="${variableName}"]`);
318
415
  const value = this.data[variableName];
319
-
320
416
  elements.forEach(element => {
321
417
  if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
322
418
  this._handleInputElement(element, value, (newValue) => {
@@ -355,7 +451,8 @@ class Resonant {
355
451
  });
356
452
  }
357
453
  else if (Array.isArray(propValue)) {
358
- this._renderNestedArray(subEl, propValue);
454
+ let parentKey = this.data[parentVarName].key;
455
+ this._renderNestedArray(subEl, propValue, parentKey);
359
456
  }
360
457
  else if (typeof propValue === 'object' && propValue !== null) {
361
458
  const nestedElements = subEl.querySelectorAll('[res-prop]');
@@ -372,68 +469,93 @@ class Resonant {
372
469
  }
373
470
 
374
471
  _renderArray(variableName, el) {
375
- let template = el.cloneNode(true);
376
- el.innerHTML = '';
472
+ const container = el.hasAttribute('res') && el.parentElement ? el.parentElement : el;
377
473
 
474
+ let template;
378
475
  if (!window[variableName + "_template"]) {
476
+ template = el.cloneNode(true);
379
477
  window[variableName + "_template"] = template;
478
+ el.style.display = 'none';
479
+ el.setAttribute('res-template', 'true');
380
480
  } else {
381
481
  template = window[variableName + "_template"];
382
482
  }
383
483
 
484
+ const existingElements = new Map();
485
+ container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(element => {
486
+ const key = element.getAttribute('res-key');
487
+ if (key) {
488
+ existingElements.set(key, element);
489
+ }
490
+ });
491
+
492
+ container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
493
+
384
494
  this.data[variableName].forEach((instance, index) => {
385
- const clonedEl = template.cloneNode(true);
386
- clonedEl.setAttribute("res-index", index);
387
-
388
- for (let key in instance) {
389
- let overrideInstanceValue = null;
390
- let subEl = clonedEl.querySelector(`[res-prop="${key}"]`);
391
- if (!subEl) {
392
- subEl = clonedEl.querySelector('[res-prop=""]');
393
- overrideInstanceValue = instance;
394
- }
395
- if (subEl) {
396
- const value = this._resolveValue(instance, key, overrideInstanceValue);
397
-
398
- if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
399
- !Array.isArray(value) &&
400
- typeof value !== 'object') {
401
- this._handleInputElement(
402
- subEl,
403
- value,
404
- (newValue) => {
405
- instance[key] = newValue;
406
- this._queueUpdate(variableName, 'modified', instance, key, value);
407
- }
408
- );
409
- }
410
- else if (Array.isArray(value)) {
411
- this._renderNestedArray(subEl, value);
495
+ const elementKey = instance.key;
496
+ let elementToUse;
497
+
498
+ if (existingElements.has(elementKey)) {
499
+ elementToUse = existingElements.get(elementKey);
500
+ existingElements.delete(elementKey);
501
+ elementToUse.setAttribute("res-index", index);
502
+ } else {
503
+ elementToUse = template.cloneNode(true);
504
+ elementToUse.removeAttribute('res-template');
505
+ elementToUse.style.display = '';
506
+ elementToUse.setAttribute("res-rendered", "true");
507
+ elementToUse.setAttribute("res-key", elementKey);
508
+ elementToUse.setAttribute("res-index", index);
509
+
510
+ for (let key in instance) {
511
+ let overrideInstanceValue = null;
512
+ let subEl = elementToUse.querySelector(`[res-prop="${key}"]`);
513
+ if (!subEl) {
514
+ subEl = elementToUse.querySelector('[res-prop=""]');
515
+ overrideInstanceValue = instance;
412
516
  }
413
- else if (typeof value === 'object' && value !== null) {
414
- const nestedElements = subEl.querySelectorAll('[res-prop]');
415
- nestedElements.forEach(nestedEl => {
416
- const nestedKey = nestedEl.getAttribute('res-prop');
417
- if (nestedKey && nestedKey in value) {
418
- this._renderObjectProperty(nestedEl, value[nestedKey], variableName, nestedKey);
419
- }
420
- });
421
- }
422
- else {
423
- subEl.innerHTML = value ?? '';
517
+ if (subEl) {
518
+ const value = this._resolveValue(instance, key, overrideInstanceValue);
519
+
520
+ if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
521
+ !Array.isArray(value) &&
522
+ typeof value !== 'object') {
523
+ this._handleInputElement(
524
+ subEl,
525
+ value,
526
+ (newValue) => {
527
+ instance[key] = newValue;
528
+ this._queueUpdate(variableName, 'modified', instance, key, value);
529
+ }
530
+ );
531
+ }
532
+ else if (Array.isArray(value)) {
533
+ let parentKey = this.data[variableName][index].key;
534
+ this._renderNestedArray(subEl, value, parentKey);
535
+ }
536
+ else if (typeof value === 'object' && value !== null) {
537
+ const nestedElements = subEl.querySelectorAll('[res-prop]');
538
+ nestedElements.forEach(nestedEl => {
539
+ const nestedKey = nestedEl.getAttribute('res-prop');
540
+ if (nestedKey && nestedKey in value) {
541
+ this._renderObjectProperty(nestedEl, value[nestedKey], variableName, nestedKey);
542
+ }
543
+ });
544
+ }
545
+ else {
546
+ subEl.innerHTML = value ?? '';
547
+ }
424
548
  }
425
549
  }
426
550
  }
427
551
 
428
- this._handleDisplayElements(clonedEl, instance);
429
- this._bindClickEvents(clonedEl, instance, this.data[variableName]);
552
+ this._handleDisplayElements(elementToUse, instance);
553
+ this._bindClickEvents(elementToUse, instance, this.data[variableName]);
430
554
 
431
- clonedEl.setAttribute("res-rendered", "true");
432
- el.appendChild(clonedEl);
555
+ container.appendChild(elementToUse);
433
556
  });
434
557
  }
435
-
436
- _renderNestedArray(subEl, arrayValue) {
558
+ _renderNestedArray(subEl, arrayValue, parentKey) {
437
559
  const template = subEl.cloneNode(true);
438
560
  subEl.innerHTML = '';
439
561
 
@@ -443,17 +565,26 @@ class Resonant {
443
565
  const cloned = template.cloneNode(true);
444
566
  cloned.setAttribute('res-rendered', 'true');
445
567
 
446
- const nestedEls = cloned.querySelectorAll('[res-prop]');
447
- nestedEls.forEach(nestedEl => {
448
- const nestedKey = nestedEl.getAttribute('res-prop');
449
- if (nestedKey && nestedKey in item) {
450
- this._renderObjectProperty(nestedEl, item[nestedKey], null, nestedKey);
451
- }
452
- });
568
+ if (item !== null && typeof item !== 'object') {
569
+ cloned.innerHTML = item;
570
+ } else {
571
+ const nestedEls = cloned.querySelectorAll('[res-prop]');
572
+ nestedEls.forEach(nestedEl => {
573
+ const nestedKey = nestedEl.getAttribute('res-prop');
574
+ if (nestedKey && nestedKey in item) {
575
+ nestedEl.setAttribute('res-child', item.key + '-' + nestedKey + '--' + parentKey);
576
+ this._renderObjectProperty(nestedEl, item[nestedKey], null, nestedKey);
577
+ }
578
+ });
453
579
 
454
- this._handleDisplayElements(cloned, item);
455
- this._bindClickEvents(cloned, item, arrayValue);
580
+ this._handleDisplayElements(cloned, item);
581
+ this._bindClickEvents(cloned, item, arrayValue);
456
582
 
583
+ const displayCondition = cloned.getAttribute('res-display');
584
+ if (displayCondition) {
585
+ this._evaluateDisplayCondition(cloned, item, displayCondition);
586
+ }
587
+ }
457
588
  subEl.appendChild(cloned);
458
589
  });
459
590
  }
@@ -464,6 +595,12 @@ class Resonant {
464
595
  const condition = conditionalElement.getAttribute('res-display');
465
596
  this._evaluateDisplayCondition(conditionalElement, this.data[variableName], condition);
466
597
  });
598
+
599
+ const contextElements = document.querySelectorAll(`[res="${variableName}"] [res-display]`);
600
+ contextElements.forEach(conditionalElement => {
601
+ const condition = conditionalElement.getAttribute('res-display');
602
+ this._evaluateDisplayCondition(conditionalElement, this.data[variableName], condition);
603
+ });
467
604
  }
468
605
 
469
606
  updateStylesFor(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){let r=e[a];e[a]=s,this.resonantInstance._queueUpdate(this.variableName,"modified",e,a,r,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);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,...a){a=a.map((t,a)=>"object"==typeof t?this._createProxy(t,e+a):t);let 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;let a=this.resonantInstance.arrayDataChangeDetection[this.variableName],s="modified";e>=a.length?s="added":void 0===a[e]&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();let r=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,r)}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 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={}}_handleInputElement(e,t,a){"checkbox"===e.type?(e.checked=t,e.hasAttribute("data-resonant-bound")||(e.onchange=()=>a(e.checked),e.setAttribute("data-resonant-bound","true"))):(e.value=t,e.hasAttribute("data-resonant-bound")||(e.oninput=()=>a(e.value),e.setAttribute("data-resonant-bound","true")))}add(e,t,a){Array.isArray(t=this.persist(e,t,a))?(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,a){if(void 0===a||!a)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,a=null){return a??e[t]}_createObject(e,t){return t[Symbol("isProxy")]=!0,new Proxy(t,{set:(t,a,s)=>{if(t[a]!==s){let r=t[a];t[a]=s,this._queueUpdate(e,"modified",t,a,r)}return!0}})}_evaluateDisplayCondition(e,t,a){try{let s=Function("item",`return ${a}`)(t);e.style.display=s?"":"none"}catch(r){console.error(`Error evaluating display condition: ${a}`,r)}}_handleDisplayElements(e,t){let a=e.querySelectorAll("[res-display]");a.forEach(e=>{let a=e.getAttribute("res-display")||"";this._evaluateDisplayCondition(e,t,a)})}_bindClickEvents(e,t,a){let s=e.querySelectorAll("[res-onclick], [res-onclick-remove]");s.forEach(e=>{let s=e.getAttribute("res-onclick"),r=e.getAttribute("res-onclick-remove");s&&(e.onclick=()=>{let e=Function("item",`return ${s}(item)`);e(t)}),r&&(e.onclick=()=>{if(a){let e=a.findIndex(e=>e[r]===t[r]);-1!==e&&a.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,a,s,r){this.pendingUpdates.has(e)||this.pendingUpdates.set(e,[]),this.pendingUpdates.get(e).push({action:t,item:a,property:s,oldValue:r}),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)).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=>{let s=t.item||t.oldValue;a(this.data[e],s,t.action)})}updateElement(e){let t=document.querySelectorAll(`[res="${e}"]`),a=this.data[e];t.forEach(t=>{if("INPUT"===t.tagName||"TEXTAREA"===t.tagName)this._handleInputElement(t,a,t=>{this.data[e]=t,this._queueUpdate(e,"modified",this.data[e])});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&&null!==a){let s=t.querySelectorAll("[res-prop]");s.forEach(t=>{let s=t.getAttribute("res-prop");s&&s in a&&this._renderObjectProperty(t,a[s],e,s)})}else t.innerHTML=a??""}),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}_renderObjectProperty(e,t,a,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 r=e.querySelectorAll("[res-prop]");r.forEach(e=>{let s=e.getAttribute("res-prop");s&&s in t&&this._renderObjectProperty(e,t[s],a,s)})}else e.innerHTML=t??""}else this._handleInputElement(e,t,e=>{window[a][s]=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,r)=>{let i=a.cloneNode(!0);for(let n in i.setAttribute("res-index",r),s){let l=null,o=i.querySelector(`[res-prop="${n}"]`);if(o||(o=i.querySelector('[res-prop=""]'),l=s),o){let h=this._resolveValue(s,n,l);if("INPUT"!==o.tagName&&"TEXTAREA"!==o.tagName||Array.isArray(h)||"object"==typeof h){if(Array.isArray(h))this._renderNestedArray(o,h);else if("object"==typeof h&&null!==h){let d=o.querySelectorAll("[res-prop]");d.forEach(t=>{let a=t.getAttribute("res-prop");a&&a in h&&this._renderObjectProperty(t,h[a],e,a)})}else o.innerHTML=h??""}else this._handleInputElement(o,h,t=>{s[n]=t,this._queueUpdate(e,"modified",s,n,h)})}}this._handleDisplayElements(i,s),this._bindClickEvents(i,s,this.data[e]),i.setAttribute("res-rendered","true"),t.appendChild(i)})}_renderNestedArray(e,t){let a=e.cloneNode(!0);e.innerHTML="",e.removeAttribute("res-prop"),t.forEach((s,r)=>{let i=a.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 a=t.getAttribute("res-display");this._evaluateDisplayCondition(t,this.data[e],a)})}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)}}
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 l=s.charCodeAt(i);a=(a<<5)-a+l,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)=>{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=e.parentElement,a=null,i=null;for(;s&&!a;)a=s.getAttribute("res"),i||(i=s.getAttribute("res-index")),s=s.parentElement;if(a&&null!==i&&Array.isArray(this.data[a])&&(t=this.data[a][i]),a&&!r.includes(a+".")){let l=r.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g)||[];l.forEach(e=>{if("=="===e||"!="===e||!(e in t))return;let s=RegExp(`\\b${e}\\b`,"g"),i=t[e];void 0!==i&&(void 0!==t["item."+e]?r=r.replace(s,`item.${e}`):"object"==typeof i&&(r=r.replace(s,`${a}.${e}`)))})}let n=!1;try{n=Function("item",`return ${r}`)(t)}catch(o){let h=r.indexOf("."),d=r.substring(h+1);n=Function("item",`return item.${d}`)(t)}e.style.display=n?"inherit":"none"}catch(c){console.error(`Error evaluating display condition: ${r}`,c)}}_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)){let a=this.data[r].key;this._renderNestedArray(e,t,a)}else if("object"==typeof t&&null!==t){let i=e.querySelectorAll("[res-prop]");i.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 l=t.key,n;if(a.has(l))n=a.get(l),a.delete(l),n.setAttribute("res-index",i);else for(let o in(n=s.cloneNode(!0)).removeAttribute("res-template"),n.style.display="",n.setAttribute("res-rendered","true"),n.setAttribute("res-key",l),n.setAttribute("res-index",i),t){let h=null,d=n.querySelector(`[res-prop="${o}"]`);if(d||(d=n.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)){let u=this.data[e][i].key;this._renderNestedArray(d,c,u)}else if("object"==typeof c&&null!==c){let p=d.querySelectorAll("[res-prop]");p.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(n,t),this._bindClickEvents(n,t,this.data[e]),r.appendChild(n)})}_renderNestedArray(e,t,r){let s=e.cloneNode(!0);e.innerHTML="",e.removeAttribute("res-prop"),t.forEach((a,i)=>{let l=s.cloneNode(!0);if(l.setAttribute("res-rendered","true"),null!==a&&"object"!=typeof a)l.innerHTML=a;else{let n=l.querySelectorAll("[res-prop]");n.forEach(e=>{let t=e.getAttribute("res-prop");t&&t in a&&(e.setAttribute("res-child",a.key+"-"+t+"--"+r),this._renderObjectProperty(e,a[t],null,t))}),this._handleDisplayElements(l,a),this._bindClickEvents(l,a,t);let o=l.getAttribute("res-display");o&&this._evaluateDisplayCondition(l,a,o)}e.appendChild(l)})}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)});let r=document.querySelectorAll(`[res="${e}"] [res-display]`);r.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)}}