resonantjs 1.0.1 → 1.0.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.
package/README.md CHANGED
@@ -10,7 +10,6 @@ Resonant.js is an open-source lightweight JavaScript framework that enables reac
10
10
  - **Efficient Conditional Updates**: Only evaluate conditional expressions tied to specific variable changes.
11
11
  - **Lightweight and Easy to Integrate**: Minimal setup required to get started.
12
12
  - **Compatible with Modern Browsers**: Works seamlessly across all modern web browsers.
13
-
14
13
  ## Installation
15
14
  ## NPM
16
15
  To install via NPM, use the following command:
@@ -32,87 +31,29 @@ To use via CDN, include the following URLs in your HTML file:
32
31
  ## Usage
33
32
  Include resonant.js in your HTML file, and use the following example to understand how to integrate it into your web application.
34
33
 
35
- ```javascript
34
+ ```html
36
35
  <!DOCTYPE html>
37
36
  <html lang="en">
38
37
  <head>
39
- <title>Resonant.js Quick Demo</title>
40
- <script src="https://unpkg.com/resonantjs@latest/resonant.js"></script>
38
+ <title>Resonant.js Basic Example</title>
39
+ <script src="https://unpkg.com/resonantjs@latest/resonant.js"></script>
41
40
  </head>
42
41
  <body>
43
- <h1>Resonant.js Quick Demo</h1>
44
-
45
- <!-- Display and update a single item -->
46
- <div>
47
- <h2>Counter</h2>
48
- <p>
49
- Current count: <span res="counter"></span>
50
- </p>
51
- <div res-conditional="counter >= 5">
52
- Only shows when counter is greater than or equal to 5
53
- </div>
54
- <button onclick="counter++">Increment Counter</button>
55
- </div>
56
-
57
- <!-- Demonstrate object property binding -->
58
- <div>
59
- <h2>Person Information</h2>
60
- <div res="person">
61
- <span res-prop="firstname"></span>
62
- <span res-prop="lastname"></span>
63
- <br/>
64
- <div res-conditional="person.firstname == 'Andrew' && person.lastname == 'Murgola'">
65
- Only shows when firstname is Andrew and lastname is Murgola
66
- </div>
67
- <br/>
68
-
69
- First Name: <input type="text" res-prop="firstname" />
70
- Last Name: <input type="text" res-prop="lastname" />
71
- </div>
72
- </div>
73
-
74
- <!-- Demonstrate dynamic list rendering -->
75
- <div>
76
- <h2>Team Members</h2>
77
- <ul res="team">
78
- <li>
79
- <span res-prop="name"></span> - <span res-prop="role"></span>
80
- </li>
81
- </ul>
82
- <button onclick="addTeamMember()">Add Team Member</button>
83
- </div>
84
-
85
- <script>
86
- const resonantJs = new Resonant();
87
-
88
- // Initialize a counter
89
- resonantJs.add("counter", 0);
90
-
91
- // Initialize a single object
92
- resonantJs.add("person", {
93
- firstname: "Andrew",
94
- lastname: "Murgola"
95
- });
96
-
97
- // Initialize an array of objects
98
- resonantJs.add("team", [
99
- { name: "Alice", role: "Developer" },
100
- { name: "Bob", role: "Designer" }
101
- ]);
102
-
103
- // Example of a callback
104
- resonantJs.addCallback("person", (result) => {
105
- console.log(result.firstname + " " + result.lastname);
106
- });
107
-
108
- function addTeamMember() {
109
- const newMember = { name: "Charlie", role: "Product Manager" };
110
- team.push(newMember);
111
- }
112
- </script>
42
+ <h1>Resonant.js Basic Example</h1>
43
+ <div>
44
+ <h2>Counter</h2>
45
+ <p>Current count: <span res="counter"></span></p>
46
+ <button onclick="counter++">Increment Counter</button>
47
+ </div>
48
+
49
+ <script>
50
+ const resonantJs = new Resonant();
51
+ resonantJs.add("counter", 0);
52
+ </script>
113
53
  </body>
114
54
  </html>
