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/README.md +9 -6
- package/dist/rasti.js +4 -4
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +117 -70
- package/es/Model.js +2 -1
- package/es/View.js +2 -1
- package/es/index.js +4 -4
- package/lib/Component.js +117 -70
- package/lib/Model.js +2 -1
- package/lib/View.js +2 -1
- package/lib/index.js +4 -4
- package/package.json +1 -1
- package/src/Component.js +113 -76
- package/src/Model.js +2 -1
- package/src/View.js +2 -1
- package/src/index.js +4 -4
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
|
-
*
|
|
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
|
-
|
|
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,
|
|
166
|
+
replaceExpressions(string, addChild) {
|
|
152
167
|
return string
|
|
153
|
-
.replace(
|
|
154
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
164
|
-
return out;
|
|
188
|
+
return out + parsed;
|
|
165
189
|
}, '');
|
|
166
190
|
|
|
167
191
|
})
|
|
168
192
|
// Replace `attribute="true"` with `attribute`
|
|
169
|
-
.replace(
|
|
170
|
-
// Replace `attribute="false"` with
|
|
171
|
-
.replace(
|
|
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, (
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
+
// Destroy new child component. Use recycled one instead.
|
|
270
|
+
component.destroy();
|
|
251
271
|
} else {
|
|
252
|
-
|
|
272
|
+
// Not found. Add new child component.
|
|
273
|
+
nextChildren.push(component);
|
|
253
274
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
360
|
-
const
|
|
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 =
|
|
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
|
|
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 };
|