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