resonantjs 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/example-basic.html +116 -4
- package/package.json +1 -1
- package/resonant.js +205 -147
- package/resonant.min.js +1 -1
|
@@ -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="
|
|
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: <span res="counter"></span><br/><br/>
|
|
49
|
+
<button onclick="counter++">Increment Counter</button><br/><br/>
|
|
50
|
+
<div res-display="counter < 10">contents</div><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
|
+
user: {<br/>
|
|
85
|
+
firstname: "<span res-prop="firstname"></span>",<br/>
|
|
86
|
+
lastname: "<span res-prop="lastname"></span>"<br/>
|
|
87
|
+
}<br/>
|
|
88
|
+
});
|
|
89
|
+
</code>
|
|
90
|
+
<code class="code">
|
|
91
|
+
<div res="user"><br/>
|
|
92
|
+
<input type="text" res-prop="firstname" /><br/>
|
|
93
|
+
<input type="text" res-prop="lastname" /><br/>
|
|
94
|
+
<span res-prop="firstname"></span><br/>
|
|
95
|
+
<span res-prop="lastname"></span><br/>
|
|
96
|
+
</div>
|
|
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
|
-
|
|
116
|
+
</section>
|
|
117
|
+
|
|
118
|
+
<section class="grid">
|
|
119
|
+
<code class="code">
|
|
120
|
+
resonantJs.add({<br/>
|
|
121
|
+
projectTeam: [<br/>
|
|
122
|
+
{ name: "Alice", role: "Developer" },<br/>
|
|
123
|
+
{ name: "Bob", role: "Designer" }<br/>
|
|
124
|
+
]<br/>
|
|
125
|
+
});<br/><br/>
|
|
126
|
+
|
|
127
|
+
function addProjectMember() {<br/>
|
|
128
|
+
const newMember = { name: "Charlie", role: "Product Manager" };<br/>
|
|
129
|
+
projectTeam.push(newMember);<br/>
|
|
130
|
+
}
|
|
131
|
+
</code>
|
|
132
|
+
<code class="code">
|
|
133
|
+
<ul res="projectTeam"><br/>
|
|
134
|
+
<li><br/>
|
|
135
|
+
<span res-prop="name"></span> - <span res-prop="role"></span><br/>
|
|
136
|
+
</li><br/>
|
|
137
|
+
</ul><br/><br/>
|
|
138
|
+
<button onclick="addProjectMember()">Add Project Member</button>
|
|
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.
|
|
3
|
+
"version": "1.1.2",
|
|
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);
|
|
@@ -35,7 +35,6 @@ class ObservableArray extends Array {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//temp fix for issues
|
|
39
38
|
forceUpdate() {
|
|
40
39
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
41
40
|
this.resonantInstance._queueUpdate(this.variableName, 'modified', this.slice());
|
|
@@ -102,9 +101,9 @@ class ObservableArray extends Array {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
105
|
-
|
|
106
104
|
const oldValue = this[index];
|
|
107
105
|
this[index] = value;
|
|
106
|
+
|
|
108
107
|
this.resonantInstance._queueUpdate(this.variableName, action, this[index], index, oldValue);
|
|
109
108
|
}
|
|
110
109
|
return true;
|
|
@@ -114,9 +113,7 @@ class ObservableArray extends Array {
|
|
|
114
113
|
const oldValue = this[index];
|
|
115
114
|
this.isDeleting = true;
|
|
116
115
|
this.splice(index, 1);
|
|
117
|
-
|
|
118
116
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
119
|
-
|
|
120
117
|
this.resonantInstance._queueUpdate(this.variableName, 'removed', null, index, oldValue);
|
|
121
118
|
this.isDeleting = false;
|
|
122
119
|
return true;
|
|
@@ -126,7 +123,6 @@ class ObservableArray extends Array {
|
|
|
126
123
|
if(this.resonantInstance === undefined || actuallyFilter === false) {
|
|
127
124
|
return super.filter(filter);
|
|
128
125
|
}
|
|
129
|
-
|
|
130
126
|
const result = super.filter(filter);
|
|
131
127
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
132
128
|
this.resonantInstance._queueUpdate(this.variableName, 'filtered');
|
|
@@ -142,12 +138,28 @@ class Resonant {
|
|
|
142
138
|
this.arrayDataChangeDetection = {};
|
|
143
139
|
}
|
|
144
140
|
|
|
141
|
+
_handleInputElement(element, value, onChangeCallback) {
|
|
142
|
+
if (element.type === 'checkbox') {
|
|
143
|
+
element.checked = value;
|
|
144
|
+
if (!element.hasAttribute('data-resonant-bound')) {
|
|
145
|
+
element.onchange = () => onChangeCallback(element.checked);
|
|
146
|
+
element.setAttribute('data-resonant-bound', 'true');
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
element.value = value;
|
|
150
|
+
if (!element.hasAttribute('data-resonant-bound')) {
|
|
151
|
+
element.oninput = () => onChangeCallback(element.value);
|
|
152
|
+
element.setAttribute('data-resonant-bound', 'true');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
add(variableName, value, persist) {
|
|
146
158
|
value = this.persist(variableName, value, persist);
|
|
147
159
|
if (Array.isArray(value)) {
|
|
148
160
|
this.data[variableName] = new ObservableArray(variableName, this, ...value);
|
|
149
161
|
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
150
|
-
} else if (typeof value === 'object') {
|
|
162
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
151
163
|
this.data[variableName] = this._createObject(variableName, value);
|
|
152
164
|
} else {
|
|
153
165
|
this.data[variableName] = value;
|
|
@@ -162,8 +174,8 @@ class Resonant {
|
|
|
162
174
|
return value;
|
|
163
175
|
}
|
|
164
176
|
var found = localStorage.getItem('res_' + variableName);
|
|
165
|
-
if (found !== null && found !== undefined){
|
|
166
|
-
return JSON.parse(
|
|
177
|
+
if (found !== null && found !== undefined) {
|
|
178
|
+
return JSON.parse(found);
|
|
167
179
|
} else {
|
|
168
180
|
localStorage.setItem('res_' + variableName, JSON.stringify(value));
|
|
169
181
|
return value;
|
|
@@ -182,6 +194,10 @@ class Resonant {
|
|
|
182
194
|
});
|
|
183
195
|
}
|
|
184
196
|
|
|
197
|
+
_resolveValue(instance, key, override = null) {
|
|
198
|
+
return override ?? instance[key];
|
|
199
|
+
}
|
|
200
|
+
|
|
185
201
|
_createObject(variableName, obj) {
|
|
186
202
|
obj[Symbol('isProxy')] = true;
|
|
187
203
|
return new Proxy(obj, {
|
|
@@ -196,15 +212,56 @@ class Resonant {
|
|
|
196
212
|
});
|
|
197
213
|
}
|
|
198
214
|
|
|
215
|
+
_evaluateDisplayCondition(element, item, condition) {
|
|
216
|
+
try {
|
|
217
|
+
const show = new Function('item', `return ${condition}`)(item);
|
|
218
|
+
element.style.display = show ? '' : 'none';
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error(`Error evaluating display condition: ${condition}`, e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_handleDisplayElements(parentElement, instance) {
|
|
225
|
+
const displayElements = parentElement.querySelectorAll('[res-display]');
|
|
226
|
+
displayElements.forEach(displayEl => {
|
|
227
|
+
const condition = displayEl.getAttribute('res-display') || '';
|
|
228
|
+
this._evaluateDisplayCondition(displayEl, instance, condition);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_bindClickEvents(parentElement, instance, arrayValue) {
|
|
233
|
+
const onclickElements = parentElement.querySelectorAll('[res-onclick], [res-onclick-remove]');
|
|
234
|
+
onclickElements.forEach(onclickEl => {
|
|
235
|
+
const functionName = onclickEl.getAttribute('res-onclick');
|
|
236
|
+
const removeKey = onclickEl.getAttribute('res-onclick-remove');
|
|
237
|
+
|
|
238
|
+
if (functionName) {
|
|
239
|
+
onclickEl.onclick = () => {
|
|
240
|
+
const func = new Function('item', `return ${functionName}(item)`);
|
|
241
|
+
func(instance);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (removeKey) {
|
|
245
|
+
onclickEl.onclick = () => {
|
|
246
|
+
if (arrayValue) {
|
|
247
|
+
const removeIdx = arrayValue.findIndex(t => t[removeKey] === instance[removeKey]);
|
|
248
|
+
if (removeIdx !== -1) {
|
|
249
|
+
arrayValue.splice(removeIdx, 1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
199
257
|
_defineProperty(variableName) {
|
|
200
258
|
Object.defineProperty(window, variableName, {
|
|
201
259
|
get: () => this.data[variableName],
|
|
202
260
|
set: (newValue) => {
|
|
203
261
|
if (Array.isArray(newValue)) {
|
|
204
262
|
this.data[variableName] = new ObservableArray(variableName, this, ...newValue);
|
|
205
|
-
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
206
|
-
|
|
207
|
-
} else if (typeof newValue === 'object') {
|
|
263
|
+
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
264
|
+
} else if (typeof newValue === 'object' && newValue !== null) {
|
|
208
265
|
this.data[variableName] = this._createObject(variableName, newValue);
|
|
209
266
|
} else {
|
|
210
267
|
this.data[variableName] = newValue;
|
|
@@ -212,7 +269,8 @@ class Resonant {
|
|
|
212
269
|
this.updateElement(variableName);
|
|
213
270
|
this.updateDisplayConditionalsFor(variableName);
|
|
214
271
|
this.updateStylesFor(variableName);
|
|
215
|
-
|
|
272
|
+
|
|
273
|
+
if (!Array.isArray(newValue) && (typeof newValue !== 'object' || newValue === null)) {
|
|
216
274
|
this._queueUpdate(variableName, 'modified', this.data[variableName]);
|
|
217
275
|
}
|
|
218
276
|
}
|
|
@@ -223,7 +281,6 @@ class Resonant {
|
|
|
223
281
|
if (!this.pendingUpdates.has(variableName)) {
|
|
224
282
|
this.pendingUpdates.set(variableName, []);
|
|
225
283
|
}
|
|
226
|
-
|
|
227
284
|
this.pendingUpdates.get(variableName).push({ action, item, property, oldValue });
|
|
228
285
|
|
|
229
286
|
if (this.pendingUpdates.get(variableName).length === 1) {
|
|
@@ -232,16 +289,19 @@ class Resonant {
|
|
|
232
289
|
this.updatePersistantData(variableName);
|
|
233
290
|
this.pendingUpdates.delete(variableName);
|
|
234
291
|
|
|
235
|
-
updates = updates.filter((v, i, a) =>
|
|
292
|
+
updates = updates.filter((v, i, a) =>
|
|
293
|
+
a.findIndex(t => (t.property === v.property && t.action === v.action)) === i
|
|
294
|
+
);
|
|
295
|
+
|
|
236
296
|
updates.forEach(update => {
|
|
237
297
|
this._triggerCallbacks(variableName, update);
|
|
238
298
|
});
|
|
299
|
+
|
|
239
300
|
this.updateElement(variableName);
|
|
240
301
|
this.updateDisplayConditionalsFor(variableName);
|
|
241
302
|
this.updateStylesFor(variableName);
|
|
242
303
|
}, 0);
|
|
243
304
|
}
|
|
244
|
-
|
|
245
305
|
}
|
|
246
306
|
|
|
247
307
|
_triggerCallbacks(variableName, callbackData) {
|
|
@@ -258,55 +318,27 @@ class Resonant {
|
|
|
258
318
|
const value = this.data[variableName];
|
|
259
319
|
|
|
260
320
|
elements.forEach(element => {
|
|
261
|
-
element.value = value;
|
|
262
321
|
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
} else if (Array.isArray(value)) {
|
|
271
|
-
element.querySelectorAll(`[res="${variableName}"][res-rendered=true]`).forEach(el => el.remove());
|
|
322
|
+
this._handleInputElement(element, value, (newValue) => {
|
|
323
|
+
this.data[variableName] = newValue;
|
|
324
|
+
this._queueUpdate(variableName, 'modified', this.data[variableName]);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
else if (Array.isArray(value)) {
|
|
328
|
+
element.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
|
|
272
329
|
this._renderArray(variableName, element);
|
|
273
|
-
}
|
|
274
|
-
|
|
330
|
+
}
|
|
331
|
+
else if (typeof value === 'object' && value !== null) {
|
|
332
|
+
const subElements = element.querySelectorAll('[res-prop]');
|
|
275
333
|
subElements.forEach(subEl => {
|
|
276
334
|
const key = subEl.getAttribute('res-prop');
|
|
277
335
|
if (key && key in value) {
|
|
278
|
-
|
|
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
|
-
}
|
|
336
|
+
this._renderObjectProperty(subEl, value[key], variableName, key);
|
|
306
337
|
}
|
|
307
338
|
});
|
|
308
|
-
}
|
|
309
|
-
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
element.innerHTML = value ?? '';
|
|
310
342
|
}
|
|
311
343
|
});
|
|
312
344
|
|
|
@@ -314,25 +346,128 @@ class Resonant {
|
|
|
314
346
|
this.updateStylesFor(variableName);
|
|
315
347
|
}
|
|
316
348
|
|
|
349
|
+
_renderObjectProperty(subEl, propValue, parentVarName, key) {
|
|
350
|
+
if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
|
|
351
|
+
!Array.isArray(propValue) &&
|
|
352
|
+
typeof propValue !== 'object') {
|
|
353
|
+
this._handleInputElement(subEl, propValue, (newValue) => {
|
|
354
|
+
window[parentVarName][key] = newValue;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else if (Array.isArray(propValue)) {
|
|
358
|
+
this._renderNestedArray(subEl, propValue);
|
|
359
|
+
}
|
|
360
|
+
else if (typeof propValue === 'object' && propValue !== null) {
|
|
361
|
+
const nestedElements = subEl.querySelectorAll('[res-prop]');
|
|
362
|
+
nestedElements.forEach(nestedEl => {
|
|
363
|
+
const nestedKey = nestedEl.getAttribute('res-prop');
|
|
364
|
+
if (nestedKey && nestedKey in propValue) {
|
|
365
|
+
this._renderObjectProperty(nestedEl, propValue[nestedKey], parentVarName, nestedKey);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
subEl.innerHTML = propValue ?? '';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_renderArray(variableName, el) {
|
|
375
|
+
let template = el.cloneNode(true);
|
|
376
|
+
el.innerHTML = '';
|
|
377
|
+
|
|
378
|
+
if (!window[variableName + "_template"]) {
|
|
379
|
+
window[variableName + "_template"] = template;
|
|
380
|
+
} else {
|
|
381
|
+
template = window[variableName + "_template"];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
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);
|
|
412
|
+
}
|
|
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 ?? '';
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._handleDisplayElements(clonedEl, instance);
|
|
429
|
+
this._bindClickEvents(clonedEl, instance, this.data[variableName]);
|
|
430
|
+
|
|
431
|
+
clonedEl.setAttribute("res-rendered", "true");
|
|
432
|
+
el.appendChild(clonedEl);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_renderNestedArray(subEl, arrayValue) {
|
|
437
|
+
const template = subEl.cloneNode(true);
|
|
438
|
+
subEl.innerHTML = '';
|
|
439
|
+
|
|
440
|
+
subEl.removeAttribute('res-prop');
|
|
441
|
+
|
|
442
|
+
arrayValue.forEach((item, idx) => {
|
|
443
|
+
const cloned = template.cloneNode(true);
|
|
444
|
+
cloned.setAttribute('res-rendered', 'true');
|
|
445
|
+
|
|
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
|
+
});
|
|
453
|
+
|
|
454
|
+
this._handleDisplayElements(cloned, item);
|
|
455
|
+
this._bindClickEvents(cloned, item, arrayValue);
|
|
456
|
+
|
|
457
|
+
subEl.appendChild(cloned);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
317
461
|
updateDisplayConditionalsFor(variableName) {
|
|
318
462
|
const conditionalElements = document.querySelectorAll(`[res-display*="${variableName}"]`);
|
|
319
463
|
conditionalElements.forEach(conditionalElement => {
|
|
320
464
|
const condition = conditionalElement.getAttribute('res-display');
|
|
321
|
-
|
|
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
|
-
}
|
|
465
|
+
this._evaluateDisplayCondition(conditionalElement, this.data[variableName], condition);
|
|
330
466
|
});
|
|
331
467
|
}
|
|
332
468
|
|
|
333
469
|
updateStylesFor(variableName) {
|
|
334
470
|
const styleElements = document.querySelectorAll(`[res-style*="${variableName}"]`);
|
|
335
|
-
|
|
336
471
|
styleElements.forEach(styleElement => {
|
|
337
472
|
let styleCondition = styleElement.getAttribute('res-style');
|
|
338
473
|
try {
|
|
@@ -355,23 +490,21 @@ class Resonant {
|
|
|
355
490
|
const item = this.data[variableName][index];
|
|
356
491
|
styleCondition = styleCondition.replace(new RegExp(`\\b${variableName}\\b`, 'g'), 'item');
|
|
357
492
|
const styleClass = new Function('item', `return ${styleCondition}`)(item);
|
|
358
|
-
|
|
359
493
|
if (styleClass) {
|
|
360
494
|
styleElement.classList.add(styleClass);
|
|
361
495
|
} else {
|
|
362
|
-
|
|
496
|
+
const elementHasStyle = styleElement.classList.contains(styleClass);
|
|
363
497
|
if (elementHasStyle) {
|
|
364
498
|
styleElement.classList.remove(styleClass);
|
|
365
499
|
}
|
|
366
500
|
}
|
|
367
501
|
} else {
|
|
368
502
|
const styleClass = eval(styleCondition);
|
|
369
|
-
|
|
370
503
|
if (styleClass) {
|
|
371
504
|
styleElement.classList.add(styleClass);
|
|
372
505
|
styleElement.setAttribute('res-styles', styleClass);
|
|
373
506
|
} else {
|
|
374
|
-
|
|
507
|
+
const elementHasStyle = styleElement.classList.contains(styleClass);
|
|
375
508
|
if (elementHasStyle) {
|
|
376
509
|
styleElement.classList.remove(styleClass);
|
|
377
510
|
}
|
|
@@ -383,81 +516,6 @@ class Resonant {
|
|
|
383
516
|
});
|
|
384
517
|
}
|
|
385
518
|
|
|
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
|
-
const subEl = clonedEl.querySelector(`[res-prop="${key}"]`);
|
|
401
|
-
if (subEl) {
|
|
402
|
-
if (!subEl.hasAttribute('data-resonant-bound')) {
|
|
403
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
404
|
-
if (subEl.type === 'checkbox') {
|
|
405
|
-
subEl.checked = instance[key];
|
|
406
|
-
subEl.onchange = () => {
|
|
407
|
-
instance[key] = subEl.checked;
|
|
408
|
-
this._queueUpdate(variableName, 'modified', instance, key, instance[key]);
|
|
409
|
-
};
|
|
410
|
-
} else {
|
|
411
|
-
subEl.value = instance[key];
|
|
412
|
-
subEl.oninput = () => {
|
|
413
|
-
instance[key] = subEl.value;
|
|
414
|
-
this._queueUpdate(variableName, 'modified', instance, key, instance[key]);
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
subEl.innerHTML = instance[key];
|
|
419
|
-
}
|
|
420
|
-
subEl.setAttribute('data-resonant-bound', 'true');
|
|
421
|
-
} else {
|
|
422
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
423
|
-
if (subEl.type === 'checkbox') {
|
|
424
|
-
subEl.checked = instance[key];
|
|
425
|
-
} else {
|
|
426
|
-
subEl.value = instance[key];
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
subEl.innerHTML = instance[key];
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const onclickElements = clonedEl.querySelectorAll('[res-onclick], [res-onclick-remove]');
|
|
436
|
-
onclickElements.forEach(onclickEl => {
|
|
437
|
-
const functionName = onclickEl.getAttribute('res-onclick');
|
|
438
|
-
const removeKey = onclickEl.getAttribute('res-onclick-remove');
|
|
439
|
-
if (functionName) {
|
|
440
|
-
onclickEl.onclick = () => {
|
|
441
|
-
const func = new Function('item', `return ${functionName}(item)`);
|
|
442
|
-
func(instance);
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (removeKey) {
|
|
447
|
-
onclickEl.onclick = () => {
|
|
448
|
-
const index = this.data[variableName].findIndex(t => t[removeKey] === instance[removeKey]);
|
|
449
|
-
if (index !== -1) {
|
|
450
|
-
this.data[variableName].splice(index, 1);
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
clonedEl.setAttribute("res-rendered", true);
|
|
457
|
-
el.appendChild(clonedEl);
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
519
|
addCallback(variableName, method) {
|
|
462
520
|
if (!this.callbacks[variableName]) {
|
|
463
521
|
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((
|
|
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)}}
|