rasti 2.0.0-alpha.0 → 2.0.0-alpha.10

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/es/Component.js CHANGED
@@ -1,7 +1,7 @@
1
1
  function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
2
2
  function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }
3
3
  function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
4
- import View from './View';
4
+ import View from './View.js';
5
5
 
6
6
  // This options keys will be extended on view instance.
7
7
  var componentOptions = {
@@ -17,16 +17,24 @@ var componentOptions = {
17
17
  * @param text {string} html text
18
18
  * @return {object} Object with keys / values representing attributes.
19
19
  */
20
- var getAttributes = function getAttributes(text) {
20
+ var extractAttributes = function extractAttributes(text) {
21
21
  var attributes = {};
22
- var re = /([a-z]+)=?(\S+)?/gi;
22
+ var re = /([\w|data-]+)(?:=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?)?/g;
23
23
  var result;
24
24
  while ((result = re.exec(text)) !== null) {
25
- attributes[result[1]] = result[2] ? result[2].replace(/'|"/g, '') : true;
25
+ attributes[result[1]] = typeof result[2] === 'undefined' ? true : result[2];
26
26
  }
27
27
  return attributes;
28
28
  };
29
29
 
30
+ /*
31
+ * Helper function. If first arg is a placeholder for an expression, return the expression.
32
+ */
33
+ var getExpression = function getExpression(placeholder, expressions) {
34
+ var match = placeholder && placeholder.match && placeholder.match(new RegExp(Component.EXPRESSION_PLACEHOLDER_TEMPLATE('(\\d+)')));
35
+ return match && match[1] ? expressions[match[1]] : placeholder;
36
+ };
37
+
30
38
  /*
31
39
  * Helper function. If expression is a function, call it with context and args.
32
40
  */
@@ -41,14 +49,21 @@ var evalExpression = function evalExpression(expression, context) {
41
49
  * Components are a special kind of `View` that is designed to be easily composable,
42
50
  * making it simple to add child views and build complex user interfaces.<br />
43
51
  * Unlike views, which are render-agnostic, components have a specific set of rendering
44
- * guidelines that allow for a more declarative development style.
52
+ * guidelines that allow for a more declarative development style.<br />
53
+ * Components are defined with the `create` static method, which takes a tagged template.
45
54
  * @module
55
+ * @extends Rasti.View
46
56
  * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
57
+ * @property {string} key A unique key to identify the component. Used to recycle child components.
58
+ * @property {object} model A `Rasti.Model` or any emitter object containing data and business logic.
59
+ * @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state.
47
60
  * @example
48
61
  * import { Component, Model } from 'rasti';
49
62
  * // Create Timer component.
50
63
  * const Timer = Component.create`
51
- * <div>Seconds: <span>${({ model }) => model.seconds}</span></div>
64
+ * <div>
65
+ * Seconds: <span>${({ model }) => model.seconds}</span>
66
+ * </div>
52
67
  * `;
53
68
  * // Create model to store seconds.
54
69
  * const model = new Model({ seconds: 0 });
@@ -72,8 +87,9 @@ var Component = /*#__PURE__*/function (_View) {
72
87
  // Ensure id.
73
88
  _this.id = _this.attributes.id ?
74
89
  // If id is provided, evaluate it.
75
- evalExpression(_this.attributes.id, _assertThisInitialized(_this), _assertThisInitialized(_this)) : // Generate a unique id and set it as id attribute.
76
- "rasti-component-" + _this.uid;
90
+ evalExpression(_this.attributes.id, _assertThisInitialized(_this), _assertThisInitialized(_this)) :
91
+ // Generate a unique id and set it as id attribute.
92
+ Component.ID_TEMPLATE(_this.uid);
77
93
  // Bind onChange to this to be used as listener.
78
94
  // Store bound version, so it can be removed on onDestroy method.
79
95
  _this.onChange = _this.onChange.bind(_assertThisInitialized(_this));
@@ -93,29 +109,81 @@ var Component = /*#__PURE__*/function (_View) {
93
109
  _proto.ensureElement = function ensureElement() {
94
110
  // If el is provided, delegate events.
95
111
  if (this.el) this.delegateEvents();
96
- };
112
+ }
113
+
114
+ /*
115
+ * Find view's element on parent node, using id.
116
+ */;
97
117
  _proto.findElement = function findElement(parent) {
98
118
  return (parent || document).querySelector("#" + this.id);
99
119
  }
100
120
 
101
121
  /*
102
- * Used internally on the render process. Attach the view to the dom element.
122
+ * Eval attributes expressions.
103
123
  */;
104
- _proto.hydrate = function hydrate(parent) {
124
+ _proto.getAttributes = function getAttributes() {
105
125
  var _this2 = this;
126
+ var add = {};
127
+ var remove = {};
128
+ var attrs = [];
129
+ Object.keys(this.attributes).forEach(function (key) {
130
+ if (key === 'id') return;
131
+ // Evaluate attribute value.
132
+ var value = evalExpression(_this2.attributes[key], _this2, _this2);
133
+
134
+ // Transform bool attribute values
135
+ if (value === false) {
136
+ remove[key] = true;
137
+ } else if (value === true) {
138
+ add[key] = '';
139
+ attrs.push(key);
140
+ } else {
141
+ if (value === null || typeof value === 'undefined') value = '';
142
+ add[key] = value;
143
+ attrs.push(key + "=\"" + value + "\"");
144
+ }
145
+ });
146
+ add.id = this.id;
147
+ attrs.push("id=\"" + this.id + "\"");
148
+ return {
149
+ add: add,
150
+ remove: remove,
151
+ html: attrs.join(' ')
152
+ };
153
+ }
154
+
155
+ /*
156
+ * Used internally on the render process.
157
+ * Attach the view to the dom element.
158
+ */;
159
+ _proto.hydrate = function hydrate(parent) {
160
+ var _this3 = this;
106
161
  this.el = this.findElement(parent);
107
162
  this.delegateEvents();
108
163
  this.children.forEach(function (child) {
109
- return child.hydrate(_this2.el);
164
+ return child.hydrate(_this3.el);
110
165
  });
111
- this.onRender.call(this);
166
+ this.onRender.call(this, 'hydrate');
167
+ }
168
+
169
+ /*
170
+ * Used internally on the render process.
171
+ * Reuse a view that has `key` when its parent is rendered.
172
+ */;
173
+ _proto.recycle = function recycle(parent) {
174
+ // Find element to be replaced. It has same id.
175
+ var toBeReplaced = this.findElement(parent);
176
+ // Replace it with this.el.
177
+ toBeReplaced.replaceWith(this.el);
178
+ // Call `onRender` lifecycle method.
179
+ this.onRender.call(this, 'recycle');
112
180
  }
113
181
 
114
182
  /*
115
183
  * Override. Add some custom logic to super `destroy` method.
116
184
  */;
117
- _proto.destroy = function destroy(options) {
118
- _View.prototype.destroy.call(this, options);
185
+ _proto.destroy = function destroy() {
186
+ _View.prototype.destroy.apply(this, arguments);
119
187
  // Stop listening to `change`.
120
188
  // Set destroyed flag to prevent a last render after destroyed. TODO: Review
121
189
  if (this.model) this.model.off('change', this.onChange);
@@ -135,9 +203,9 @@ var Component = /*#__PURE__*/function (_View) {
135
203
  * This method should be extended with custom logic.
136
204
  * Maybe comparing new attributes with previous ones and calling
137
205
  * render when needed. Or doing some dom transformation.
138
- * @param model {Rasti.Model}
139
- * @param key {string}
140
- * @param value {any}
206
+ * @param model {Rasti.Model} The model that emitted the event.
207
+ * @param changed {object} Object containing keys and values that has changed.
208
+ * @param [...args] {any} Any extra arguments passed to set method.
141
209
  */;
142
210
  _proto.onChange = function onChange() {
143
211
  this.render();
@@ -145,131 +213,139 @@ var Component = /*#__PURE__*/function (_View) {
145
213
 
146
214
  /**
147
215
  * Lifecycle method. Called when the view is rendered.
216
+ * @param type {string} The render type. Can be `render`, `hydrate` or `recycle`.
148
217
  */;
149
218
  _proto.onRender = function onRender() {}
150
219
 
151
220
  /**
152
221
  * Lifecycle method. Called when the view is destroyed.
222
+ * @param {object} options Options object or any arguments passed to `destroy` method.
153
223
  */;
154
224
  _proto.onDestroy = function onDestroy() {}
155
225
 
156
226
  /*
157
227
  * Replace expressions.
158
228
  */;
159
- _proto.replaceExpressions = function replaceExpressions(string, parseExpressionResult) {
160
- var _this3 = this;
161
- return string.replace(/{(\d+)}/g, function (match) {
162
- // Get expression index.
163
- var idx = match.match(/{(\d+)}/)[1];
229
+ _proto.replaceExpressions = function replaceExpressions(string, addChild) {
230
+ var _this4 = this;
231
+ return string.replace(new RegExp(Component.EXPRESSION_PLACEHOLDER_TEMPLATE('(\\d+)'), 'g'), function (match) {
232
+ var expression = getExpression(match, _this4.template.expressions);
164
233
  // Eval expression. Pass view as argument.
165
- var result = _this3.template.expressions[idx].call(_this3, _this3);
234
+ var result = evalExpression(expression, _this4, _this4);
166
235
  // Treat all expressions as arrays.
167
236
  var results = result instanceof Array ? result : [result];
168
237
  // Replace expression with the result of the evaluation.
169
- return results.reduce(function (out, current) {
238
+ return results.reduce(function (out, result) {
239
+ var parsed;
240
+ // If result is true, replace it with a placeholder.
241
+ if (result === true) parsed = Component.TRUE_PLACEHOLDER;
242
+ // If result is false, replace it with a placeholder.
243
+ else if (result === false) parsed = Component.FALSE_PLACEHOLDER;
244
+ // Replace null or undefined with empty string.
245
+ else if (result === null || typeof result === 'undefined') parsed = '';
246
+ // If result is a view, call addChild callback.
247
+ else if (result && typeof result.render === 'function') parsed = addChild(result);
248
+ // Return expression itself.
249
+ else parsed = result;
170
250
  // Concatenate expressions.
171
- out += parseExpressionResult(current);
172
- return out;
251
+ return out + parsed;
173
252
  }, '');
174
253
  })
175
254
  // Replace `attribute="true"` with `attribute`
176
- .replace(/([a-z]+)=["|']true["|']/g, '$1')
177
- // Replace `attribute="false"` with ``
178
- .replace(/([a-z]+)=["|']false["|']/g, '');
255
+ .replace(new RegExp("([a-z]+)=[\"|']" + Component.TRUE_PLACEHOLDER + "[\"|']", 'g'), '$1')
256
+ // Replace `attribute="false"` with empty string.
257
+ .replace(new RegExp("([a-z]+)=[\"|']" + Component.FALSE_PLACEHOLDER + "[\"|']", 'g'), '')
258
+ // Replace rest of false expressions with empty string.
259
+ .replace(new RegExp(Component.FALSE_PLACEHOLDER, 'g'), '');
179
260
  }
180
261
 
181
262
  /*
182
263
  * Treat the whole view as a HTML string.
183
264
  */;
184
265
  _proto.toString = function toString() {
185
- var _this4 = this;
266
+ var _this5 = this;
186
267
  // Normally there won't be any children, but if there are, destroy them.
187
268
  this.destroyChildren();
188
- // Add id to template including root element (this.el) as part of it.
189
- var tpl = this.attributes.id ? this.template.outer : this.template.outer.replace(/^<([a-z]+)/, "<$1 id=\"" + this.id + "\"");
190
269
  // Replace expressions.
191
- return this.replaceExpressions(tpl, function (result) {
192
- var out = result;
193
- // Return empty string if result is null or undefined.
194
- if (result === null || typeof result === 'undefined') out = '';
195
- // If result is a view, add it to children array.
196
- else if (result && typeof result.render === 'function') {
197
- // Add child component.
198
- _this4.addChild(result);
199
- }
200
- return out;
270
+ var inner = this.template.inner && this.replaceExpressions(this.template.inner, function (component) {
271
+ // Add child component.
272
+ return _this5.addChild(component);
201
273
  });
274
+ // Generate outer template.
275
+ return inner ? "<" + this.tag + " " + this.getAttributes().html + ">" + inner + "</" + this.tag + ">" : "<" + this.tag + " " + this.getAttributes().html + " />";
202
276
  }
203
277
 
204
278
  /*
205
279
  * View render method.
206
280
  */;
207
281
  _proto.render = function render() {
208
- var _this5 = this;
282
+ var _this6 = this;
209
283
  // Prevent a last re render if view is already destroyed.
210
284
  if (this.destroyed) return this;
211
- // If "this.el" is not present,
212
- // create a new element according "this.tag"
213
- // and "this.attributes".
285
+ // If `this.el` is not present, create a new `this.tag` element.
214
286
  if (!this.el) {
215
287
  this.el = this.createElement(this.tag);
216
288
  this.delegateEvents();
217
289
  }
218
290
  // Set `this.el` attributes.
219
- Object.keys(this.attributes).forEach(function (key) {
220
- // Omit changing id.
221
- if (key === 'id') return;
222
- // Evaluate attribute value.
223
- var value = evalExpression(_this5.attributes[key], _this5, _this5);
224
- // Transform bool attribute values
225
- if (value === false) {
226
- // Remove false attributes.
227
- _this5.el.removeAttribute(key);
228
- } else {
229
- if (value === true) value = '';
230
- _this5.el.setAttribute(key, value);
231
- }
291
+ var attributes = this.getAttributes();
292
+ // Remove attributes.
293
+ Object.keys(attributes.remove).forEach(function (key) {
294
+ _this6.el.removeAttribute(key);
232
295
  });
233
- var previousChildren = this.children;
234
- this.children = [];
235
- var nextChildren = [];
236
- var recycledChildren = [];
237
-
238
- // Replace expressions.
239
- // Set html text inside `this.el`. Make it part of the dom.
240
- this.el.innerHTML = this.replaceExpressions(this.template.inner, function (result) {
241
- var out = result;
242
- // Return empty string if result is null or undefined.
243
- if (result === null || typeof result === 'undefined') out = '';
244
-
245
- // If result is a view, add it to children array.
246
- else if (result && typeof result.render === 'function') {
247
- var found = result.key && previousChildren.find(function (previousChild) {
248
- return previousChild.key === result.key;
296
+ // Add attributes.
297
+ Object.keys(attributes.add).forEach(function (key) {
298
+ _this6.el.setAttribute(key, attributes.add[key]);
299
+ });
300
+ // Check for `template.inner` to see if view has innerHTML.
301
+ if (this.template.inner) {
302
+ var previousChildren = this.children;
303
+ this.children = [];
304
+ var nextChildren = [];
305
+ var recycledChildren = [];
306
+ // Store active element.
307
+ var activeElement = document.activeElement;
308
+ // Replace expressions. Set html inside of `this.el`.
309
+ this.el.innerHTML = this.replaceExpressions(this.template.inner, function (component) {
310
+ var out = component;
311
+ // Check if child already exists.
312
+ var found = component.key && previousChildren.find(function (previousChild) {
313
+ return previousChild.key === component.key;
249
314
  });
250
315
  if (found) {
251
- out = "<" + found.tag + " id=\"" + evalExpression(found.id, found, found) + "\"></" + found.tag + ">";
316
+ // If child already exists, replace it html by its root element.
317
+ out = "<" + found.tag + " id=\"" + found.id + "\"></" + found.tag + ">";
318
+ // Add child to recycled children.
252
319
  recycledChildren.push(found);
253
- result.destroy();
320
+ // Destroy new child component. Use recycled one instead.
321
+ component.destroy();
254
322
  } else {
255
- nextChildren.push(result);
323
+ // Not found. Add new child component.
324
+ nextChildren.push(component);
256
325
  }
326
+ // Component html.
327
+ return out;
328
+ });
329
+ // Add new children. Hydrate them.
330
+ nextChildren.forEach(function (nextChild) {
331
+ _this6.addChild(nextChild).hydrate(_this6.el);
332
+ });
333
+ // Replace children root elements with recycled components.
334
+ recycledChildren.forEach(function (recycledChild) {
335
+ _this6.addChild(recycledChild).recycle(_this6.el);
336
+ });
337
+ // Destroy unused children.
338
+ previousChildren.forEach(function (previousChild) {
339
+ var found = recycledChildren.indexOf(previousChild) > -1;
340
+ if (!found) previousChild.destroy();
341
+ });
342
+ // Restore focus.
343
+ if (this.el.contains(activeElement)) {
344
+ activeElement.focus();
257
345
  }
258
- return out;
259
- });
260
- nextChildren.forEach(function (nextChild) {
261
- _this5.addChild(nextChild).hydrate(_this5.el);
262
- });
263
- recycledChildren.forEach(function (recycledChild) {
264
- _this5.el.replaceChild(_this5.addChild(recycledChild).el, recycledChild.findElement(_this5.el));
265
- });
266
- previousChildren.forEach(function (previousChild) {
267
- var found = recycledChildren.indexOf(previousChild) > -1;
268
- if (!found) previousChild.destroy();
269
- });
270
-
346
+ }
271
347
  // Call onRender lifecycle method.
272
- this.onRender.call(this);
348
+ this.onRender.call(this, 'render');
273
349
  // Return this for chaining.
274
350
  return this;
275
351
  }
@@ -301,7 +377,7 @@ var Component = /*#__PURE__*/function (_View) {
301
377
  * @param {object} options The view options.
302
378
  * @param {node} el Dom element to append the view element.
303
379
  * @param {boolean} hydrate If true, the view will use existing html.
304
- * @return {Rasti.View}
380
+ * @return {Rasti.Component}
305
381
  */;
306
382
  Component.mount = function mount() {
307
383
  var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -309,7 +385,6 @@ var Component = /*#__PURE__*/function (_View) {
309
385
  var hydrate = arguments.length > 2 ? arguments[2] : undefined;
310
386
  // Instantiate view.
311
387
  var view = new this(options);
312
-
313
388
  // If `el` is passed, mount component.
314
389
  if (el) {
315
390
  if (hydrate) {
@@ -323,12 +398,23 @@ var Component = /*#__PURE__*/function (_View) {
323
398
  }
324
399
  view.hydrate(el);
325
400
  }
401
+ // Return view instance.
326
402
  return view;
327
403
  }
328
404
  /**
329
- * Tagged template that receives an HTML string,
330
- * and returns a `Component`.
405
+ * Takes a tagged template containing an HTML string,
406
+ * and returns a new `Component` class.
407
+ * - The template outer tag and attributes will be used to create the view's root element.
408
+ * - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
409
+ * - Event handlers should be passed, at the root element, in the form of `onEventName=${{'selector' : listener }}`. Where `selector` is a css selector. The event will be delegated to the view's root element.
410
+ * - The template inner HTML will be used as the view's template.
411
+ * - Template interpolations that are functions will be evaluated on the render process. Receiving the view instance as argument. And being bound to it.
412
+ * - If the function returns `null`, `undefined`, `false` or empty string, the interpolation won't render any content.
413
+ * - If the function returns a component instance, it will be added as a child component.
414
+ * - If the function returns an array, each item will be evaluated as above.
331
415
  * @static
416
+ * @param {string} HTML template for the component.
417
+ * @return {Rasti.Component}
332
418
  */;
333
419
  Component.create = function create(strings) {
334
420
  for (var _len2 = arguments.length, expressions = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
@@ -342,25 +428,26 @@ var Component = /*#__PURE__*/function (_View) {
342
428
  parts.push(string);
343
429
  // Add expression placeholder for later or expression eval.
344
430
  if (expressions[i]) {
345
- parts.push(typeof expressions[i] === 'function' || typeof expressions[i] === 'object' ? "{" + i + "}" : expressions[i]);
431
+ parts.push(typeof expressions[i] === 'function' || typeof expressions[i] === 'object' ? Component.EXPRESSION_PLACEHOLDER_TEMPLATE(i) : expressions[i]);
346
432
  }
347
433
  });
348
434
  // Create output text for main template.
349
435
  var main = parts.join('').trim().replace(/\n/g, '');
350
436
  // Extract outer tag, attributes and inner html.
351
- var result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/);
352
- // Remove events listeners.
353
- var string = main.replace(/on([A-Z]{1}[a-z]+)+=[^>\s]+/g, '');
437
+ var result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/) || main.match(/^<([a-z]+)(.*?)\/>$/);
354
438
  // Parse attributes from html text into an object.
355
- var attributes = getAttributes(result[2]);
439
+ var attributes = extractAttributes(result[2]);
440
+ // Events to be delegated.
356
441
  var events = {};
357
- // Filter events. Replace placeholders `{number}` with expressions.
442
+ // Filter events. To generate events object.
443
+ // Generate attributes object, replace placeholders with expressions.
358
444
  attributes = Object.keys(attributes).reduce(function (out, key) {
359
- var matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/); // Is Event?
360
- var matchValue = attributes[key].match(/{(\d+)}/); // Is placeholder for function or object?
445
+ // Is Event?
446
+ var matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/);
447
+ // Is placeholder for function or object?
361
448
  // Get expression or value.
362
- var value = matchValue && matchValue[1] ? expressions[matchValue[1]] : attributes[key];
363
- // Is event handler.
449
+ var value = getExpression(attributes[key], expressions);
450
+ // Is event handler. Add to events object.
364
451
  if (matchKey && matchKey[1]) {
365
452
  var eventType = matchKey[1].toLowerCase();
366
453
  Object.keys(value).forEach(function (selector) {
@@ -368,22 +455,19 @@ var Component = /*#__PURE__*/function (_View) {
368
455
  });
369
456
  return out;
370
457
  }
371
- // Is function expression.
458
+ // Is attribute. Add to attributes object.
372
459
  out[key] = value;
373
460
  return out;
374
461
  }, {});
375
462
  var Current = this;
376
-
377
463
  // Create subclass for this component.
378
464
  return Current.extend({
379
465
  // Set events.
380
466
  events: events,
381
467
  // Set attributes.
382
468
  attributes: attributes,
383
- // Set template data.
469
+ // Set template.
384
470
  template: {
385
- // Template including root element (this.el) as part of the template.
386
- outer: string,
387
471
  // Template for innerHTML of root element.
388
472
  inner: result[3],
389
473
  // Template expressions.
@@ -395,4 +479,12 @@ var Component = /*#__PURE__*/function (_View) {
395
479
  };
396
480
  return Component;
397
481
  }(View);
398
- export { Component as default };
482
+ export { Component as default };
483
+ Component.ID_TEMPLATE = function (uid) {
484
+ return "rasti-component-" + uid;
485
+ };
486
+ Component.EXPRESSION_PLACEHOLDER_TEMPLATE = function (idx) {
487
+ return "__RASTI_EXPRESSION{" + idx + "}";
488
+ };
489
+ Component.TRUE_PLACEHOLDER = '__RASTI_TRUE';
490
+ Component.FALSE_PLACEHOLDER = '__RASTI_FALSE';
package/es/Model.js CHANGED
@@ -1,7 +1,7 @@
1
1
  function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
2
2
  function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }
3
3
  function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
4
- import Emitter from './Emitter';
4
+ import Emitter from './Emitter.js';
5
5
  /**
6
6
  * - Orchestrates data and business logic.
7
7
  * - Emits events when data changes.
@@ -15,6 +15,7 @@ import Emitter from './Emitter';
15
15
  * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
16
16
  * and `change:attribute` events.
17
17
  * @module
18
+ * @extends Rasti.Emitter
18
19
  * @param {object} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.
19
20
  * @example
20
21
  * import { Model } from 'rasti';
@@ -100,7 +101,7 @@ var Model = /*#__PURE__*/function (_Emitter) {
100
101
 
101
102
  /**
102
103
  * Set an attribute into `this.attributes`.
103
- * Emit `change` and `change:attribute` if value change.
104
+ * Emit `change` and `change:attribute` if a value change.
104
105
  * Could be called in two forms, `this.set('key', value)` and
105
106
  * `this.set({ key : value })`.
106
107
  * This method is called internally by generated setters.
@@ -111,19 +112,57 @@ var Model = /*#__PURE__*/function (_Emitter) {
111
112
  * @emits change:attribute
112
113
  */;
113
114
  _proto.set = function set(key, value) {
114
- var _ref,
115
- _this3 = this;
116
- var attrs = typeof key === 'object' ? key : (_ref = {}, _ref[key] = value, _ref);
115
+ var _this3 = this;
116
+ var attrs, args;
117
+ // Handle both `"key", value` and `{key: value}` style arguments.
118
+ for (var _len = arguments.length, rest = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
119
+ rest[_key - 2] = arguments[_key];
120
+ }
121
+ if (typeof key === 'object') {
122
+ attrs = key;
123
+ args = [value].concat(rest);
124
+ } else {
125
+ var _attrs;
126
+ attrs = (_attrs = {}, _attrs[key] = value, _attrs);
127
+ args = rest;
128
+ }
129
+ // Are we in a nested `set` call?
130
+ // Calling a `set` inside a `change:attribute` or `change` event listener
131
+ var changing = this._changing;
132
+ this._changing = true;
133
+ // Store changed attributes.
134
+ var changed = {};
135
+ // Store previous attributes.
136
+ if (!changing) {
137
+ this.previous = Object.assign({}, this.attributes);
138
+ }
139
+ // Set attributes.
117
140
  Object.keys(attrs).forEach(function (key) {
118
- var changed = key in _this3.attributes && attrs[key] !== _this3.attributes[key];
119
- _this3.previous[key] = _this3.attributes[key];
120
- _this3.attributes[key] = attrs[key];
121
- // Emit change events.
122
- if (changed) {
123
- _this3.emit('change', _this3, key, attrs[key]);
124
- _this3.emit("change:" + key, _this3, attrs[key]);
141
+ // Use equality to determine if value changed.
142
+ if (attrs[key] !== _this3.attributes[key]) {
143
+ changed[key] = attrs[key];
144
+ _this3.attributes[key] = attrs[key];
125
145
  }
126
146
  });
147
+ var changedKeys = Object.keys(changed);
148
+ // Pending `change` event arguments.
149
+ if (changedKeys.length) this._pending = ['change', this, changed].concat(args);
150
+ // Emit `change:attribute` events.
151
+ changedKeys.forEach(function (key) {
152
+ _this3.emit.apply(_this3, ["change:" + key, _this3, attrs[key]].concat(args));
153
+ });
154
+ // Don't emit `change` event until the end of the nested
155
+ // `set` calls inside `change:attribute` event listeners.
156
+ if (changing) return this;
157
+ // Emit `change` events, that might be nested.
158
+ while (this._pending) {
159
+ var pendingChange = this._pending;
160
+ this._pending = null;
161
+ this.emit.apply(this, pendingChange);
162
+ }
163
+ // Reset flags.
164
+ this._pending = null;
165
+ this._changing = false;
127
166
  return this;
128
167
  }
129
168