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/README.md +27 -0
- package/dist/rasti.js +2 -2
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +143 -79
- package/es/View.js +1 -0
- package/lib/Component.js +143 -79
- package/lib/View.js +1 -0
- package/package.json +1 -1
- package/src/Component.js +152 -76
- package/src/View.js +1 -0
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 =
|
|
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(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
270
|
-
|
|
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
|
|
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
|
-
|
|
288
|
-
if (!this.
|
|
289
|
-
this.el
|
|
290
|
-
this.
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
this.children = [];
|
|
347
|
+
// Store active element.
|
|
348
|
+
const activeElement = document.activeElement;
|
|
307
349
|
|
|
308
350
|
const nextChildren = [];
|
|
309
351
|
const recycledChildren = [];
|
|
310
|
-
|
|
311
|
-
const
|
|
352
|
+
|
|
353
|
+
const previousChildren = this.children;
|
|
354
|
+
this.children = [];
|
|
312
355
|
// Replace expressions. Set html inside of `this.el`.
|
|
313
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 =
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
|
555
|
+
inner,
|
|
480
556
|
// Template expressions.
|
|
481
557
|
expressions,
|
|
482
558
|
},
|
|
483
559
|
// Set root element tag.
|
|
484
|
-
tag
|
|
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() {
|