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/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 {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 @@ 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,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} [...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
  }
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
- * 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.
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 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.
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
- * // Product model
24
- * class ProductModel extends Model {
30
+ *
31
+ * // User model
32
+ * class User extends Model {
25
33
  * 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.
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
- * name: '',
32
- * price: 0
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
- * 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;
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
- * // 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"
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(attributes = {}) {
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, attributes);
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
- * 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`.
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. 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.
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 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
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 };