rasti 3.0.1 → 4.0.0-alpha.0
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 +42 -11
- package/dist/rasti.js +1767 -622
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +728 -467
- package/es/Emitter.js +182 -28
- package/es/Model.js +237 -51
- package/es/View.js +73 -31
- package/es/core/Element.js +55 -0
- package/es/core/EventsManager.js +41 -0
- package/es/core/Interpolation.js +70 -0
- package/es/core/InterpolationWrapper.js +14 -0
- package/es/core/Partial.js +12 -0
- package/es/core/PathManager.js +88 -0
- package/es/core/SafeHTML.js +17 -0
- package/es/index.js +13 -0
- package/es/utils/deepFlat.js +4 -2
- package/es/utils/findComment.js +42 -0
- package/es/utils/getAttributesDiff.js +33 -0
- package/es/utils/getAttributesHTML.js +25 -0
- package/es/utils/getResult.js +4 -2
- package/es/utils/parseHTML.js +14 -0
- package/es/utils/syncNode.js +109 -0
- package/es/utils/validateListener.js +14 -0
- package/lib/Component.cjs +729 -468
- package/lib/Emitter.cjs +182 -28
- package/lib/Model.cjs +237 -51
- package/lib/View.cjs +73 -31
- package/lib/core/Element.cjs +57 -0
- package/lib/core/EventsManager.cjs +43 -0
- package/lib/core/Interpolation.cjs +72 -0
- package/lib/core/InterpolationWrapper.cjs +16 -0
- package/lib/core/Partial.cjs +14 -0
- package/lib/core/PathManager.cjs +90 -0
- package/lib/core/SafeHTML.cjs +19 -0
- package/lib/index.cjs +13 -0
- package/lib/utils/deepFlat.cjs +4 -2
- package/lib/utils/findComment.cjs +44 -0
- package/lib/utils/getAttributesDiff.cjs +35 -0
- package/lib/utils/getAttributesHTML.cjs +27 -0
- package/lib/utils/getResult.cjs +4 -2
- package/lib/utils/parseHTML.cjs +16 -0
- package/lib/utils/syncNode.cjs +111 -0
- package/lib/utils/validateListener.cjs +16 -0
- package/package.json +11 -8
- package/src/Component.js +725 -466
- package/src/Emitter.js +182 -28
- package/src/Model.js +236 -51
- package/src/View.js +73 -31
- package/src/core/Element.js +55 -0
- package/src/core/EventsManager.js +41 -0
- package/src/core/Interpolation.js +70 -0
- package/src/core/InterpolationWrapper.js +14 -0
- package/src/core/Partial.js +12 -0
- package/src/core/PathManager.js +88 -0
- package/src/core/SafeHTML.js +17 -0
- package/src/index.js +4 -5
- package/src/utils/deepFlat.js +4 -2
- package/src/utils/findComment.js +40 -0
- package/src/utils/getAttributesDiff.js +31 -0
- package/src/utils/getAttributesHTML.js +23 -0
- package/src/utils/getResult.js +6 -2
- package/src/utils/parseHTML.js +12 -0
- package/src/utils/syncNode.js +107 -0
- package/src/utils/validateListener.js +12 -0
package/es/Emitter.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
+
import validateListener from './utils/validateListener.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* `Emitter` is a class that provides an easy way to implement the observer pattern
|
|
3
5
|
* in your applications.
|
|
4
6
|
* It can be extended to create new classes that have the ability to emit and bind custom named events.
|
|
5
7
|
* Emitter is used by `Model` and `View` classes, which inherit from it to implement
|
|
6
8
|
* event-driven functionality.
|
|
9
|
+
*
|
|
10
|
+
* ## Inverse of Control Pattern
|
|
11
|
+
*
|
|
12
|
+
* The Emitter class includes "inverse of control" methods (`listenTo`, `listenToOnce`, `stopListening`)
|
|
13
|
+
* that allow an object to manage its own listening relationships. Instead of:
|
|
14
|
+
*
|
|
15
|
+
* ```javascript
|
|
16
|
+
* // Traditional approach - harder to clean up
|
|
17
|
+
* otherObject.on('change', this.myHandler);
|
|
18
|
+
* otherObject.on('destroy', this.cleanup);
|
|
19
|
+
* // Later you need to remember to clean up each listener
|
|
20
|
+
* otherObject.off('change', this.myHandler);
|
|
21
|
+
* otherObject.off('destroy', this.cleanup);
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* You can use:
|
|
25
|
+
*
|
|
26
|
+
* ```javascript
|
|
27
|
+
* // Inverse of control - easier cleanup
|
|
28
|
+
* this.listenTo(otherObject, 'change', this.myHandler);
|
|
29
|
+
* this.listenTo(otherObject, 'destroy', this.cleanup);
|
|
30
|
+
* // Later, clean up ALL listeners at once
|
|
31
|
+
* this.stopListening(); // Removes all listening relationships
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* This pattern is particularly useful for preventing memory leaks and simplifying cleanup
|
|
35
|
+
* in component lifecycle management.
|
|
7
36
|
*
|
|
8
37
|
* @module
|
|
9
38
|
* @example
|
|
@@ -39,20 +68,20 @@ class Emitter {
|
|
|
39
68
|
/**
|
|
40
69
|
* Adds event listener.
|
|
41
70
|
* @param {string} type Type of the event (e.g. `change`).
|
|
42
|
-
* @param {
|
|
71
|
+
* @param {Function} listener Callback function to be called when the event is emitted.
|
|
72
|
+
* @return {Function} A function to remove the listener.
|
|
43
73
|
* @example
|
|
44
74
|
* // Re render when model changes.
|
|
45
75
|
* this.model.on('change', this.render.bind(this));
|
|
46
76
|
*/
|
|
47
77
|
on(type, listener) {
|
|
48
78
|
// Validate listener.
|
|
49
|
-
|
|
50
|
-
throw new TypeError('Listener must be a function');
|
|
51
|
-
}
|
|
79
|
+
validateListener(listener);
|
|
52
80
|
// Create listeners object if it doesn't exist.
|
|
53
81
|
if (!this.listeners) this.listeners = {};
|
|
82
|
+
// Every type must have an array of listeners.
|
|
54
83
|
if (!this.listeners[type]) this.listeners[type] = [];
|
|
55
|
-
// Add listener.
|
|
84
|
+
// Add listener to the array of listeners.
|
|
56
85
|
this.listeners[type].push(listener);
|
|
57
86
|
// Return a function to remove the listener.
|
|
58
87
|
return () => this.off(type, listener);
|
|
@@ -61,33 +90,47 @@ class Emitter {
|
|
|
61
90
|
/**
|
|
62
91
|
* Adds event listener that executes once.
|
|
63
92
|
* @param {string} type Type of the event (e.g. `change`).
|
|
64
|
-
* @param {
|
|
93
|
+
* @param {Function} listener Callback function to be called when the event is emitted.
|
|
94
|
+
* @return {Function} A function to remove the listener.
|
|
65
95
|
* @example
|
|
66
96
|
* // Log a message once when model changes.
|
|
67
97
|
* this.model.once('change', () => console.log('This will happen once'));
|
|
68
98
|
*/
|
|
69
99
|
once(type, listener) {
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self.off(type, listener);
|
|
78
|
-
};
|
|
79
|
-
}
|
|
100
|
+
// Validate listener.
|
|
101
|
+
validateListener(listener);
|
|
102
|
+
// Wrap listener to remove it after it is called.
|
|
103
|
+
const wrapper = (...args) => {
|
|
104
|
+
listener(...args);
|
|
105
|
+
this.off(type, wrapper);
|
|
106
|
+
};
|
|
80
107
|
// Add listener.
|
|
81
|
-
return this.on(type,
|
|
108
|
+
return this.on(type, wrapper);
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
/**
|
|
85
|
-
* Removes event listeners.
|
|
86
|
-
* @param {string} [type] Type of the event (e.g. `change`). If
|
|
87
|
-
* @param {
|
|
112
|
+
* Removes event listeners with flexible parameter combinations.
|
|
113
|
+
* @param {string} [type] Type of the event (e.g. `change`). If not provided, removes ALL listeners from this emitter.
|
|
114
|
+
* @param {Function} [listener] Specific callback function to remove. If not provided, removes all listeners for the specified type.
|
|
115
|
+
*
|
|
116
|
+
* **Behavior based on parameters:**
|
|
117
|
+
* - `off()` - Removes ALL listeners from this emitter
|
|
118
|
+
* - `off(type)` - Removes all listeners for the specified event type
|
|
119
|
+
* - `off(type, listener)` - Removes the specific listener for the specified event type
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // Remove all listeners from this emitter
|
|
123
|
+
* this.model.off();
|
|
124
|
+
*
|
|
88
125
|
* @example
|
|
89
|
-
* //
|
|
126
|
+
* // Remove all 'change' event listeners
|
|
90
127
|
* this.model.off('change');
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* // Remove specific listener for 'change' events
|
|
131
|
+
* const myListener = () => console.log('changed');
|
|
132
|
+
* this.model.on('change', myListener);
|
|
133
|
+
* this.model.off('change', myListener);
|
|
91
134
|
*/
|
|
92
135
|
off(type, listener) {
|
|
93
136
|
// No listeners.
|
|
@@ -114,21 +157,132 @@ class Emitter {
|
|
|
114
157
|
/**
|
|
115
158
|
* Emits event of specified type. Listeners will receive specified arguments.
|
|
116
159
|
* @param {string} type Type of the event (e.g. `change`).
|
|
117
|
-
* @param {any} [
|
|
160
|
+
* @param {...any} [args] Optional arguments to be passed to listeners.
|
|
118
161
|
* @example
|
|
119
|
-
* // Emit validation error event
|
|
162
|
+
* // Emit validation error event with no arguments
|
|
120
163
|
* this.emit('invalid');
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* // Emit change event with data
|
|
167
|
+
* this.emit('change', { field : 'name', value : 'John' });
|
|
121
168
|
*/
|
|
122
169
|
emit(type, ...args) {
|
|
123
170
|
// No listeners.
|
|
124
171
|
if (!this.listeners || !this.listeners[type]) return;
|
|
125
172
|
// Call listeners. Use `slice` to make a copy and prevent errors when
|
|
126
173
|
// removing listeners inside a listener.
|
|
127
|
-
this.listeners[type]
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
174
|
+
this.listeners[type].slice().forEach(fn => fn(...args));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Listen to an event of another emitter (Inverse of Control pattern).
|
|
179
|
+
*
|
|
180
|
+
* This method allows this object to manage its own listening relationships,
|
|
181
|
+
* making cleanup easier and preventing memory leaks. Instead of calling
|
|
182
|
+
* `otherEmitter.on()`, you call `this.listenTo(otherEmitter, ...)` which
|
|
183
|
+
* allows this object to track and clean up all its listeners at once.
|
|
184
|
+
*
|
|
185
|
+
* @param {Emitter} emitter The emitter to listen to.
|
|
186
|
+
* @param {string} type The type of the event to listen to.
|
|
187
|
+
* @param {Function} listener The listener to call when the event is emitted.
|
|
188
|
+
* @return {Function} A function to stop listening to the event.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* // Instead of: otherModel.on('change', this.render.bind(this));
|
|
192
|
+
* // Use: this.listenTo(otherModel, 'change', this.render.bind(this));
|
|
193
|
+
* // This way you can later call this.stopListening() to clean up all listeners
|
|
194
|
+
*/
|
|
195
|
+
listenTo(emitter, type, listener) {
|
|
196
|
+
// Add listener to the emitter.
|
|
197
|
+
emitter.on(type, listener);
|
|
198
|
+
// Create listeningTo array if it doesn't exist.
|
|
199
|
+
if (!this.listeningTo) this.listeningTo = [];
|
|
200
|
+
// Add listener to the array of listeners.
|
|
201
|
+
this.listeningTo.push({ emitter, type, listener });
|
|
202
|
+
// Return a function to stop listening to the event.
|
|
203
|
+
return () => this.stopListening(emitter, type, listener);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Listen to an event of another emitter and remove the listener after it is called (Inverse of Control pattern).
|
|
208
|
+
*
|
|
209
|
+
* Similar to `listenTo()` but automatically removes the listener after the first execution,
|
|
210
|
+
* like `once()` but with the inverse of control benefits for cleanup management.
|
|
211
|
+
*
|
|
212
|
+
* @param {Emitter} emitter The emitter to listen to.
|
|
213
|
+
* @param {string} type The type of the event to listen to.
|
|
214
|
+
* @param {Function} listener The listener to call when the event is emitted.
|
|
215
|
+
* @return {Function} A function to stop listening to the event.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* // Listen once to another emitter's initialization event
|
|
219
|
+
* this.listenToOnce(otherModel, 'initialized', () => {
|
|
220
|
+
* console.log('Other model initialized');
|
|
221
|
+
* });
|
|
222
|
+
*/
|
|
223
|
+
listenToOnce(emitter, type, listener) {
|
|
224
|
+
validateListener(listener);
|
|
225
|
+
// Wrap listener to remove it after it is called.
|
|
226
|
+
const wrapper = (...args) => {
|
|
227
|
+
listener(...args);
|
|
228
|
+
this.stopListening(emitter, type, wrapper);
|
|
229
|
+
};
|
|
230
|
+
// Add listener.
|
|
231
|
+
return this.listenTo(emitter, type, wrapper);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Stop listening to events from other emitters (Inverse of Control pattern).
|
|
236
|
+
*
|
|
237
|
+
* This method provides flexible cleanup of listening relationships established with `listenTo()`.
|
|
238
|
+
* All parameters are optional, allowing different levels of cleanup granularity.
|
|
239
|
+
*
|
|
240
|
+
* @param {Emitter} [emitter] The emitter to stop listening to. If not provided, stops listening to ALL emitters.
|
|
241
|
+
* @param {string} [type] The type of event to stop listening to. If not provided, stops listening to all event types from the specified emitter.
|
|
242
|
+
* @param {Function} [listener] The specific listener to remove. If not provided, removes all listeners for the specified event type from the specified emitter.
|
|
243
|
+
*
|
|
244
|
+
* **Behavior based on parameters:**
|
|
245
|
+
* - `stopListening()` - Stops listening to ALL events from ALL emitters
|
|
246
|
+
* - `stopListening(emitter)` - Stops listening to all events from the specified emitter
|
|
247
|
+
* - `stopListening(emitter, type)` - Stops listening to the specified event type from the specified emitter
|
|
248
|
+
* - `stopListening(emitter, type, listener)` - Stops listening to the specific listener for the specific event from the specific emitter
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* // Stop listening to all events from all emitters (complete cleanup)
|
|
252
|
+
* this.stopListening();
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* // Stop listening to all events from a specific emitter
|
|
256
|
+
* this.stopListening(otherModel);
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* // Stop listening to 'change' events from a specific emitter
|
|
260
|
+
* this.stopListening(otherModel, 'change');
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* // Stop listening to a specific listener
|
|
264
|
+
* const myListener = () => console.log('changed');
|
|
265
|
+
* this.listenTo(otherModel, 'change', myListener);
|
|
266
|
+
* this.stopListening(otherModel, 'change', myListener);
|
|
267
|
+
*/
|
|
268
|
+
stopListening(emitter, type, listener) {
|
|
269
|
+
// No listeningTo object.
|
|
270
|
+
if (!this.listeningTo) return;
|
|
271
|
+
// Remove listener from the array of listeners.
|
|
272
|
+
this.listeningTo = this.listeningTo.filter(item => {
|
|
273
|
+
if (
|
|
274
|
+
!emitter ||
|
|
275
|
+
(emitter === item.emitter && !type) ||
|
|
276
|
+
(emitter === item.emitter && type === item.type && !listener) ||
|
|
277
|
+
(emitter === item.emitter && type === item.type && listener === item.listener)
|
|
278
|
+
) {
|
|
279
|
+
item.emitter.off(item.type, item.listener);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
// Remove listeningTo object if it's empty.
|
|
285
|
+
if (!this.listeningTo.length) delete this.listeningTo;
|
|
132
286
|
}
|
|
133
287
|
}
|
|
134
288
|
|
package/es/Model.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Emitter from './Emitter.js';
|
|
2
2
|
import getResult from './utils/getResult.js';
|
|
3
|
+
import './utils/validateListener.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* - Orchestrates data and business logic.
|
|
@@ -8,55 +9,113 @@ import getResult from './utils/getResult.js';
|
|
|
8
9
|
* A `Model` manages an internal table of data attributes and triggers change events when any of its data is modified.
|
|
9
10
|
* Models may handle syncing data with a persistence layer. To design your models, create atomic, reusable objects
|
|
10
11
|
* that contain all the necessary functions for manipulating their specific data.
|
|
11
|
-
* Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
|
|
13
|
+
*
|
|
14
|
+
* ## Construction Flow
|
|
15
|
+
* 1. `preinitialize()` is called with all constructor arguments
|
|
16
|
+
* 2. `this.defaults` are resolved (if function, it's called and bound to the model)
|
|
17
|
+
* 3. `parse()` is called with all constructor arguments to process the data
|
|
18
|
+
* 4. `this.attributes` is built by merging defaults and parsed data
|
|
19
|
+
* 5. Getters/setters are generated for each attribute to emit change events
|
|
20
|
+
*
|
|
16
21
|
* @module
|
|
17
|
-
* @extends
|
|
18
|
-
* @param {object} attributes
|
|
19
|
-
* @
|
|
22
|
+
* @extends Emitter
|
|
23
|
+
* @param {object} [attributes={}] Primary data object containing model attributes
|
|
24
|
+
* @param {...*} [args] Additional arguments passed to `preinitialize` and `parse` methods
|
|
25
|
+
* @property {object|Function} defaults Default attributes for the model. If a function, it's called bound to the model instance to get defaults.
|
|
20
26
|
* @property {object} previous Object containing previous attributes when a change occurs.
|
|
27
|
+
* @property {string} attributePrefix Static property that defines a prefix for generated getters/setters. Defaults to empty string.
|
|
21
28
|
* @example
|
|
22
29
|
* import { Model } from 'rasti';
|
|
23
|
-
*
|
|
24
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* // User model
|
|
32
|
+
* class User extends Model {
|
|
25
33
|
* preinitialize() {
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* this.defaults = { name : '', email : '', role : 'user' };
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* // Order model with nested User and custom methods
|
|
38
|
+
* class Order extends Model {
|
|
39
|
+
* preinitialize(attributes, options = {}) {
|
|
30
40
|
* this.defaults = {
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* id : null,
|
|
42
|
+
* total : 0,
|
|
43
|
+
* status : 'pending',
|
|
44
|
+
* user : null
|
|
33
45
|
* };
|
|
46
|
+
*
|
|
47
|
+
* this.apiUrl = options.apiUrl || '/api/orders';
|
|
34
48
|
* }
|
|
35
49
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* //
|
|
40
|
-
*
|
|
41
|
-
*
|
|
50
|
+
* parse(data, options = {}) {
|
|
51
|
+
* const parsed = { ...data };
|
|
52
|
+
*
|
|
53
|
+
* // Convert user object to User model instance
|
|
54
|
+
* if (data.user && !(data.user instanceof User)) {
|
|
55
|
+
* parsed.user = new User(data.user);
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* return parsed;
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* toJSON() {
|
|
62
|
+
* const result = {};
|
|
63
|
+
* for (const [key, value] of Object.entries(this.attributes)) {
|
|
64
|
+
* if (value instanceof Model) {
|
|
65
|
+
* result[key] = value.toJSON();
|
|
66
|
+
* } else {
|
|
67
|
+
* result[key] = value;
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
70
|
+
* return result;
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* async fetch() {
|
|
74
|
+
* try {
|
|
75
|
+
* const response = await fetch(`${this.apiUrl}/${this.id}`);
|
|
76
|
+
* const data = await response.json();
|
|
77
|
+
*
|
|
78
|
+
* // Parse the fetched data and update model
|
|
79
|
+
* const parsed = this.parse(data);
|
|
80
|
+
* this.set(parsed, { source : 'fetch' });
|
|
81
|
+
*
|
|
82
|
+
* return this;
|
|
83
|
+
* } catch (error) {
|
|
84
|
+
* console.error('Failed to fetch order:', error);
|
|
85
|
+
* throw error;
|
|
86
|
+
* }
|
|
42
87
|
* }
|
|
43
88
|
* }
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
89
|
+
*
|
|
90
|
+
* // Create order with nested user data
|
|
91
|
+
* const order = new Order({
|
|
92
|
+
* id : 123,
|
|
93
|
+
* total : 99.99,
|
|
94
|
+
* user : { name : 'Alice', email : 'alice@example.com' }
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* console.log(order.user instanceof User); // true
|
|
98
|
+
* // Serialize with nested models
|
|
99
|
+
* const json = order.toJSON();
|
|
100
|
+
* console.log(json); // { id: 123, total: 99.99, status: 'pending', user: { name: 'Alice', email: 'alice@example.com', role: 'user' } }
|
|
101
|
+
*
|
|
102
|
+
* // Listen to fetch updates
|
|
103
|
+
* order.on('change', (model, changed, options) => {
|
|
104
|
+
* if (options?.source === 'fetch') {
|
|
105
|
+
* console.log('Order updated from server:', changed);
|
|
106
|
+
* }
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* // Fetch latest data from server
|
|
110
|
+
* await order.fetch();
|
|
50
111
|
*/
|
|
51
112
|
class Model extends Emitter {
|
|
52
|
-
constructor(
|
|
113
|
+
constructor() {
|
|
53
114
|
super();
|
|
54
115
|
// Call preinitialize.
|
|
55
116
|
this.preinitialize.apply(this, arguments);
|
|
56
|
-
// Get defaults. If `this.defaults` is a function, call it.
|
|
57
|
-
const defaults = getResult(this.defaults, this) || {};
|
|
58
117
|
// Set attributes object with defaults and passed attributes.
|
|
59
|
-
this.attributes = Object.assign({}, defaults,
|
|
118
|
+
this.attributes = Object.assign({}, getResult(this.defaults, this), this.parse.apply(this, arguments));
|
|
60
119
|
// Object to store previous attributes when a change occurs.
|
|
61
120
|
this.previous = {};
|
|
62
121
|
// Generate getters/setters for every attribute.
|
|
@@ -64,21 +123,53 @@ class Model extends Emitter {
|
|
|
64
123
|
}
|
|
65
124
|
|
|
66
125
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
126
|
+
* Called before any instantiation logic runs for the Model.
|
|
127
|
+
* Receives all constructor arguments, allowing for flexible initialization patterns.
|
|
128
|
+
* Use this to set up `defaults`, configure the model, or handle custom constructor arguments.
|
|
129
|
+
* @param {object} [attributes={}] Primary data object containing model attributes
|
|
130
|
+
* @param {...*} [args] Additional arguments passed from the constructor
|
|
131
|
+
* @example
|
|
132
|
+
* class User extends Model {
|
|
133
|
+
* preinitialize(attributes, options = {}) {
|
|
134
|
+
* this.defaults = { name : '', role : options.defaultRole || 'user' };
|
|
135
|
+
* this.apiEndpoint = options.apiEndpoint || '/users';
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* const user = new User({ name : 'Alice' }, { defaultRole : 'admin', apiEndpoint : '/api/users' });
|
|
69
139
|
*/
|
|
70
140
|
preinitialize() {}
|
|
71
141
|
|
|
72
142
|
/**
|
|
73
|
-
* Generate getter/setter for the given key
|
|
74
|
-
*
|
|
75
|
-
* for `this.attributes`.
|
|
76
|
-
*
|
|
143
|
+
* Generate getter/setter for the given attribute key to emit `change` events.
|
|
144
|
+
* The property name uses `attributePrefix` + key (e.g., with prefix 'attr_', key 'name' becomes 'attr_name').
|
|
145
|
+
* Called internally by the constructor for each key in `this.attributes`.
|
|
146
|
+
* Override with an empty method if you don't want automatic getters/setters.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} key Attribute key from `this.attributes`
|
|
149
|
+
* @example
|
|
150
|
+
* // Custom prefix for all attributes
|
|
151
|
+
* class PrefixedModel extends Model {
|
|
152
|
+
* static attributePrefix = 'attr_';
|
|
153
|
+
* }
|
|
154
|
+
* const model = new PrefixedModel({ name: 'Alice' });
|
|
155
|
+
* console.log(model.attr_name); // 'Alice'
|
|
156
|
+
*
|
|
157
|
+
* // Disable automatic getters/setters
|
|
158
|
+
* class ManualModel extends Model {
|
|
159
|
+
* defineAttribute() {
|
|
160
|
+
* // Empty - no getters/setters generated
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* getName() {
|
|
164
|
+
* return this.get('name'); // Manual getter
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
77
167
|
*/
|
|
78
168
|
defineAttribute(key) {
|
|
79
169
|
Object.defineProperty(
|
|
80
170
|
this,
|
|
81
|
-
key
|
|
171
|
+
`${this.constructor.attributePrefix}${key}`,
|
|
172
|
+
{
|
|
82
173
|
get : () => this.get(key),
|
|
83
174
|
set : (value) => { this.set(key, value); }
|
|
84
175
|
}
|
|
@@ -96,18 +187,28 @@ class Model extends Emitter {
|
|
|
96
187
|
}
|
|
97
188
|
|
|
98
189
|
/**
|
|
99
|
-
* Set
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @
|
|
107
|
-
* @
|
|
108
|
-
* @
|
|
109
|
-
* @
|
|
110
|
-
*
|
|
190
|
+
* Set one or more attributes into `this.attributes` and emit change events.
|
|
191
|
+
* Supports two call signatures: `set(key, value, ...args)` or `set(object, ...args)`.
|
|
192
|
+
* Additional arguments are passed to change event listeners, enabling custom behavior.
|
|
193
|
+
*
|
|
194
|
+
* @param {string|object} key Attribute key (string) or object containing key-value pairs
|
|
195
|
+
* @param {*} [value] Attribute value (when key is string)
|
|
196
|
+
* @param {...*} [args] Additional arguments passed to event listeners
|
|
197
|
+
* @return {Model} This model instance for chaining
|
|
198
|
+
* @emits change Emitted when any attribute changes. Listeners receive `(model, changedAttributes, ...args)`
|
|
199
|
+
* @emits change:attribute Emitted for each changed attribute. Listeners receive `(model, newValue, ...args)`
|
|
200
|
+
* @example
|
|
201
|
+
* // Basic usage
|
|
202
|
+
* model.set('name', 'Alice');
|
|
203
|
+
* model.set({ name : 'Alice', age : 30 });
|
|
204
|
+
*
|
|
205
|
+
* // With options for listeners
|
|
206
|
+
* model.set('name', 'Bob', { silent : false, validate : true });
|
|
207
|
+
* model.on('change:name', (model, value, options) => {
|
|
208
|
+
* if (options?.validate) {
|
|
209
|
+
* // Custom validation logic
|
|
210
|
+
* }
|
|
211
|
+
* });
|
|
111
212
|
*/
|
|
112
213
|
set(key, value, ...rest) {
|
|
113
214
|
let attrs, args;
|
|
@@ -161,14 +262,99 @@ class Model extends Emitter {
|
|
|
161
262
|
return this;
|
|
162
263
|
}
|
|
163
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Transforms and validates data before it becomes model attributes.
|
|
267
|
+
* Called during construction with all constructor arguments, allowing flexible data processing.
|
|
268
|
+
* Override this method to transform incoming data, create nested models, or handle different data formats.
|
|
269
|
+
*
|
|
270
|
+
* @param {object} [data={}] Primary data object to be parsed into attributes
|
|
271
|
+
* @param {...*} [args] Additional arguments from constructor, useful for parsing options
|
|
272
|
+
* @return {object} Processed data that will become the model's attributes
|
|
273
|
+
* @example
|
|
274
|
+
* // Transform nested objects into models
|
|
275
|
+
* class User extends Model {}
|
|
276
|
+
* class Order extends Model {
|
|
277
|
+
* parse(data, options = {}) {
|
|
278
|
+
* // Skip parsing if requested
|
|
279
|
+
* if (options.raw) return data;
|
|
280
|
+
* // Transform user data into User model
|
|
281
|
+
* const parsed = { ...data };
|
|
282
|
+
* if (data.user && !(data.user instanceof User)) {
|
|
283
|
+
* parsed.user = new User(data.user);
|
|
284
|
+
* }
|
|
285
|
+
* return parsed;
|
|
286
|
+
* }
|
|
287
|
+
* }
|
|
288
|
+
*
|
|
289
|
+
* // Usage with parsing options
|
|
290
|
+
* const order1 = new Order({ id : 1, user : { name : 'Alice' } }); // user becomes User model
|
|
291
|
+
* const order2 = new Order({ id : 2, user : { name : 'Bob' } }, { raw : true }); // user stays plain object
|
|
292
|
+
*/
|
|
293
|
+
parse(data) {
|
|
294
|
+
return data;
|
|
295
|
+
}
|
|
296
|
+
|
|
164
297
|
/**
|
|
165
298
|
* Return object representation of the model to be used for JSON serialization.
|
|
166
299
|
* By default returns a copy of `this.attributes`.
|
|
300
|
+
* You can override this method to customize serialization behavior, such as calling `toJSON` recursively on nested Model instances.
|
|
167
301
|
* @return {object} Object representation of the model to be used for JSON serialization.
|
|
302
|
+
* @example
|
|
303
|
+
* // Basic usage - returns a copy of model attributes:
|
|
304
|
+
* const user = new Model({ name : 'Alice', age : 30 });
|
|
305
|
+
* const json = user.toJSON();
|
|
306
|
+
* console.log(json); // { name : 'Alice', age : 30 }
|
|
307
|
+
*
|
|
308
|
+
* // Override toJSON for recursive serialization of nested models:
|
|
309
|
+
* class User extends Model {}
|
|
310
|
+
* class Order extends Model {
|
|
311
|
+
* parse(data) {
|
|
312
|
+
* // Ensure user is always a User model
|
|
313
|
+
* return { ...data, user : data.user instanceof User ? data.user : new User(data.user) };
|
|
314
|
+
* }
|
|
315
|
+
*
|
|
316
|
+
* toJSON() {
|
|
317
|
+
* const result = {};
|
|
318
|
+
* for (const [key, value] of Object.entries(this.attributes)) {
|
|
319
|
+
* if (value instanceof Model) {
|
|
320
|
+
* result[key] = value.toJSON();
|
|
321
|
+
* } else {
|
|
322
|
+
* result[key] = value;
|
|
323
|
+
* }
|
|
324
|
+
* }
|
|
325
|
+
* return result;
|
|
326
|
+
* }
|
|
327
|
+
* }
|
|
328
|
+
* const order = new Order({ id : 1, user : { name : 'Alice' } });
|
|
329
|
+
* const json = order.toJSON();
|
|
330
|
+
* console.log(json); // { id : 1, user : { name : 'Alice' } }
|
|
168
331
|
*/
|
|
169
332
|
toJSON() {
|
|
170
333
|
return Object.assign({}, this.attributes);
|
|
171
334
|
}
|
|
172
335
|
}
|
|
173
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Static property that defines a prefix for generated getters/setters.
|
|
339
|
+
* When set, all attribute properties will be prefixed (e.g., 'attr_name' instead of 'name').
|
|
340
|
+
* Useful for avoiding naming conflicts or creating a consistent property naming convention.
|
|
341
|
+
* @type {string}
|
|
342
|
+
* @default ''
|
|
343
|
+
* @example
|
|
344
|
+
* // Set prefix for all models of this class
|
|
345
|
+
* class ApiModel extends Model {
|
|
346
|
+
* static attributePrefix = 'attr_';
|
|
347
|
+
* }
|
|
348
|
+
*
|
|
349
|
+
* const user = new ApiModel({ name : 'Alice', email : 'alice@example.com' });
|
|
350
|
+
* console.log(user.attr_name); // 'Alice'
|
|
351
|
+
* console.log(user.attr_email); // 'alice@example.com'
|
|
352
|
+
*
|
|
353
|
+
* // Still access via get/set methods without prefix
|
|
354
|
+
* console.log(user.get('name')); // 'Alice'
|
|
355
|
+
* user.set('name', 'Bob');
|
|
356
|
+
* console.log(user.attr_name); // 'Bob'
|
|
357
|
+
*/
|
|
358
|
+
Model.attributePrefix = '';
|
|
359
|
+
|
|
174
360
|
export { Model as default };
|