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 +91 -78
- package/examples/example-basic.html +88 -0
- package/examples/example-taskmanager.html +69 -0
- package/package.json +1 -1
- package/resonant.js +169 -36
- package/resonant.min.js +1 -0
- package/example.html +0 -79
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
|
-
```
|
|
34
|
+
```html
|
|
36
35
|
<!DOCTYPE html>
|
|
37
36
|
<html lang="en">
|
|
38
37
|
<head>
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
###
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
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.
|
|
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,
|
|
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(
|
|
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]
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
47
|
+
get(target, index) {
|
|
45
48
|
if (typeof target[index] === 'object' && !target[index][Symbol('isProxy')]) {
|
|
46
|
-
target[index] =
|
|
49
|
+
target[index] = self._createObject(`${variableName}[${index}]`, target[index]);
|
|
47
50
|
}
|
|
48
51
|
return target[index];
|
|
49
52
|
},
|
|
50
|
-
set
|
|
51
|
-
target[index]
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
76
|
-
element.
|
|
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.
|
|
85
|
-
subEl.
|
|
86
|
-
|
|
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.
|
|
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.
|
|
137
|
-
subEl.
|
|
138
|
-
|
|
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.
|
|
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]
|
|
280
|
+
if (!this.callbacks[variableName]) {
|
|
281
|
+
this.callbacks[variableName] = [];
|
|
282
|
+
}
|
|
283
|
+
this.callbacks[variableName].push(method);
|
|
151
284
|
}
|
|
152
285
|
}
|
package/resonant.min.js
ADDED
|
@@ -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>
|