rasti 3.0.0 → 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 +45 -14
  2. package/dist/rasti.js +1854 -679
  3. package/dist/rasti.min.js +1 -1
  4. package/es/Component.js +770 -479
  5. package/es/Emitter.js +182 -28
  6. package/es/Model.js +237 -51
  7. package/es/View.js +95 -53
  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 +771 -480
  25. package/lib/Emitter.cjs +182 -28
  26. package/lib/Model.cjs +237 -51
  27. package/lib/View.cjs +95 -53
  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 +767 -478
  46. package/src/Emitter.js +182 -28
  47. package/src/Model.js +236 -51
  48. package/src/View.js +95 -53
  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/src/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 @@ export default class Emitter {
39
68
  /**
40
69
  * Adds event listener.
41
70
  * @param {string} type Type of the event (e.g. `change`).
42
- * @param {function} listener Callback function to be called when the event is emitted.
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
- if (typeof listener !== 'function') {
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 @@ export default 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 {function} listener Callback function to be called when the event is emitted.
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
- // If listener is a function, wrap it to remove it after it is called.
71
- if (typeof listener === 'function') {
72
- const self = this;
73
- const originalListener = listener;
74
-
75
- listener = function(...args) {
76
- originalListener(...args);
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, listener);
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 is not provided, it removes all listeners.
87
- * @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.
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
- * // Stop listening to changes.
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,20 +157,131 @@ export default 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} [...args] Arguments to be passed to listener.
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
- .slice()
129
- .forEach(function(fn) {
130
- fn(...args);
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
  }
package/src/Model.js CHANGED
@@ -8,55 +8,113 @@ import getResult from './utils/getResult.js';
8
8
  * A `Model` manages an internal table of data attributes and triggers change events when any of its data is modified.
9
9
  * Models may handle syncing data with a persistence layer. To design your models, create atomic, reusable objects
10
10
  * 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
- * Rasti models store their attributes in `this.attributes`, which is extended from `this.defaults` and the
13
- * constructor `attributes` parameter. For every attribute, a getter is generated to retrieve the model property
14
- * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
15
- * and `change:attribute` events.
11
+ * Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
12
+ *
13
+ * ## Construction Flow
14
+ * 1. `preinitialize()` is called with all constructor arguments
15
+ * 2. `this.defaults` are resolved (if function, it's called and bound to the model)
16
+ * 3. `parse()` is called with all constructor arguments to process the data
17
+ * 4. `this.attributes` is built by merging defaults and parsed data
18
+ * 5. Getters/setters are generated for each attribute to emit change events
19
+ *
16
20
  * @module
17
- * @extends Rasti.Emitter
18
- * @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.
19
- * @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.
21
+ * @extends Emitter
22
+ * @param {object} [attributes={}] Primary data object containing model attributes
23
+ * @param {...*} [args] Additional arguments passed to `preinitialize` and `parse` methods
24
+ * @property {object|Function} defaults Default attributes for the model. If a function, it's called bound to the model instance to get defaults.
20
25
  * @property {object} previous Object containing previous attributes when a change occurs.
26
+ * @property {string} attributePrefix Static property that defines a prefix for generated getters/setters. Defaults to empty string.
21
27
  * @example
22
28
  * import { Model } from 'rasti';
23
- * // Product model
24
- * class ProductModel extends Model {
29
+ *
30
+ * // User model
31
+ * class User extends Model {
25
32
  * preinitialize() {
26
- * // The Product model has `name` and `price` default attributes.
27
- * // `defaults` will extend `this.attributes`.
28
- * // Getters and setters are generated for `this.attributes`,
29
- * // in order to emit `change` events.
33
+ * this.defaults = { name : '', email : '', role : 'user' };
34
+ * }
35
+ * }
36
+ * // Order model with nested User and custom methods
37
+ * class Order extends Model {
38
+ * preinitialize(attributes, options = {}) {
30
39
  * this.defaults = {
31
- * name: '',
32
- * price: 0
40
+ * id : null,
41
+ * total : 0,
42
+ * status : 'pending',
43
+ * user : null
33
44
  * };
45
+ *
46
+ * this.apiUrl = options.apiUrl || '/api/orders';
34
47
  * }
35
48
  *
36
- * setDiscount(discountPercentage) {
37
- * // Apply a discount to the price property.
38
- * // This will call a setter that will update `price` in `this.attributes`,
39
- * // and emit `change` and `change:price` events.
40
- * const discount = this.price * (discountPercentage / 100);
41
- * this.price -= discount;
49
+ * parse(data, options = {}) {
50
+ * const parsed = { ...data };
51
+ *
52
+ * // Convert user object to User model instance
53
+ * if (data.user && !(data.user instanceof User)) {
54
+ * parsed.user = new User(data.user);
55
+ * }
56
+ *
57
+ * return parsed;
58
+ * }
59
+ *
60
+ * toJSON() {
61
+ * const result = {};
62
+ * for (const [key, value] of Object.entries(this.attributes)) {
63
+ * if (value instanceof Model) {
64
+ * result[key] = value.toJSON();
65
+ * } else {
66
+ * result[key] = value;
67
+ * }
68
+ * }
69
+ * return result;
70
+ * }
71
+ *
72
+ * async fetch() {
73
+ * try {
74
+ * const response = await fetch(`${this.apiUrl}/${this.id}`);
75
+ * const data = await response.json();
76
+ *
77
+ * // Parse the fetched data and update model
78
+ * const parsed = this.parse(data);
79
+ * this.set(parsed, { source : 'fetch' });
80
+ *
81
+ * return this;
82
+ * } catch (error) {
83
+ * console.error('Failed to fetch order:', error);
84
+ * throw error;
85
+ * }
42
86
  * }
43
87
  * }
44
- * // Create a product instance with a name and price.
45
- * const product = new ProductModel({ name: 'Smartphone', price: 1000 });
46
- * // Listen to the `change:price` event.
47
- * product.on('change:price', () => console.log('New Price:', product.price));
48
- * // Apply a 10% discount to the product.
49
- * product.setDiscount(10); // Output: "New Price: 900"
88
+ *
89
+ * // Create order with nested user data
90
+ * const order = new Order({
91
+ * id : 123,
92
+ * total : 99.99,
93
+ * user : { name : 'Alice', email : 'alice@example.com' }
94
+ * });
95
+ *
96
+ * console.log(order.user instanceof User); // true
97
+ * // Serialize with nested models
98
+ * const json = order.toJSON();
99
+ * console.log(json); // { id: 123, total: 99.99, status: 'pending', user: { name: 'Alice', email: 'alice@example.com', role: 'user' } }
100
+ *
101
+ * // Listen to fetch updates
102
+ * order.on('change', (model, changed, options) => {
103
+ * if (options?.source === 'fetch') {
104
+ * console.log('Order updated from server:', changed);
105
+ * }
106
+ * });
107
+ *
108
+ * // Fetch latest data from server
109
+ * await order.fetch();
50
110
  */
51
111
  export default class Model extends Emitter {
52
- constructor(attributes = {}) {
112
+ constructor() {
53
113
  super();
54
114
  // Call preinitialize.
55
115
  this.preinitialize.apply(this, arguments);
56
- // Get defaults. If `this.defaults` is a function, call it.
57
- const defaults = getResult(this.defaults, this) || {};
58
116
  // Set attributes object with defaults and passed attributes.
59
- this.attributes = Object.assign({}, defaults, attributes);
117
+ this.attributes = Object.assign({}, getResult(this.defaults, this), this.parse.apply(this, arguments));
60
118
  // Object to store previous attributes when a change occurs.
61
119
  this.previous = {};
62
120
  // Generate getters/setters for every attribute.
@@ -64,21 +122,53 @@ export default class Model extends Emitter {
64
122
  }
65
123
 
66
124
  /**
67
- * 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.
68
- * @param {object} attributes Object containing model attributes to extend `this.attributes`.
125
+ * Called before any instantiation logic runs for the Model.
126
+ * Receives all constructor arguments, allowing for flexible initialization patterns.
127
+ * Use this to set up `defaults`, configure the model, or handle custom constructor arguments.
128
+ * @param {object} [attributes={}] Primary data object containing model attributes
129
+ * @param {...*} [args] Additional arguments passed from the constructor
130
+ * @example
131
+ * class User extends Model {
132
+ * preinitialize(attributes, options = {}) {
133
+ * this.defaults = { name : '', role : options.defaultRole || 'user' };
134
+ * this.apiEndpoint = options.apiEndpoint || '/users';
135
+ * }
136
+ * }
137
+ * const user = new User({ name : 'Alice' }, { defaultRole : 'admin', apiEndpoint : '/api/users' });
69
138
  */
70
139
  preinitialize() {}
71
140
 
72
141
  /**
73
- * Generate getter/setter for the given key. In order to emit `change` events.
74
- * This method is called internally by the constructor
75
- * for `this.attributes`.
76
- * @param {string} key Attribute key.
142
+ * Generate getter/setter for the given attribute key to emit `change` events.
143
+ * The property name uses `attributePrefix` + key (e.g., with prefix 'attr_', key 'name' becomes 'attr_name').
144
+ * Called internally by the constructor for each key in `this.attributes`.
145
+ * Override with an empty method if you don't want automatic getters/setters.
146
+ *
147
+ * @param {string} key Attribute key from `this.attributes`
148
+ * @example
149
+ * // Custom prefix for all attributes
150
+ * class PrefixedModel extends Model {
151
+ * static attributePrefix = 'attr_';
152
+ * }
153
+ * const model = new PrefixedModel({ name: 'Alice' });
154
+ * console.log(model.attr_name); // 'Alice'
155
+ *
156
+ * // Disable automatic getters/setters
157
+ * class ManualModel extends Model {
158
+ * defineAttribute() {
159
+ * // Empty - no getters/setters generated
160
+ * }
161
+ *
162
+ * getName() {
163
+ * return this.get('name'); // Manual getter
164
+ * }
165
+ * }
77
166
  */
78
167
  defineAttribute(key) {
79
168
  Object.defineProperty(
80
169
  this,
81
- key, {
170
+ `${this.constructor.attributePrefix}${key}`,
171
+ {
82
172
  get : () => this.get(key),
83
173
  set : (value) => { this.set(key, value); }
84
174
  }
@@ -96,18 +186,28 @@ export default class Model extends Emitter {
96
186
  }
97
187
 
98
188
  /**
99
- * Set an attribute into `this.attributes`.
100
- * Emit `change` and `change:attribute` if a value changes.
101
- * Could be called in two forms, `this.set('key', value)` and
102
- * `this.set({ key : value })`.
103
- * This method is called internally by generated setters.
104
- * 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.
105
- * The `change:attribute` event listener will receive the model instance, the new attribute value, and the rest of the arguments passed to `set` method.
106
- * @param {string} key Attribute key or object containing keys/values.
107
- * @param [value] Attribute value.
108
- * @return {this} This model.
109
- * @emits change
110
- * @emits change:attribute
189
+ * Set one or more attributes into `this.attributes` and emit change events.
190
+ * Supports two call signatures: `set(key, value, ...args)` or `set(object, ...args)`.
191
+ * Additional arguments are passed to change event listeners, enabling custom behavior.
192
+ *
193
+ * @param {string|object} key Attribute key (string) or object containing key-value pairs
194
+ * @param {*} [value] Attribute value (when key is string)
195
+ * @param {...*} [args] Additional arguments passed to event listeners
196
+ * @return {Model} This model instance for chaining
197
+ * @emits change Emitted when any attribute changes. Listeners receive `(model, changedAttributes, ...args)`
198
+ * @emits change:attribute Emitted for each changed attribute. Listeners receive `(model, newValue, ...args)`
199
+ * @example
200
+ * // Basic usage
201
+ * model.set('name', 'Alice');
202
+ * model.set({ name : 'Alice', age : 30 });
203
+ *
204
+ * // With options for listeners
205
+ * model.set('name', 'Bob', { silent : false, validate : true });
206
+ * model.on('change:name', (model, value, options) => {
207
+ * if (options?.validate) {
208
+ * // Custom validation logic
209
+ * }
210
+ * });
111
211
  */
112
212
  set(key, value, ...rest) {
113
213
  let attrs, args;
@@ -161,12 +261,97 @@ export default class Model extends Emitter {
161
261
  return this;
162
262
  }
163
263
 
264
+ /**
265
+ * Transforms and validates data before it becomes model attributes.
266
+ * Called during construction with all constructor arguments, allowing flexible data processing.
267
+ * Override this method to transform incoming data, create nested models, or handle different data formats.
268
+ *
269
+ * @param {object} [data={}] Primary data object to be parsed into attributes
270
+ * @param {...*} [args] Additional arguments from constructor, useful for parsing options
271
+ * @return {object} Processed data that will become the model's attributes
272
+ * @example
273
+ * // Transform nested objects into models
274
+ * class User extends Model {}
275
+ * class Order extends Model {
276
+ * parse(data, options = {}) {
277
+ * // Skip parsing if requested
278
+ * if (options.raw) return data;
279
+ * // Transform user data into User model
280
+ * const parsed = { ...data };
281
+ * if (data.user && !(data.user instanceof User)) {
282
+ * parsed.user = new User(data.user);
283
+ * }
284
+ * return parsed;
285
+ * }
286
+ * }
287
+ *
288
+ * // Usage with parsing options
289
+ * const order1 = new Order({ id : 1, user : { name : 'Alice' } }); // user becomes User model
290
+ * const order2 = new Order({ id : 2, user : { name : 'Bob' } }, { raw : true }); // user stays plain object
291
+ */
292
+ parse(data) {
293
+ return data;
294
+ }
295
+
164
296
  /**
165
297
  * Return object representation of the model to be used for JSON serialization.
166
298
  * By default returns a copy of `this.attributes`.
299
+ * You can override this method to customize serialization behavior, such as calling `toJSON` recursively on nested Model instances.
167
300
  * @return {object} Object representation of the model to be used for JSON serialization.
301
+ * @example
302
+ * // Basic usage - returns a copy of model attributes:
303
+ * const user = new Model({ name : 'Alice', age : 30 });
304
+ * const json = user.toJSON();
305
+ * console.log(json); // { name : 'Alice', age : 30 }
306
+ *
307
+ * // Override toJSON for recursive serialization of nested models:
308
+ * class User extends Model {}
309
+ * class Order extends Model {
310
+ * parse(data) {
311
+ * // Ensure user is always a User model
312
+ * return { ...data, user : data.user instanceof User ? data.user : new User(data.user) };
313
+ * }
314
+ *
315
+ * toJSON() {
316
+ * const result = {};
317
+ * for (const [key, value] of Object.entries(this.attributes)) {
318
+ * if (value instanceof Model) {
319
+ * result[key] = value.toJSON();
320
+ * } else {
321
+ * result[key] = value;
322
+ * }
323
+ * }
324
+ * return result;
325
+ * }
326
+ * }
327
+ * const order = new Order({ id : 1, user : { name : 'Alice' } });
328
+ * const json = order.toJSON();
329
+ * console.log(json); // { id : 1, user : { name : 'Alice' } }
168
330
  */
169
331
  toJSON() {
170
332
  return Object.assign({}, this.attributes);
171
333
  }
172
334
  }
335
+
336
+ /**
337
+ * Static property that defines a prefix for generated getters/setters.
338
+ * When set, all attribute properties will be prefixed (e.g., 'attr_name' instead of 'name').
339
+ * Useful for avoiding naming conflicts or creating a consistent property naming convention.
340
+ * @type {string}
341
+ * @default ''
342
+ * @example
343
+ * // Set prefix for all models of this class
344
+ * class ApiModel extends Model {
345
+ * static attributePrefix = 'attr_';
346
+ * }
347
+ *
348
+ * const user = new ApiModel({ name : 'Alice', email : 'alice@example.com' });
349
+ * console.log(user.attr_name); // 'Alice'
350
+ * console.log(user.attr_email); // 'alice@example.com'
351
+ *
352
+ * // Still access via get/set methods without prefix
353
+ * console.log(user.get('name')); // 'Alice'
354
+ * user.set('name', 'Bob');
355
+ * console.log(user.attr_name); // 'Bob'
356
+ */
357
+ Model.attributePrefix = '';