rasti 2.0.0 → 2.1.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.
package/src/Component.js CHANGED
@@ -91,6 +91,21 @@ export default class Component extends View {
91
91
  this.onCreate.apply(this, arguments);
92
92
  }
93
93
 
94
+ /*
95
+ * Tell if component is a container.
96
+ * In which case, it will not have an element by itself.
97
+ * It will render a single expression which is expected to return a single component as child.
98
+ * `this.el` will be a reference to that child component's element.
99
+ * @return {boolean}
100
+ */
101
+ isContainer() {
102
+ return !!(!this.tag &&
103
+ this.template &&
104
+ this.template.inner &&
105
+ this.template.expressions &&
106
+ this.template.expressions.length === 1);
107
+ }
108
+
94
109
  /*
95
110
  * Override. We don't want to ensure an element on instantiation.
96
111
  * We will provide it later.
@@ -102,7 +117,7 @@ export default class Component extends View {
102
117
  this.id = this.el.id;
103
118
  }
104
119
  // Ensure id.
105
- if (!this.id) {
120
+ if (!this.isContainer() && !this.id) {
106
121
  this.id = this.attributes && this.attributes.id ?
107
122
  // If id is provided, evaluate it.
108
123
  evalExpression(this.attributes.id, this, this) :
@@ -113,6 +128,8 @@ export default class Component extends View {
113
128
 
114
129
  /*
115
130
  * Find view's element on parent node, using id.
131
+ * @param parent {node} The parent node.
132
+ * @return {node} The component's element.
116
133
  */
