rasti 2.0.0-alpha.0 → 2.0.0-alpha.2

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/src/Component.js CHANGED
@@ -1,4 +1,4 @@
1
- import View from './View';
1
+ import View from './View.js';
2
2
 
3
3
  // This options keys will be extended on view instance.
4
4
  const componentOptions = {
@@ -27,6 +27,14 @@ const getAttributes = text => {
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
+ const getExpression = (placeholder, expressions) => {
34
+ const 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
  */
@@ -39,14 +47,21 @@ const evalExpression = (expression, context, ...args) =>
39
47
  * Components are a special kind of `View` that is designed to be easily composable,
40
48
  * making it simple to add child views and build complex user interfaces.<br />
41
49
  * Unlike views, which are render-agnostic, components have a specific set of rendering
42
- * guidelines that allow for a more declarative development style.
50
+ * guidelines that allow for a more declarative development style.<br />
51
+ * Components are defined with the `create` static method, which takes a tagged template.
43
52
  * @module
53
+ * @extends Rasti.View
44
54
  * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
55
+ * @property {string} key A unique key to identify the component. Used to recycle child components.
56
+ * @property {object} model A `Rasti.Model` or any emitter object containing data and business logic.
57
+ * @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state.
45
58
  * @example
46
59
  * import { Component, Model } from 'rasti';
47
60
  * // Create Timer component.
48
61
  * const Timer = Component.create`
49
- * <div>Seconds: <span>${({ model }) => model.seconds}</span></div>
62
+ * <div>
63
+ * Seconds: <span>${({ model }) => model.seconds}</span>
64
+ * </div>
50
65
  * `;
51
66
  * // Create model to store seconds.
52
67
  * const model = new Model({ seconds: 0 });
@@ -69,7 +84,7 @@ export default class Component extends View {
69
84
  // If id is provided, evaluate it.
70
85
  evalExpression(this.attributes.id, this, this) :
71
86
  // Generate a unique id and set it as id attribute.
72
- `rasti-component-${this.uid}`;
87
+ Component.ID_TEMPLATE(this.uid);
73
88
  // Bind onChange to this to be used as listener.
74
89
  // Store bound version, so it can be removed on onDestroy method.
75
90
  this.onChange = this.onChange.bind(this);
@@ -148,27 +163,38 @@ export default class Component extends View {
148
163
  /*
149
164
  * Replace expressions.
150
165
  */
151
- replaceExpressions(string, parseExpressionResult) {
166
+ replaceExpressions(string, addChild) {
152
167
  return string
153
- .replace(/{(\d+)}/g, (match) => {
154
- // Get expression index.
155
- const idx = match.match(/{(\d+)}/)[1];
168
+ .replace(new RegExp(Component.EXPRESSION_PLACEHOLDER_TEMPLATE('(\\d+)'), 'g'), (match) => {
169
+ const expression = getExpression(match, this.template.expressions);
156
170
  // Eval expression. Pass view as argument.
157
- const result = this.template.expressions[idx].call(this, this);
171
+ const result = evalExpression(expression, this, this);
158
172
  // Treat all expressions as arrays.
159
173
  const results = result instanceof Array ? result : [result];
160
174
  // Replace expression with the result of the evaluation.
161
- return results.reduce((out, current) => {
175
+ return results.reduce((out, result) => {
176
+ let parsed;
177
+ // If result is true, replace it with a placeholder.
178
+ if (result === true) parsed = Component.TRUE_PLACEHOLDER;
179
+ // If result is false, replace it with a placeholder.
180
+ else if (result === false) parsed = Component.FALSE_PLACEHOLDER;
181
+ // Replace null or undefined with empty string.
182
+ else if (result === null || typeof result === 'undefined') parsed = '';
183
+ // If result is a view, call addChild callback.
184
+ else if (result && typeof result.render === 'function') parsed = addChild(result);
185
+ // Return expression itself.
186
+ else parsed = result;
162
187
  // Concatenate expressions.
163
- out += parseExpressionResult(current);
164
- return out;
188
+ return out + parsed;
165
189
  }, '');
166
190
 
167
191
  })
168
192
  // Replace `attribute="true"` with `attribute`
169
- .replace(/([a-z]+)=["|']true["|']/g, '$1')
170
- // Replace `attribute="false"` with ``
171
- .replace(/([a-z]+)=["|']false["|']/g, '');
193
+ .replace(new RegExp(`([a-z]+)=["|']${Component.TRUE_PLACEHOLDER}["|']`, 'g'), '$1')
194
+ // Replace `attribute="false"` with empty string.
195
+ .replace(new RegExp(`([a-z]+)=["|']${Component.FALSE_PLACEHOLDER}["|']`, 'g'), '')
196
+ // Replace rest of false expressions with empty string.
197
+ .replace(new RegExp(Component.FALSE_PLACEHOLDER, 'g'), '');
172
198
  }
173
199
 
174
200
  /*
@@ -182,17 +208,9 @@ export default class Component extends View {
182
208
  this.template.outer :
183
209
  this.template.outer.replace(/^<([a-z]+)/, `<$1 id="${this.id}"`);
184
210
  // Replace expressions.
185
- return this.replaceExpressions(tpl, (result) => {
186
- let out = result;
187
- // Return empty string if result is null or undefined.
188
- if (result === null || typeof result === 'undefined') out = '';
189
- // If result is a view, add it to children array.
190
- else if (result && typeof result.render === 'function') {
191
- // Add child component.
192
- this.addChild(result);
193
- }
194
-
195
- return out;
211
+ return this.replaceExpressions(tpl, (component) => {
212
+ // Add child component.
213
+ return this.addChild(component);
196
214
  });
197
215
  }
198
216
 
@@ -225,50 +243,52 @@ export default class Component extends View {
225
243
  }
226
244
  });
227
245
 
228
- const previousChildren = this.children;
229
- this.children = [];
230
-
231
- const nextChildren = [];
232
- const recycledChildren = [];
233
-
234
- // Replace expressions.
235
- // Set html text inside `this.el`. Make it part of the dom.
236
- this.el.innerHTML = this.replaceExpressions(this.template.inner, (result) => {
237
- let out = result;
238
- // Return empty string if result is null or undefined.
239
- if (result === null || typeof result === 'undefined') out = '';
240
-
241
- // If result is a view, add it to children array.
242
- else if (result && typeof result.render === 'function') {
243
- const found = result.key && previousChildren.find(
244
- previousChild => previousChild.key === result.key
246
+ // Check for `template.inner`.
247
+ // Root element may be a self enclosed tag element without innerHTML.
248
+ if (this.template.inner) {
249
+ const previousChildren = this.children;
250
+ this.children = [];
251
+
252
+ const nextChildren = [];
253
+ const recycledChildren = [];
254
+
255
+ // Replace expressions.
256
+ // Set html text inside `this.el`. Make it part of the dom.
257
+ this.el.innerHTML = this.replaceExpressions(this.template.inner, (component) => {
258
+ let out = component;
259
+ // Check if child already exists.
260
+ const found = component.key && previousChildren.find(
261
+ previousChild => previousChild.key === component.key
245
262
  );
246
263
 
247
264
  if (found) {
248
- out = `<${found.tag} id="${evalExpression(found.id, found, found)}"></${found.tag}>`;
265
+ // If child already exists, replace it html by its root element.
266
+ out = `<${found.tag} id="${found.id}"></${found.tag}>`;
267
+ // Add child to recycled children.
249
268
  recycledChildren.push(found);
250
- result.destroy();
269
+ // Destroy new child component. Use recycled one instead.
270
+ component.destroy();
251
271
  } else {
252
- nextChildren.push(result);
272
+ // Not found. Add new child component.
273
+ nextChildren.push(component);
253
274
  }
254
- }
255
-
256
- return out;
257
- });
258
-
259
- nextChildren.forEach(nextChild => {
260
- this.addChild(nextChild).hydrate(this.el);
261
- });
262
-
263
- recycledChildren.forEach(recycledChild => {
264
- this.el.replaceChild(this.addChild(recycledChild).el, recycledChild.findElement(this.el));
265
- });
266
-
267
- previousChildren.forEach(previousChild => {
268
- const found = recycledChildren.indexOf(previousChild) > -1;
269
- if (!found) previousChild.destroy();
270
- });
271
-
275
+ // Component html.
276
+ return out;
277
+ });
278
+ // Add new children. Hydrate them.
279
+ nextChildren.forEach(nextChild => {
280
+ this.addChild(nextChild).hydrate(this.el);
281
+ });
282
+ // Replace children root elements with recycled components.
283
+ recycledChildren.forEach(recycledChild => {
284
+ this.el.replaceChild(this.addChild(recycledChild).el, recycledChild.findElement(this.el));
285
+ });
286
+ // Destroy unused children.
287
+ previousChildren.forEach(previousChild => {
288
+ const found = recycledChildren.indexOf(previousChild) > -1;
289
+ if (!found) previousChild.destroy();
290
+ });
291
+ }
272
292
  // Call onRender lifecycle method.
273
293
  this.onRender.call(this);
274
294
  // Return this for chaining.
@@ -302,7 +322,7 @@ export default class Component extends View {
302
322
  * @param {object} options The view options.
303
323
  * @param {node} el Dom element to append the view element.
304
324
  * @param {boolean} hydrate If true, the view will use existing html.
305
- * @return {Rasti.View}
325
+ * @return {Rasti.Component}
306
326
  */
307
327
  static mount(options = {}, el, hydrate) {
308
328
  // Instantiate view.
@@ -325,9 +345,19 @@ export default class Component extends View {
325
345
  return view;
326
346
  }
327
347
  /**
328
- * Tagged template that receives an HTML string,
329
- * and returns a `Component`.
348
+ * Takes a tagged template containing an HTML string,
349
+ * and returns a new `Component` class.
350
+ * - The template outer tag and attributes will be used to create the view's root element.
351
+ * - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
352
+ * - 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.
353
+ * - The template inner HTML will be used as the view's template.
354
+ * - Template interpolations that are functions will be evaluated on the render process. Receiving the view instance as argument. And being bound to it.
355
+ * - If the function returns `null`, `undefined`, `false` or empty string, the interpolation won't render any content.
356
+ * - If the function returns a component instance, it will be added as a child component.
357
+ * - If the function returns an array, each item will be evaluated as above.
330
358
  * @static
359
+ * @param {string} HTML template for the component.
360
+ * @return {Rasti.Component}
331
361
  */
332
362
  static create(strings, ...expressions) {
333
363
  const parts = [];
@@ -340,7 +370,7 @@ export default class Component extends View {
340
370
  if (expressions[i]) {
341
371
  parts.push(
342
372
  typeof expressions[i] === 'function' || typeof expressions[i] === 'object' ?
343
- `{${i}}` :
373
+ Component.EXPRESSION_PLACEHOLDER_TEMPLATE(i) :
344
374
  expressions[i]
345
375
  );
346
376
  }
@@ -348,19 +378,22 @@ export default class Component extends View {
348
378
  // Create output text for main template.
349
379
  const main = parts.join('').trim().replace(/\n/g, '');
350
380
  // Extract outer tag, attributes and inner html.
351
- const result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/);
381
+ const result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/) || main.match(/^<([a-z]+)(.*?)\/>$/);
352
382
  // Remove events listeners.
353
383
  const string = main.replace(/on([A-Z]{1}[a-z]+)+=[^>\s]+/g, '');
354
384
  // Parse attributes from html text into an object.
355
385
  let attributes = getAttributes(result[2]);
386
+ // Events to be delegated.
356
387
  let events = {};
357
- // Filter events. Replace placeholders `{number}` with expressions.
388
+ // Filter events. To generate events object.
389
+ // Generate attributes object, replace placeholders with expressions.
358
390
  attributes = Object.keys(attributes).reduce((out, key) => {
359
- const matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/); // Is Event?
360
- const matchValue = attributes[key].match(/{(\d+)}/); // Is placeholder for function or object?
391
+ // Is Event?
392
+ const matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/);
393
+ // Is placeholder for function or object?
361
394
  // Get expression or value.
362
- const value = matchValue && matchValue[1] ? expressions[matchValue[1]] : attributes[key];
363
- // Is event handler.
395
+ const value = getExpression(attributes[key], expressions);
396
+ // Is event handler. Add to events object.
364
397
  if (matchKey && matchKey[1]) {
365
398
  const eventType = matchKey[1].toLowerCase();
366
399
  Object.keys(value).forEach(
@@ -368,13 +401,12 @@ export default class Component extends View {
368
401
  );
369
402
  return out;
370
403
  }
371
- // Is function expression.
404
+ // Is attribute. Add to attributes object.
372
405
  out[key] = value;
373
406
  return out;
374
407
  }, {});
375
408
 
376
409
  const Current = this;
377
-
378
410
  // Create subclass for this component.
379
411
  return Current.extend({
380
412
  // Set events.
@@ -395,3 +427,8 @@ export default class Component extends View {
395
427
  });
396
428
  }
397
429
  }
430
+
431
+ Component.ID_TEMPLATE = (uid) => `rasti-component-${uid}`;
432
+ Component.EXPRESSION_PLACEHOLDER_TEMPLATE = (idx) => `__RASTI_EXPRESSION{${idx}}`;
433
+ Component.TRUE_PLACEHOLDER = '__RASTI_TRUE';
434
+ Component.FALSE_PLACEHOLDER = '__RASTI_FALSE';
package/src/Model.js CHANGED
@@ -1,4 +1,4 @@
1
- import Emitter from './Emitter';
1
+ import Emitter from './Emitter.js';
2
2
  /**
3
3
  * - Orchestrates data and business logic.
4
4
  * - Emits events when data changes.
@@ -12,6 +12,7 @@ import Emitter from './Emitter';
12
12
  * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
13
13
  * and `change:attribute` events.
14
14
  * @module
15
+ * @extends Rasti.Emitter
15
16
  * @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.
16
17
  * @example
17
18
  * import { Model } from 'rasti';
package/src/View.js CHANGED
@@ -1,4 +1,4 @@
1
- import Emitter from './Emitter';
1
+ import Emitter from './Emitter.js';
2
2
 
3
3
  // This options keys will be extended on view instance.
4
4
  const viewOptions = {
@@ -24,6 +24,7 @@ const viewOptions = {
24
24
  * All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it.
25
25
  * If `this.el` is not present, an element will be created using `this.tag` (defaulting to div) and `this.attributes`.
26
26
  * @module
27
+ * @extends Rasti.Emitter
27
28
  * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
28
29
  * @property {node} el Every view has a root element, `this.el`. If not present it will be created.
29
30
  * @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`.
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import Emitter from './Emitter';
2
- import Model from './Model';
3
- import View from './View';
4
- import Component from './Component';
1
+ import Emitter from './Emitter.js';
2
+ import Model from './Model.js';
3
+ import View from './View.js';
4
+ import Component from './Component.js';
5
5
 
6
6
  export { Emitter, Model, View, Component };