115
55
  ```
56
+
116
57
  ## Features Overview
117
58
 
118
59
  ### Core Concepts
@@ -120,6 +61,8 @@ Include resonant.js in your HTML file, and use the following example to understa
120
61
  - `res` is used to identify an overarching data model.
121
62
  - `res-prop` links individual properties within that model to corresponding UI elements.
122
63
  - **`res-conditional` Attribute**: Conditionally display elements based on the data model's properties.
64
+ - **`res-onclick` Attribute**: Triggers a function when an element is clicked, allowing for custom event handling.
65
+ - **`res-onclick-remove` Attribute**: Removes an item from an array when the associated element is clicked.
123
66
  - **Automatic UI Updates**: Changes to your JavaScript objects instantly reflect in the associated UI components, reducing manual DOM manipulation.
124
67
 
125
68
  ### Advanced Features
@@ -127,10 +70,80 @@ Include resonant.js in your HTML file, and use the following example to understa
127
70
  - **Event Callbacks**: Register custom functions to execute whenever your data model changes.
128
71
  - **Bidirectional Input Binding**: Bind form input fields directly to your data, making two-way synchronization simple.
129
72
 
130
- ### Example Applications
131
- - **Single-Page Applications**: Build dynamic and responsive single-page applications quickly.
132
- - **Admin Dashboards**: Create data-rich dashboards that reflect real-time changes in your database.
133
- - **Form-Based Applications**: Automate forms, surveys, and user profiles for seamless data entry.
73
+ ### New Features in Version 1.0.2
74
+
75
+ #### Pending Updates Mechanism
76
+ - Introduced to prevent redundant updates and ensure callbacks are only triggered once per update cycle, improving performance and user experience.
77
+
78
+ #### Callback Parameter Enhancement
79
+ - Callbacks now receive detailed parameters including the specific action taken (`added`, `modified`, `removed`), the item affected, and the previous value. This provides better context for handling updates.
80
+
81
+ #### Batched Updates for Object Properties
82
+ - Improved handling of object property updates to ensure changes are batched together, preventing multiple redundant callback triggers.
83
+
84
+ #### Refined Data Binding
85
+ - Enhanced data binding between model and view to ensure consistent synchronization without unnecessary updates.
86
+
87
+ ### Task Manager Example
88
+
89
+ Here is an example of a task manager application using Resonant.js:
90
+
91
+ ```html
92
+ <!DOCTYPE html>
93
+ <html lang="en">
94
+ <head>
95
+ <title>Resonant.js Task Manager Demo</title>
96
+ <script src="https://cdn.jsdelivr.net/npm/resonantjs@latest/dist/resonant.min.js"></script>
97
+ </head>
98
+ <body>
99
+ <h1>Resonant.js Task Manager Demo</h1>
100
+
101
+ <!-- Task Input -->
102
+ <div>
103
+ <h2>Add New Task</h2>
104
+ <input type="text" placeholder="Task Name" res="taskName" />
105
+ <button onclick="addTask()">Add Task</button>
106
+ </div>
107
+
108
+ <!-- Task List -->
109
+ <div>
110
+ <h2>Task List</h2>
111
+ <ul res="tasks">
112
+ <li>
113
+ <input type="checkbox" res-prop="done" />
114
+ <span res-prop="name"></span>
115
+ <button res-onclick-remove="name">Remove</button>
116
+ </li>
117
+ </ul>
118
+ </div>
119
+
120
+ <script>
121
+ const resonantJs = new Resonant();
122
+
123
+ // Initialize variables using a configuration object
124
+ resonantJs.addAll({
125
+ tasks: [
126
+ { name: "Task 1", done: false },
127
+ { name: "Task 2", done: true }
128
+ ],
129
+ taskName: ""
130
+ });
131
+
132
+ // Add a callback to log actions taken on tasks
133
+ resonantJs.addCallback("tasks", (tasks, task, action) => {
134
+ console.log(`Action taken: ${action} for ${task.name}`);
135
+ });
136
+
137
+ // Add a function to add a new task
138
+ function addTask() {
139
+ const newTask = { name: taskName, done: false };
140
+ tasks.push(newTask);
141
+ taskName = '';
142
+ }
143
+ </script>
144
+ </body>
145
+ </html>
146
+ ```
134
147
 
135
148
  ## Future Enhancements
136
149
 
@@ -0,0 +1,88 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Resonant.js Practical Demo</title>
5
+ <script src="../resonant.js"></script>
6
+ </head>
7
+ <body>
8
+ <h1>Resonant.js Practical Demo</h1>
9
+
10
+ <!-- Display and update a single item -->
11
+ <div>
12
+ <h2>Counter</h2>
13
+ <p>
14
+ Current count: <span res="counter"></span>
15
+ </p>
16
+ <div res-conditional="counter < 10">
17
+ Only shows when counter is less than 10
18
+ </div>
19
+ <div res-conditional="counter >= 10">
20
+ Only shows when counter is greater than or equal to 10
21
+ </div>
22
+ <button onclick="counter++">Increment Counter</button>
23
+ </div>
24
+
25
+ <!-- Demonstrate object property binding -->
26
+ <div>
27
+ <h2>User Information</h2>
28
+ <div res="user">
29
+ <span res-prop="firstname"></span>
30
+ <span res-prop="lastname"></span>
31
+ <br/>
32
+ <div res-conditional="user.firstname == 'John' && user.lastname == 'Doe'">
33
+ Only shows when firstname is John and lastname is Doe
34
+ </div>
35
+ <br/>
36
+ First Name: <input type="text" res-prop="firstname" />
37
+ Last Name: <input type="text" res-prop="lastname" />
38
+ </div>
39
+ </div>
40
+
41
+ <!-- Demonstrate dynamic list rendering -->
42
+ <div>
43
+ <h2>Project Members</h2>
44
+ <ul res="projectTeam">
45
+ <li>
46
+ <span res-prop="name"></span> - <span res-prop="role"></span>
47
+ </li>
48
+ </ul>
49
+ <button onclick="addProjectMember()">Add Project Member</button>
50
+ </div>
51
+
52
+ <script>
53
+ const resonantJs = new Resonant();
54
+
55
+ // Initialize a single object with add method
56
+ resonantJs.add("counter", 0);
57
+
58
+ // Initialize variables using a configuration object
59
+ resonantJs.addAll({
60
+ user: {
61
+ firstname: "John",
62
+ lastname: "Doe",
63
+ email: ""
64
+ },
65
+ projectTeam: [
66
+ { name: "Alice", role: "Developer" },
67
+ { name: "Bob", role: "Designer" }
68
+ ]
69
+ });
70
+
71
+ // Chain together callbacks
72
+ resonantJs.addCallback("user", (user) => {
73
+ console.log(`User updated: ${user.firstname} ${user.lastname}`);
74
+ // You can nest updates within callbacks
75
+ counter++;
76
+ });
77
+
78
+ resonantJs.addCallback("counter", (count) => {
79
+ console.log(`Counter updated: ${count}`);
80
+ });
81
+
82
+ function addProjectMember() {
83
+ const newMember = { name: "Charlie", role: "Product Manager" };
84
+ projectTeam.push(newMember);
85
+ }
86
+ </script>
87
+ </body>
88
+ </html>
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Resonant.js Task Manager Demo</title>
5
+ <script src="../resonant.js"></script>
6
+ </head>
7
+ <style>
8
+ .done {
9
+ text-decoration: line-through;
10
+ }
11
+ </style>
12
+ <body>
13
+ <h1>Resonant.js Task Manager Demo</h1>
14
+
15
+ <!-- Task Input -->
16
+ <div>
17
+ <h2>Add New Task</h2>
18
+ <input type="text" placeholder="Task Name" res="taskName" />
19
+ <p>
20
+ Name: <span res="taskName"></span>
21
+ </p>
22
+ <button onclick="addTask()">Add Task</button>
23
+ </div>
24
+
25
+ <!-- Task List -->
26
+ <div>
27
+ <h2>Task List</h2>
28
+ <ul res="tasks">
29
+ <li>
30
+ <input type="checkbox" res-prop="done" />
31
+ <span res-prop="name"></span>
32
+ <button res-onclick-remove="name">Remove</button>
33
+ </li>
34
+ </ul>
35
+ </div>
36
+
37
+ <script>
38
+ const resonantJs = new Resonant();
39
+
40
+ // Initialize variables using a configuration object
41
+ resonantJs.addAll({
42
+ tasks: [
43
+ { name: "Task 1", done: false },
44
+ { name: "Task 2", done: true }
45
+ ],
46
+ taskName: ""
47
+ });
48
+
49
+ // Add a callback to log actions taken on tasks
50
+ resonantJs.addCallback("tasks", (tasks, task, action) => {
51
+ console.log(`Action taken: ${action} for ${task.name}`);
52
+ });
53
+
54
+ function remove(task) {
55
+ const index = tasks.indexOf(task);
56
+ tasks.splice(index, 1);
57
+
58
+ //You could use as well, still trying to figure out if I want to leave this or not
59
+ }
60
+
61
+ // Add a function to add a new task
62
+ function addTask() {
63
+ const newTask = { name: taskName, done: false };
64
+ tasks.push(newTask);
65
+ taskName = '';
66
+ }
67
+ </script>
68
+ </body>
69
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resonantjs",
3
- "version": "1.0.1",
3
+ "version": "1.0.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
@@ -2,21 +2,21 @@ class Resonant {
2
2
  constructor() {
3
3
  this.data = {};
4
4
  this.callbacks = {};
5
+ this.pendingUpdates = new Set();
5
6
  }
6
7
 
7
- add(variableName, ...values) {
8
- let value;
9
- if (values.length > 1) {
10
- value = values;
11
- } else {
12
- value = values[0];
13
- }
14
-
8
+ add(variableName, value) {
15
9
  this._assignValueToData(variableName, value);
16
10
  this._defineProperty(variableName);
17
11
  this.updateElement(variableName);
18
12
  }
19
13
 
14
+ addAll(config) {
15
+ Object.entries(config).forEach(([variableName, value]) => {
16
+ this.add(variableName, value);
17
+ });
18
+ }
19
+
20
20
  _assignValueToData(variableName, value) {
21
21
  if (Array.isArray(value)) {
22
22
  this.data[variableName] = this._createArray(variableName, value);
@@ -27,35 +27,66 @@ class Resonant {
27
27
  }
28
28
  }
29
29
 
30
- _createObject(parentName, obj) {
30
+ _createObject(variableName, obj) {
31
31
  obj[Symbol('isProxy')] = true;
32
32
  return new Proxy(obj, {
33
33
  set: (target, property, value) => {
34
- target[property] = value;
35
- this.updateElement(parentName);
36
- this.updateConditionalsFor(parentName);
34
+ if (target[property] !== value) {
35
+ const oldValue = target[property];
36
+ target[property] = value;
37
+ this._queueUpdate(variableName, 'modified', target, property, oldValue);
38
+ }
37
39
  return true;
38
40
  }
39
41
  });
40
42
  }
41
43
 
42
44
  _createArray(variableName, arr) {
45
+ const self = this;
43
46
  return new Proxy(arr, {
44
- get: (target, index) => {
47
+ get(target, index) {
45
48
  if (typeof target[index] === 'object' && !target[index][Symbol('isProxy')]) {
46
- target[index] = this._createObject(`${variableName}[${index}]`, target[index]);
49
+ target[index] = self._createObject(`${variableName}[${index}]`, target[index]);
47
50
  }
48
51
  return target[index];
49
52
  },
50
- set: (target, index, value) => {
51
- target[index] = value;
52
- this.updateElement(variableName);
53
- this.updateConditionalsFor(variableName);
53
+ set(target, index, value) {
54
+ if (target[index] !== value) {
55
+ const action = target.hasOwnProperty(index) ? 'modified' : 'added';
56
+ const oldValue = target[index];
57
+ target[index] = value;
58
+ self._queueUpdate(variableName, action, target[index], index, oldValue);
59
+ }
60
+ return true;
61
+ },
62
+ deleteProperty(target, index) {
63
+ const oldValue = target[index];
64
+ target.splice(index, 1);
65
+ self._queueUpdate(variableName, 'removed', oldValue, index);
54
66
  return true;
55
67
  }
56
68
  });
57
69
  }
58
70
 
71
+ _queueUpdate(variableName, action, item, property, oldValue) {
72
+ if (!this.pendingUpdates.has(variableName)) {
73
+ this.pendingUpdates.add(variableName);
74
+ setTimeout(() => {
75
+ this.pendingUpdates.delete(variableName);
76
+ this._triggerCallbacks(variableName, action, item, property, oldValue);
77
+ this.updateElement(variableName);
78
+ this.updateConditionalsFor(variableName);
79
+ this.updateStylesFor(variableName);
80
+ }, 0);
81
+ }
82
+ }
83
+
84
+ _triggerCallbacks(variableName, action, item, property, oldValue) {
85
+ if (this.callbacks[variableName]) {
86
+ this.callbacks[variableName].forEach(callback => callback(this.data[variableName], item, action, property, oldValue));
87
+ }
88
+ }
89
+
59
90
  _defineProperty(variableName) {
60
91
  Object.defineProperty(window, variableName, {
61
92
  get: () => this.data[variableName],
@@ -63,6 +94,10 @@ class Resonant {
63
94
  this._assignValueToData(variableName, newValue);
64
95
  this.updateElement(variableName);
65
96
  this.updateConditionalsFor(variableName);
97
+ this.updateStylesFor(variableName);
98
+ if (!Array.isArray(newValue) && typeof newValue !== 'object') {
99
+ this._queueUpdate(variableName, 'modified', this.data[variableName]);
100
+ }
66
101
  }
67
102
  });
68
103
  }
@@ -72,20 +107,50 @@ class Resonant {
72
107
  const value = this.data[variableName];
73
108
 
74
109
  elements.forEach(element => {
75
- if (Array.isArray(value)) {
76
- element.querySelectorAll(`[res="${variableName}"]` && "[res-rendered=true]").forEach(el => el.remove());
110
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
111
+ if (!element.hasAttribute('data-resonant-bound')) {
112
+ element.value = value;
113
+ element.oninput = () => {
114
+ this.data[variableName] = element.value;
115
+ this._queueUpdate(variableName, 'modified', this.data[variableName]);
116
+ };
117
+ element.setAttribute('data-resonant-bound', 'true');
118
+ }
119
+ } else if (Array.isArray(value)) {
120
+ element.querySelectorAll(`[res="${variableName}"][res-rendered=true]`).forEach(el => el.remove());
77
121
  this._renderArray(variableName, element);
78
122
  } else if (typeof value === 'object') {
79
123
  const subElements = element.querySelectorAll(`[res-prop]`);
80
-
81
124
  subElements.forEach(subEl => {
82
125
  const key = subEl.getAttribute('res-prop');
83
126
  if (key && key in value) {
84
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
85
- subEl.value = value[key];
86
- subEl.oninput = () => this.data[variableName][key] = subEl.value;
127
+ if (!subEl.hasAttribute('data-resonant-bound')) {
128
+ if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
129
+ if (subEl.type === 'checkbox') {
130
+ subEl.checked = value[key];
131
+ subEl.onchange = () => {
132
+ this.data[variableName][key] = subEl.checked;
133
+ };
134
+ } else {
135
+ subEl.value = value[key];
136
+ subEl.oninput = () => {
137
+ this.data[variableName][key] = subEl.value;
138
+ };
139
+ }
140
+ } else {
141
+ subEl.innerHTML = value[key];
142
+ }
143
+ subEl.setAttribute('data-resonant-bound', 'true');
87
144
  } else {
88
- subEl.innerHTML = value[key];
145
+ if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
146
+ if (subEl.type === 'checkbox') {
147
+ subEl.checked = value[key];
148
+ } else {
149
+ subEl.value = value[key];
150
+ }
151
+ } else {
152
+ subEl.innerHTML = value[key];
153
+ }
89
154
  }
90
155
  }
91
156
  });
@@ -94,12 +159,8 @@ class Resonant {
94
159
  }
95
160
  });
96
161
 
97
- // Call the variable-specific condition update
98
162
  this.updateConditionalsFor(variableName);
99
-
100
- if (this.callbacks[variableName]) {
101
- this.callbacks[variableName](value);
102
- }
163
+ this.updateStylesFor(variableName);
103
164
  }
104
165
 
105
166
  updateConditionalsFor(variableName) {
@@ -118,6 +179,23 @@ class Resonant {
118
179
  });
119
180
  }
120
181
 
182
+ updateStylesFor(variableName) {
183
+ const styleElements = document.querySelectorAll(`[res-style*="${variableName}"]`);
184
+ styleElements.forEach(styleElement => {
185
+ const styleCondition = styleElement.getAttribute('res-style');
186
+ try {
187
+ const styleClass = eval(styleCondition);
188
+ if (styleClass) {
189
+ styleElement.classList.add(styleClass);
190
+ } else {
191
+ styleElement.classList.remove(styleClass);
192
+ }
193
+ } catch (e) {
194
+ console.error(`Error evaluating style for ${variableName}: ${styleCondition}`, e);
195
+ }
196
+ });
197
+ }
198
+
121
199
  _renderArray(variableName, el) {
122
200
  let template = el.cloneNode(true);
123
201
  el.innerHTML = '';
@@ -128,25 +206,80 @@ class Resonant {
128
206
  template = window[variableName + "_template"];
129
207
  }
130
208
 
131
- this.data[variableName].forEach((instance) => {
209
+ this.data[variableName].forEach((instance, index) => {
132
210
  const clonedEl = template.cloneNode(true);
211
+ clonedEl.setAttribute("res-index", index);
133
212
  for (let key in instance) {
134
213
  const subEl = clonedEl.querySelector(`[res-prop="${key}"]`);
135
214
  if (subEl) {
136
- if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
137
- subEl.value = instance[key];
138
- subEl.oninput = () => instance[key] = subEl.value;
215
+ if (!subEl.hasAttribute('data-resonant-bound')) {
216
+ if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
217
+ if (subEl.type === 'checkbox') {
218
+ subEl.checked = instance[key];
219
+ subEl.onchange = () => {
220
+ instance[key] = subEl.checked;
221
+ this._queueUpdate(variableName, 'modified', instance, key, instance[key]);
222
+ };
223
+ } else {
224
+ subEl.value = instance[key];
225
+ subEl.oninput = () => {
226
+ instance[key] = subEl.value;
227
+ this._queueUpdate(variableName, 'modified', instance, key, instance[key]);
228
+ };
229
+ }
230
+ } else {
231
+ subEl.innerHTML = instance[key];
232
+ }
233
+ subEl.setAttribute('data-resonant-bound', 'true');
139
234
  } else {
140
- subEl.innerHTML = instance[key];
235
+ if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
236
+ if (subEl.type === 'checkbox') {
237
+ subEl.checked = instance[key];
238
+ } else {
239
+ subEl.value = instance[key];
240
+ }
241
+ } else {
242
+ subEl.innerHTML = instance[key];
243
+ }
141
244
  }
142
245
  }
143
246
  }
247
+
248
+ const onclickElements = clonedEl.querySelectorAll('[res-onclick], [res-onclick-remove]');
249
+ onclickElements.forEach(onclickEl => {
250
+ const functionName = onclickEl.getAttribute('res-onclick');
251
+ const removeKey = onclickEl.getAttribute('res-onclick-remove');
252
+
253
+ if (functionName) {
254
+ onclickEl.onclick = null;
255
+
256
+ onclickEl.onclick = () => {
257
+ const func = new Function('item', `return ${functionName}(item)`);
258
+ func(instance);
259
+ };
260
+ }
261
+
262
+ if (removeKey) {
263
+ onclickEl.onclick = null;
264
+
265
+ onclickEl.onclick = () => {
266
+ const index = this.data[variableName].findIndex(t => t[removeKey] === instance[removeKey]);
267
+ if (index !== -1) {
268
+ this.data[variableName].splice(index, 1);
269
+ }
270
+ };
271
+ }
272
+ });
273
+
144
274
  clonedEl.setAttribute("res-rendered", true);
145
275
  el.appendChild(clonedEl);
146
276
  });
147
277
  }
148
278
 
149
279
  addCallback(variableName, method) {
150
- this.callbacks[variableName] = method;
280
+ if (!this.callbacks[variableName]) {
281
+ this.callbacks[variableName] = [];
282
+ }
283
+ this.callbacks[variableName].push(method);
151
284
  }
152
285
  }
@@ -0,0 +1 @@
1
+ class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Set}add(e,t){this._assignValueToData(e,t),this._defineProperty(e),this.updateElement(e)}addAll(e){Object.entries(e).forEach(([e,t])=>{this.add(e,t)})}_assignValueToData(e,t){Array.isArray(t)?this.data[e]=this._createArray(e,t):this.data[e]="object"==typeof t?this._createObject(e,t):t}_createObject(r,e){return e[Symbol("isProxy")]=!0,new Proxy(e,{set:(e,t,a)=>{var s;return e[t]!==a&&(s=e[t],e[t]=a,this._queueUpdate(r,"modified",e,t,s)),!0}})}_createArray(i,e){const n=this;return new Proxy(e,{get(e,t){return"object"!=typeof e[t]||e[t][Symbol("isProxy")]||(e[t]=n._createObject(i+`[${t}]`,e[t])),e[t]},set(e,t,a){var s,r;return e[t]!==a&&(s=e.hasOwnProperty(t)?"modified":"added",r=e[t],e[t]=a,n._queueUpdate(i,s,e[t],t,r)),!0},deleteProperty(e,t){var a=e[t];return e.splice(t,1),n._queueUpdate(i,"removed",a,t),!0}})}_queueUpdate(e,t,a,s,r){this.pendingUpdates.has(e)||(this.pendingUpdates.add(e),setTimeout(()=>{this.pendingUpdates.delete(e),this._triggerCallbacks(e,t,a,s,r),this.updateElement(e),this.updateConditionalsFor(e),this.updateStylesFor(e)},0))}_triggerCallbacks(t,a,s,r,i){this.callbacks[t]&&this.callbacks[t].forEach(e=>e(this.data[t],s,a,r,i))}_defineProperty(t){Object.defineProperty(window,t,{get:()=>this.data[t],set:e=>{this._assignValueToData(t,e),this.updateElement(t),this.updateConditionalsFor(t),this.updateStylesFor(t),Array.isArray(e)||"object"==typeof e||this._queueUpdate(t,"modified",this.data[t])}})}updateElement(a){var e=document.querySelectorAll(`[res="${a}"]`);const s=this.data[a];e.forEach(e=>{"INPUT"===e.tagName||"TEXTAREA"===e.tagName?e.hasAttribute("data-resonant-bound")||(e.value=s,e.oninput=()=>{this.data[a]=e.value,this._queueUpdate(a,"modified",this.data[a])},e.setAttribute("data-resonant-bound","true")):Array.isArray(s)?(e.querySelectorAll(`[res="${a}"][res-rendered=true]`).forEach(e=>e.remove()),this._renderArray(a,e)):"object"==typeof s?e.querySelectorAll("[res-prop]").forEach(e=>{const t=e.getAttribute("res-prop");t&&t in s&&(e.hasAttribute("data-resonant-bound")?"INPUT"===e.tagName||"TEXTAREA"===e.tagName?"checkbox"===e.type?e.checked=s[t]:e.value=s[t]:e.innerHTML=s[t]:("INPUT"===e.tagName||"TEXTAREA"===e.tagName?"checkbox"===e.type?(e.checked=s[t],e.onchange=()=>{this.data[a][t]=e.checked}):(e.value=s[t],e.oninput=()=>{this.data[a][t]=e.value}):e.innerHTML=s[t],e.setAttribute("data-resonant-bound","true")))}):e.innerHTML=s}),this.updateConditionalsFor(a),this.updateStylesFor(a)}updateConditionalsFor(variableName){const conditionalElements=document.querySelectorAll(`[res-conditional*="${variableName}"]`);conditionalElements.forEach(conditionalElement=>{const condition=conditionalElement.getAttribute("res-conditional");try{eval(condition)?conditionalElement.style.display="":conditionalElement.style.display="none"}catch(e){}})}updateStylesFor(variableName){const styleElements=document.querySelectorAll(`[res-style*="${variableName}"]`);styleElements.forEach(styleElement=>{const styleCondition=styleElement.getAttribute("res-style");try{const styleClass=eval(styleCondition);styleClass?styleElement.classList.add(styleClass):styleElement.classList.remove(styleClass)}catch(e){}})}_renderArray(r,i){let n=i.cloneNode(!0);i.innerHTML="",window[r+"_template"]?n=window[r+"_template"]:window[r+"_template"]=n,this.data[r].forEach((s,e)=>{var t=n.cloneNode(!0);t.setAttribute("res-index",e);for(let e in s){const a=t.querySelector(`[res-prop="${e}"]`);a&&(a.hasAttribute("data-resonant-bound")?"INPUT"===a.tagName||"TEXTAREA"===a.tagName?"checkbox"===a.type?a.checked=s[e]:a.value=s[e]:a.innerHTML=s[e]:("INPUT"===a.tagName||"TEXTAREA"===a.tagName?"checkbox"===a.type?(a.checked=s[e],a.onchange=()=>{s[e]=a.checked,this._queueUpdate(r,"modified",s,e,s[e])}):(a.value=s[e],a.oninput=()=>{s[e]=a.value,this._queueUpdate(r,"modified",s,e,s[e])}):a.innerHTML=s[e],a.setAttribute("data-resonant-bound","true")))}t.querySelectorAll("[res-onclick], [res-onclick-remove]").forEach(e=>{const t=e.getAttribute("res-onclick"),a=e.getAttribute("res-onclick-remove");t&&(e.onclick=null,e.onclick=()=>{new Function("item",`return ${t}(item)`)(s)}),a&&(e.onclick=null,e.onclick=()=>{var e=this.data[r].findIndex(e=>e[a]===s[a]);-1!==e&&this.data[r].splice(e,1)})}),t.setAttribute("res-rendered",!0),i.appendChild(t)})}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}
package/example.html DELETED
@@ -1,79 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <title>Resonant.js Quick Demo</title>
5
- <script src="resonant.js"></script>
6
- </head>
7
- <body>
8
- <h1>Resonant.js Quick Demo</h1>
9
-
10
- <!-- Display and update a single item -->
11
- <div>
12
- <h2>Counter</h2>
13
- <p>
14
- Current count: <span res="counter"></span>
15
- </p>
16
- <div res-conditional="counter >= 5">
17
- Only shows when counter is greater than or equal to 5
18
- </div>
19
- <button onclick="counter++">Increment Counter</button>
20
- </div>
21
-
22
- <!-- Demonstrate object property binding -->
23
- <div>
24
- <h2>Person Information</h2>
25
- <div res="person">
26
- <span res-prop="firstname"></span>
27
- <span res-prop="lastname"></span>
28
- <br/>
29
- <div res-conditional="person.firstname == 'Andrew' && person.lastname == 'Murgola'">
30
- Only shows when firstname is Andrew and lastname is Murgola
31
- </div>
32
- <br/>
33
-
34
- First Name: <input type="text" res-prop="firstname" />
35
- Last Name: <input type="text" res-prop="lastname" />
36
- </div>
37
- </div>
38
-
39
- <!-- Demonstrate dynamic list rendering -->
40
- <div>
41
- <h2>Team Members</h2>
42
- <ul res="team">
43
- <li>
44
- <span res-prop="name"></span> - <span res-prop="role"></span>
45
- </li>
46
- </ul>
47
- <button onclick="addTeamMember()">Add Team Member</button>
48
- </div>
49
-
50
- <script>
51
- const resonantJs = new Resonant();
52
-
53
- // Initialize a counter
54
- resonantJs.add("counter", 0);
55
-
56
- // Initialize a single object
57
- resonantJs.add("person", {
58
- firstname: "Andrew",
59
- lastname: "Murgola"
60
- });
61
-
62
- // Initialize an array of objects
63
- resonantJs.add("team", [
64
- { name: "Alice", role: "Developer" },
65
- { name: "Bob", role: "Designer" }
66
- ]);
67
-
68
- // Example of a callback
69
- resonantJs.addCallback("person", (result) => {
70
- console.log(result.firstname + " " + result.lastname);
71
- });
72
-
73
- function addTeamMember() {
74
- const newMember = { name: "Charlie", role: "Product Manager" };
75
- team.push(newMember);
76
- }
77
- </script>
78
- </body>
79
- </html>