117
134
  findElement(parent) {
118
135
  return (parent || document).querySelector(`#${this.id}`);
@@ -120,6 +137,7 @@ export default class Component extends View {
120
137
 
121
138
  /*
122
139
  * Eval attributes expressions.
140
+ * @return {object} Object containing add, remove and html properties.
123
141
  */
124
142
  getAttributes() {
125
143
  const add = { id : this.id };
@@ -153,37 +171,56 @@ export default class Component extends View {
153
171
  /*
154
172
  * Used internally on the render process.
155
173
  * Attach the view to the dom element.
174
+ * @param parent {node} The parent node.
175
+ * @return {Rasti.Component} The component instance.
156
176
  */
157
177
  hydrate(parent) {
158
- this.el = this.findElement(parent);
159
- this.delegateEvents();
160
- this.children.forEach(child => child.hydrate(this.el));
178
+ if (!this.isContainer()) {
179
+ this.el = this.findElement(parent);
180
+ this.delegateEvents();
181
+ this.children.forEach(child => child.hydrate(this.el));
182
+ } else {
183
+ this.children[0].hydrate(parent);
184
+ this.el = this.children[0].el;
185
+ }
186
+ // Call `onRender` lifecycle method.
161
187
  this.onRender.call(this, 'hydrate');
188
+ // Return `this` for chaining.
189
+ return this;
162
190
  }
163
191
 
164
192
  /*
165
193
  * Used internally on the render process.
166
194
  * Reuse a view that has `key` when its parent is rendered.
195
+ * @param parent {node} The parent node.
196
+ * @return {Rasti.Component} The component instance.
167
197
  */
168
198
  recycle(parent) {
169
199
  // Find element to be replaced. It has same id.
170
- const toBeReplaced = this.findElement(parent);
200
+ const toBeReplaced = (
201
+ this.isContainer() ? this.children[0] : this
202
+ ).findElement(parent);
171
203
  // Replace it with this.el.
172
204
  toBeReplaced.replaceWith(this.el);
173
205
  // Call `onRender` lifecycle method.
174
206
  this.onRender.call(this, 'recycle');
207
+ // Return `this` for chaining.
208
+ return this;
175
209
  }
176
210
 
177
211
  /*
178
212
  * Override. Add some custom logic to super `destroy` method.
213
+ * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
179
214
  */
180
215
  destroy() {
181
216
  super.destroy.apply(this, arguments);
182
217
  // Stop listening to `change`.
183
- // Set destroyed flag to prevent a last render after destroyed. TODO: Review
184
218
  if (this.model && this.model.off) this.model.off('change', this.onChange);
185
219
  if (this.state && this.state.off) this.state.off('change', this.onChange);
220
+ // Set destroyed flag to prevent a last render after destroyed.
186
221
  this.destroyed = true;
222
+ // Return `this` for chaining.
223
+ return this;
187
224
  }
188
225
 
189
226
  /**
@@ -247,12 +284,13 @@ export default class Component extends View {
247
284
  }, '');
248
285
 
249
286
  })
250
- // Replace `attribute="true"` with `attribute`
251
- .replace(new RegExp(`([a-z]+)=["|']${Component.TRUE_PLACEHOLDER}["|']`, 'g'), '$1')
252
- // Replace `attribute="false"` with empty string.
253
- .replace(new RegExp(`([a-z]+)=["|']${Component.FALSE_PLACEHOLDER}["|']`, 'g'), '')
254
- // Replace rest of false expressions with empty string.
255
- .replace(new RegExp(Component.FALSE_PLACEHOLDER, 'g'), '');
287
+ // Replace `attribute="true"` with `attribute` and `attribute="false"` with empty string.
288
+ .replace(
289
+ new RegExp(`([\\w|data-]+)=(["'])?(${Component.TRUE_PLACEHOLDER}|${Component.FALSE_PLACEHOLDER})\\2`, 'g'),
290
+ (match, attribute, quote, placeholder) => placeholder === Component.TRUE_PLACEHOLDER ? attribute : ''
291
+ )
292
+ // Replace rest of true or false expressions with empty string.
293
+ .replace(new RegExp(`${Component.TRUE_PLACEHOLDER}|${Component.FALSE_PLACEHOLDER}`, 'g'), '');
256
294
  }
257
295
 
258
296
  /*
@@ -261,19 +299,21 @@ export default class Component extends View {
261
299
  toString() {
262
300
  // Normally there won't be any children, but if there are, destroy them.
263
301
  this.destroyChildren();
264
- // Get tag name.
265
- const tag = this.tag || 'div';
266
- // Get attributes.
267
- const attributes = this.getAttributes().html;
268
302
  // Replace expressions of inner template.
269
- const inner = this.template &&
270
- this.template.inner &&
303
+ const close = this.template && this.template.inner;
304
+ const inner = close &&
271
305
  this.replaceExpressions(this.template.inner, (component) => {
272
306
  // Add child component.
273
307
  return this.addChild(component);
274
308
  });
309
+ // If component is a container, return inner which will be an expression thats return a single component.
310
+ if (this.isContainer()) return inner;
311
+ // Get tag name.
312
+ const tag = this.tag || 'div';
313
+ // Get attributes.
314
+ const attributes = this.getAttributes().html;
275
315
  // Generate outer template.
276
- return inner ?
316
+ return close ?
277
317
  `<${tag} ${attributes}>${inner}</${tag}>` :
278
318
  `<${tag} ${attributes} />`;
279
319
  }
@@ -284,33 +324,36 @@ export default class Component extends View {
284
324
  render() {
285
325
  // Prevent a last re render if view is already destroyed.
286
326
  if (this.destroyed) return this;
287
- // If `this.el` is not present, create a new `this.tag` element.
288
- if (!this.el) {
289
- this.el = this.createElement(this.tag);
290
- this.delegateEvents();
327
+
328
+ if (!this.isContainer()) {
329
+ // If `this.el` is not present, create a new `this.tag` element.
330
+ if (!this.el) {
331
+ this.el = this.createElement(this.tag);
332
+ this.delegateEvents();
333
+ }
334
+ // Set `this.el` attributes.
335
+ const attributes = this.getAttributes();
336
+ // Remove attributes.
337
+ Object.keys(attributes.remove).forEach(key => {
338
+ this.el.removeAttribute(key);
339
+ });
340
+ // Add attributes.
341
+ Object.keys(attributes.add).forEach(key => {
342
+ this.el.setAttribute(key, attributes.add[key]);
343
+ });
291
344
  }
292
- // Set `this.el` attributes.
293
- const attributes = this.getAttributes();
294
- // Remove attributes.
295
- Object.keys(attributes.remove).forEach(key => {
296
- this.el.removeAttribute(key);
297
- });
298
- // Add attributes.
299
- Object.keys(attributes.add).forEach(key => {
300
- this.el.setAttribute(key, attributes.add[key]);
301
- });
302
345
  // Check for `template.inner` to see if view has innerHTML.
303
346
  if (this.template && this.template.inner) {
304
- const previousChildren = this.children;
305
-
306
- this.children = [];
347
+ // Store active element.
348
+ const activeElement = document.activeElement;
307
349
 
308
350
  const nextChildren = [];
309
351
  const recycledChildren = [];
310
- // Store active element.
311
- const activeElement = document.activeElement;
352
+
353
+ const previousChildren = this.children;
354
+ this.children = [];
312
355
  // Replace expressions. Set html inside of `this.el`.
313
- this.el.innerHTML = this.replaceExpressions(this.template.inner, (component) => {
356
+ const inner = this.replaceExpressions(this.template.inner, (component) => {
314
357
  let out = component;
315
358
  // Check if child already exists.
316
359
  const found = component.key && previousChildren.find(
@@ -333,14 +376,33 @@ export default class Component extends View {
333
376
  // Component html.
334
377
  return out;
335
378
  });
336
- // Add new children. Hydrate them.
337
- nextChildren.forEach(nextChild => {
338
- this.addChild(nextChild).hydrate(this.el);
339
- });
340
- // Replace children root elements with recycled components.
341
- recycledChildren.forEach(recycledChild => {
342
- this.addChild(recycledChild).recycle(this.el);
343
- });
379
+
380
+ if (this.isContainer()) {
381
+ if (nextChildren[0]) {
382
+ const fragment = this.createElement('template');
383
+ fragment.innerHTML = inner;
384
+ // Add new child to dom fragment and hydrate it.
385
+ this.addChild(nextChildren[0]).hydrate(fragment.content);
386
+ // Get next child element.
387
+ const nextEl = fragment.content.children[0];
388
+ // If `this.el` is present, replace it with nextEl.
389
+ if (this.el) this.el.replaceWith(nextEl);
390
+ // Set `this.el` to nextEl.
391
+ this.el = nextEl;
392
+ } else if (recycledChildren[0]) {
393
+ this.addChild(recycledChildren[0]);
394
+ }
395
+ } else {
396
+ this.el.innerHTML = inner;
397
+ // Add new children. Hydrate them.
398
+ nextChildren.forEach(nextChild => {
399
+ this.addChild(nextChild).hydrate(this.el);
400
+ });
401
+ // Replace children root elements with recycled components.
402
+ recycledChildren.forEach(recycledChild => {
403
+ this.addChild(recycledChild).recycle(this.el);
404
+ });
405
+ }
344
406
  // Destroy unused children.
345
407
  previousChildren.forEach(previousChild => {
346
408
  const found = recycledChildren.indexOf(previousChild) > -1;
@@ -394,11 +456,11 @@ export default class Component extends View {
394
456
  if (hydrate) {
395
457
  view.toString();
396
458
  } else {
397
- const fragment = document.createElement('template');
459
+ const fragment = this.prototype.createElement('template');
398
460
  // Add html text into element inner html.
399
461
  fragment.innerHTML = view;
400
462
  // Add to dom.
401
- el.appendChild(fragment.content);
463
+ el.appendChild(fragment.content.children[0]);
402
464
  }
403
465
  view.hydrate(el);
404
466
  }
@@ -418,10 +480,18 @@ export default class Component extends View {
418
480
  * - If the function returns a component instance, it will be added as a child component.
419
481
  * - If the function returns an array, each item will be evaluated as above.
420
482
  * @static
421
- * @param {string} HTML template for the component.
483
+ * @param {string} HTML template for the component. Or a function that mounts a sub component.
422
484
  * @return {Rasti.Component}
423
485
  */
424
486
  static create(strings, ...expressions) {
487
+ // Containers can be created using create as a functions instead of a tagged template.
488
+ if (typeof strings === 'function') {
489
+ expressions = [strings];
490
+ strings = ['', ''];
491
+ }
492
+
493
+ let tag, attributes, inner, events;
494
+
425
495
  const parts = [];
426
496
  // Replace functions and objects interpolations with `{number}`.
427
497
  // Where `number` is the index on expressions array.
@@ -440,31 +510,37 @@ export default class Component extends View {
440
510
  // Create output text for main template.
441
511
  const main = parts.join('').trim().replace(/\n/g, '');
442
512
  // Extract outer tag, attributes and inner html.
443
- const result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/) || main.match(/^<([a-z]+)(.*?)\/>$/);
444
- // Parse attributes from html text into an object.
445
- let attributes = extractAttributes(result[2]);
446
- // Events to be delegated.
447
- let events = {};
448
- // Filter events. To generate events object.
449
- // Generate attributes object, replace placeholders with expressions.
450
- attributes = Object.keys(attributes).reduce((out, key) => {
451
- // Is Event?
452
- const matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/);
453
- // Is placeholder for function or object?
454
- // Get expression or value.
455
- const value = getExpression(attributes[key], expressions);
456
- // Is event handler. Add to events object.
457
- if (matchKey && matchKey[1]) {
458
- const eventType = matchKey[1].toLowerCase();
459
- Object.keys(value).forEach(
460
- selector => events[`${eventType}${selector === '&' ? '' : ` ${selector}`}`] = value[selector]
461
- );
513
+ const result = main.match(/^<([a-z]+[1-6]?)(.*?)>(.*)<\/\1>$/) || main.match(/^<([a-z]+)(.*?)\/>$/);
514
+
515
+ if (result) {
516
+ tag = result[1];
517
+ inner = result[3];
518
+ // Parse attributes from html text into an object.
519
+ attributes = extractAttributes(result[2]);
520
+ // Events to be delegated.
521
+ events = {};
522
+ // Filter events. To generate events object.
523
+ // Generate attributes object, replace placeholders with expressions.
524
+ attributes = Object.keys(attributes).reduce((out, key) => {
525
+ // Is Event?
526
+ const matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/);
527
+ // Is placeholder for function or object?
528
+ // Get expression or value.
529
+ const value = getExpression(attributes[key], expressions);
530
+ // Is event handler. Add to events object.
531
+ if (matchKey && matchKey[1]) {
532
+ const eventType = matchKey[1].toLowerCase();
533
+ Object.keys(value).forEach(
534
+ selector => events[`${eventType}${selector === '&' ? '' : ` ${selector}`}`] = value[selector]
535
+ );
536
+ return out;
537
+ }
538
+ // Is attribute. Add to attributes object.
539
+ out[key] = value;
540
+
462
541
  return out;
463
- }
464
- // Is attribute. Add to attributes object.
465
- out[key] = value;
466
- return out;
467
- }, {});
542
+ }, {});
543
+ } else inner = main;
468
544
 
469
545
  const Current = this;
470
546
  // Create subclass for this component.
@@ -476,12 +552,12 @@ export default class Component extends View {
476
552
  // Set template.
477
553
  template : {
478
554
  // Template for innerHTML of root element.
479
- inner : result[3],
555
+ inner,
480
556
  // Template expressions.
481
557
  expressions,
482
558
  },
483
559
  // Set root element tag.
484
- tag : result[1]
560
+ tag
485
561
  });
486
562
  }
487
563
  }
package/src/View.js CHANGED
@@ -104,6 +104,7 @@ export default class View extends Emitter {
104
104
  /**
105
105
  * Destroy the view.
106
106
  * Destroy children views if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.
107
+ * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
107
108
  * @return {Rasti.View} Return `this` for chaining.
108
109
  */
109
110
  destroy() {