rasti 3.0.0-alpha.0 → 3.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 +40 -65
- package/dist/rasti.js +237 -107
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +140 -60
- package/es/Emitter.js +7 -3
- package/es/Model.js +6 -6
- package/es/View.js +74 -38
- package/es/index.js +1 -0
- package/es/utils/deepFlat.js +12 -0
- package/lib/Component.cjs +140 -60
- package/lib/Emitter.cjs +7 -3
- package/lib/Model.cjs +6 -6
- package/lib/View.cjs +74 -38
- package/lib/index.cjs +1 -0
- package/lib/utils/deepFlat.cjs +14 -0
- package/package.json +2 -2
- package/src/Component.js +141 -61
- package/src/Emitter.js +7 -3
- package/src/Model.js +6 -6
- package/src/View.js +74 -38
- package/src/utils/deepFlat.js +12 -0
package/es/View.js
CHANGED
|
@@ -15,7 +15,7 @@ const viewOptions = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* - Listens for changes and renders UI.
|
|
18
|
+
* - Listens for changes and renders the UI.
|
|
19
19
|
* - Handles user input and interactivity.
|
|
20
20
|
* - Sends captured input to the model.
|
|
21
21
|
*
|
|
@@ -25,16 +25,16 @@ const viewOptions = {
|
|
|
25
25
|
* emitted by the models to re-render themselves based on changes.
|
|
26
26
|
* Each `View` has a root element, `this.el`, which is used for event delegation.
|
|
27
27
|
* All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it.
|
|
28
|
-
* If `this.el` is not present, an element will be created using `this.tag` (defaulting to div) and `this.attributes`.
|
|
28
|
+
* If `this.el` is not present, an element will be created using `this.tag` (defaulting to `div`) and `this.attributes`.
|
|
29
29
|
* @module
|
|
30
|
-
* @extends
|
|
31
|
-
* @param {object} options Object containing options. The following keys will be merged
|
|
32
|
-
* @property {node} el Every view has a root element
|
|
33
|
-
* @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`. If a function
|
|
34
|
-
* @property {object} attributes If `this.el` is not present, an element will be created using `this.attributes`. If a function
|
|
35
|
-
* @property {object} events Object in the format `{'event selector' : 'listener'}`.
|
|
36
|
-
* @property {object} model A
|
|
37
|
-
* @property {function} template A function that
|
|
30
|
+
* @extends Emitter
|
|
31
|
+
* @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.
|
|
32
|
+
* @property {node|function} el Every view has a root DOM element stored at `this.el`. If not present, it will be created. If `this.el` is a function, it will be called to get the element at `this.ensureElement`, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
33
|
+
* @property {string|function} tag If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. Default is `div`. If it is a function, it will be called to get the tag, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
34
|
+
* @property {object|function} attributes If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. If it is a function, it will be called to get the attributes object, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
35
|
+
* @property {object|function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.
|
|
36
|
+
* @property {object} model A model or any object containing data and business logic.
|
|
37
|
+
* @property {function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
|
|
38
38
|
* @example
|
|
39
39
|
* import { View } from 'rasti';
|
|
40
40
|
*
|
|
@@ -183,15 +183,15 @@ class View extends Emitter {
|
|
|
183
183
|
* Called from the constructor if `this.el` is undefined, to ensure
|
|
184
184
|
* the view has a root element.
|
|
185
185
|
* @param {string} tag Tag for the element. Default to `div`
|
|
186
|
-
* @param {object}
|
|
186
|
+
* @param {object} attributes Attributes for the element.
|
|
187
187
|
* @return {node} The created element.
|
|
188
188
|
*/
|
|
189
|
-
createElement(tag = 'div',
|
|
189
|
+
createElement(tag = 'div', attributes = {}) {
|
|
190
190
|
// Create DOM element.
|
|
191
191
|
let el = document.createElement(tag);
|
|
192
192
|
// Add element attributes.
|
|
193
|
-
Object.keys(
|
|
194
|
-
.forEach(key => el.setAttribute(key,
|
|
193
|
+
Object.keys(attributes)
|
|
194
|
+
.forEach(key => el.setAttribute(key, attributes[key]));
|
|
195
195
|
|
|
196
196
|
return el;
|
|
197
197
|
}
|
|
@@ -207,24 +207,39 @@ class View extends Emitter {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
* Provide declarative listeners for DOM events within a view. If an events
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* The
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* are
|
|
222
|
-
*
|
|
223
|
-
*
|
|
210
|
+
* Provide declarative listeners for DOM events within a view. If an events object is not provided,
|
|
211
|
+
* it defaults to using `this.events`. If `this.events` is a function, it will be called to get the events object.
|
|
212
|
+
*
|
|
213
|
+
* The events object should follow the format `{'event selector': 'listener'}`:
|
|
214
|
+
* - `event`: The type of event (e.g., 'click').
|
|
215
|
+
* - `selector`: A CSS selector to match the event target. If omitted, the event is bound to the root element.
|
|
216
|
+
* - `listener`: A function or a string representing a method name on the view. The method will be called with `this` bound to the view instance.
|
|
217
|
+
*
|
|
218
|
+
* By default, `delegateEvents` is called within the View's constructor. If you have a simple events object,
|
|
219
|
+
* all of your DOM events will be connected automatically, and you will not need to call this function manually.
|
|
220
|
+
*
|
|
221
|
+
* All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.
|
|
222
|
+
* When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.
|
|
223
|
+
*
|
|
224
|
+
* The listeners will be invoked with the event and the view as arguments.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
|
|
227
|
+
* @return {Rasti.View} Returns `this` for chaining.
|
|
224
228
|
* @example
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
229
|
+
* // Using a function.
|
|
230
|
+
* class Modal extends View {
|
|
231
|
+
* events() {
|
|
232
|
+
* return {
|
|
233
|
+
* 'click button.ok': 'onClickOkButton',
|
|
234
|
+
* 'click button.cancel': function() {}
|
|
235
|
+
* };
|
|
236
|
+
* }
|
|
237
|
+
* }
|
|
238
|
+
*
|
|
239
|
+
* // Using an object.
|
|
240
|
+
* Modal.prototype.events = {
|
|
241
|
+
* 'click button.ok' : 'onClickOkButton',
|
|
242
|
+
* 'click button.cancel' : function() {}
|
|
228
243
|
* };
|
|
229
244
|
*/
|
|
230
245
|
delegateEvents(events) {
|
|
@@ -274,7 +289,7 @@ class View extends Emitter {
|
|
|
274
289
|
/**
|
|
275
290
|
* Removes all of the view's delegated events.
|
|
276
291
|
* Useful if you want to disable or remove a view from the DOM temporarily.
|
|
277
|
-
* Called automatically when the view is destroyed.
|
|
292
|
+
* Called automatically when the view is destroyed and when `delegateEvents` is called again.
|
|
278
293
|
* @return {Rasti.View} Return `this` for chaining.
|
|
279
294
|
*/
|
|
280
295
|
undelegateEvents() {
|
|
@@ -290,22 +305,43 @@ class View extends Emitter {
|
|
|
290
305
|
/**
|
|
291
306
|
* Renders the view.
|
|
292
307
|
* This method should be overridden with custom logic.
|
|
293
|
-
* The convention is to
|
|
308
|
+
* The only convention is to manipulate the DOM within the scope of `this.el`,
|
|
294
309
|
* and to return `this` for chaining.
|
|
295
|
-
* If you add any child views, you
|
|
310
|
+
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
296
311
|
* The default implementation sets the innerHTML of `this.el` with the result
|
|
297
312
|
* of calling `this.template`, passing `this.model` as an argument.
|
|
298
313
|
* <br><br> ⚠ **Security Notice:** The default implementation utilizes `innerHTML` on the root element
|
|
299
|
-
* for rendering, which may introduce Cross
|
|
300
|
-
* content is properly sanitized before inserting it into the DOM.
|
|
301
|
-
*
|
|
302
|
-
*
|
|
314
|
+
* for rendering, which may introduce Cross-Site Scripting (XSS) risks. Ensure that any user-generated
|
|
315
|
+
* content is properly sanitized before inserting it into the DOM. You can use the @link{#module_view_sanitize View.sanitize}
|
|
316
|
+
* static method to escape HTML entities in a string.
|
|
317
|
+
* For best practices on secure data handling, refer to the
|
|
318
|
+
* [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
|
|
319
|
+
* @return {Rasti.View} Returns `this` for chaining.
|
|
303
320
|
*/
|
|
304
321
|
render() {
|
|
305
322
|
if (this.template) this.el.innerHTML = this.template(this.model);
|
|
306
323
|
// Return `this` for chaining.
|
|
307
324
|
return this;
|
|
308
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Escape HTML entities in a string.
|
|
329
|
+
* Use method to sanitize user-generated content before inserting it into the DOM.
|
|
330
|
+
* Override this method to provide a custom escape function.
|
|
331
|
+
* This method is used by `Component` to escape template interpolations.
|
|
332
|
+
* @static
|
|
333
|
+
* @param {string} str String to escape.
|
|
334
|
+
* @return {string} Escaped string.
|
|
335
|
+
*/
|
|
336
|
+
static sanitize(value) {
|
|
337
|
+
return `${value}`.replace(/[&<>"']/g, match => ({
|
|
338
|
+
'&' : '&',
|
|
339
|
+
'<' : '<',
|
|
340
|
+
'>' : '>',
|
|
341
|
+
'"' : '"',
|
|
342
|
+
'\'' : '''
|
|
343
|
+
}[match]));
|
|
344
|
+
}
|
|
309
345
|
}
|
|
310
346
|
|
|
311
347
|
/*
|
package/es/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Flatten an array recursively
|
|
3
|
+
* @param {Array} arr Array to flat recursively
|
|
4
|
+
* @return {Array} Flat array
|
|
5
|
+
*/
|
|
6
|
+
const deepFlat = (arr) => arr.reduce((acc, val) => {
|
|
7
|
+
if (Array.isArray(val)) acc.push(...deepFlat(val));
|
|
8
|
+
else acc.push(val);
|
|
9
|
+
return acc;
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
export { deepFlat as default };
|
package/lib/Component.cjs
CHANGED
|
@@ -2,8 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
var View = require('./View.cjs');
|
|
4
4
|
var utils_getResult = require('./utils/getResult.cjs');
|
|
5
|
+
var utils_deepFlat = require('./utils/deepFlat.cjs');
|
|
5
6
|
require('./Emitter.cjs');
|
|
6
7
|
|
|
8
|
+
/*
|
|
9
|
+
* Wrapper class for HTML strings marked as safe.
|
|
10
|
+
*/
|
|
11
|
+
class SafeHTML {
|
|
12
|
+
constructor(value) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toString() {
|
|
17
|
+
return this.value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/*
|
|
8
22
|
* Same as getResult, but pass context as argument to the expression.
|
|
9
23
|
* Used to evaluate expressions in the context of a component.
|
|
@@ -11,7 +25,7 @@ require('./Emitter.cjs');
|
|
|
11
25
|
* @param {any} context The context to call the expression with.
|
|
12
26
|
* @return {any} The result of the evaluated expression.
|
|
13
27
|
*/
|
|
14
|
-
const
|
|
28
|
+
const getExpressionResult = (expression, context) => utils_getResult(expression, context, context);
|
|
15
29
|
|
|
16
30
|
/*
|
|
17
31
|
* Generate string with placeholders for interpolated expressions.
|
|
@@ -46,10 +60,10 @@ const splitPlaceholders = (main, expressions) => {
|
|
|
46
60
|
// so all the components are added as children by the parent component.
|
|
47
61
|
while ((match = regExp.exec(main)) !== null) {
|
|
48
62
|
const before = main.slice(lastIndex, match.index);
|
|
49
|
-
out.push(before, expressions[match[1]]);
|
|
63
|
+
out.push(new SafeHTML(before), expressions[match[1]]);
|
|
50
64
|
lastIndex = match.index + match[0].length;
|
|
51
65
|
}
|
|
52
|
-
out.push(main.slice(lastIndex));
|
|
66
|
+
out.push(new SafeHTML(main.slice(lastIndex)));
|
|
53
67
|
|
|
54
68
|
return out;
|
|
55
69
|
};
|
|
@@ -57,15 +71,15 @@ const splitPlaceholders = (main, expressions) => {
|
|
|
57
71
|
/*
|
|
58
72
|
* Expand attributes.
|
|
59
73
|
* @param attributes {array} Array of attributes as key, value pairs.
|
|
60
|
-
* @param
|
|
74
|
+
* @param getExpressionResult {function} Function to render expressions.
|
|
61
75
|
* @return {object}
|
|
62
76
|
* @property {object} all All attributes.
|
|
63
77
|
* @property {object} events Event listeners.
|
|
64
78
|
* @property {object} attributes Attributes.
|
|
65
79
|
*/
|
|
66
|
-
const expandAttributes = (attributes,
|
|
80
|
+
const expandAttributes = (attributes, getExpressionResult) => {
|
|
67
81
|
const out = attributes.reduce((out, pair) => {
|
|
68
|
-
const attribute =
|
|
82
|
+
const attribute = getExpressionResult(pair[0]);
|
|
69
83
|
// Attribute without value.
|
|
70
84
|
if (pair.length === 1) {
|
|
71
85
|
if (typeof attribute === 'object') {
|
|
@@ -77,7 +91,7 @@ const expandAttributes = (attributes, renderExpression) => {
|
|
|
77
91
|
}
|
|
78
92
|
} else {
|
|
79
93
|
// Attribute with value.
|
|
80
|
-
const value =
|
|
94
|
+
const value = getExpressionResult(pair[1]);
|
|
81
95
|
out.all[attribute] = value;
|
|
82
96
|
}
|
|
83
97
|
|
|
@@ -128,12 +142,12 @@ const expandComponents = (main, expressions) => {
|
|
|
128
142
|
const list = splitPlaceholders(expandComponents(inner, expressions), expressions);
|
|
129
143
|
// Create renderChildren function.
|
|
130
144
|
renderChildren = function() {
|
|
131
|
-
return list.map(item =>
|
|
145
|
+
return utils_deepFlat(list.map(item => getExpressionResult(item, this)));
|
|
132
146
|
};
|
|
133
147
|
}
|
|
134
148
|
// Create mount function.
|
|
135
149
|
const mount = function() {
|
|
136
|
-
const options = expandAttributes(attributes, value =>
|
|
150
|
+
const options = expandAttributes(attributes, value => getExpressionResult(value, this)).all;
|
|
137
151
|
// Add renderChildren function to options.
|
|
138
152
|
if (renderChildren) options.renderChildren = renderChildren.bind(this);
|
|
139
153
|
// Mount component.
|
|
@@ -197,6 +211,14 @@ const parseMatch = (match, expressions) => {
|
|
|
197
211
|
return data;
|
|
198
212
|
};
|
|
199
213
|
|
|
214
|
+
/*
|
|
215
|
+
* HTML tags that are self closing.
|
|
216
|
+
*/
|
|
217
|
+
const selfClosingTags = {
|
|
218
|
+
area : true, base : true, br : true, col : true, embed : true, hr : true,
|
|
219
|
+
img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
|
|
220
|
+
};
|
|
221
|
+
|
|
200
222
|
/*
|
|
201
223
|
* These option keys will be extended on the component instance.
|
|
202
224
|
*/
|
|
@@ -208,26 +230,19 @@ const componentOptions = {
|
|
|
208
230
|
onRender : true
|
|
209
231
|
};
|
|
210
232
|
|
|
211
|
-
/*
|
|
212
|
-
* HTML tags that are self closing.
|
|
213
|
-
*/
|
|
214
|
-
const selfClosingTags = {
|
|
215
|
-
area : true, base : true, br : true, col : true, embed : true, hr : true,
|
|
216
|
-
img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
|
|
217
|
-
};
|
|
218
|
-
|
|
219
233
|
/**
|
|
220
234
|
* Components are a special kind of `View` that is designed to be easily composable,
|
|
221
235
|
* making it simple to add child views and build complex user interfaces.
|
|
222
236
|
* Unlike views, which are render-agnostic, components have a specific set of rendering
|
|
223
237
|
* guidelines that allow for a more declarative development style.
|
|
224
|
-
* Components are defined with the
|
|
238
|
+
* Components are defined with the {@link #module_component_create Component.create} static method, which takes a tagged template string or a function that returns another component.
|
|
225
239
|
* @module
|
|
226
240
|
* @extends Rasti.View
|
|
227
241
|
* @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
|
|
228
242
|
* @property {string} key A unique key to identify the component. Used to recycle child components.
|
|
229
243
|
* @property {object} model A `Rasti.Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method.
|
|
230
244
|
* @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state. The component will listen to `change` events and call `onChange` lifecycle method.
|
|
245
|
+
* @see {@link #module_component_create Component.create}
|
|
231
246
|
* @example
|
|
232
247
|
* import { Component, Model } from 'rasti';
|
|
233
248
|
* // Create Timer component.
|
|
@@ -326,9 +341,11 @@ class Component extends View {
|
|
|
326
341
|
const attributes = { [Component.DATA_ATTRIBUTE_UID] : this.uid };
|
|
327
342
|
|
|
328
343
|
if (this.attributes) Object.assign(attributes, utils_getResult(this.attributes, this));
|
|
344
|
+
// Store previous attributes.
|
|
345
|
+
const previousAttributes = this.previousAttributes || {};
|
|
346
|
+
this.previousAttributes = attributes;
|
|
329
347
|
|
|
330
348
|
Object.keys(attributes).forEach(key => {
|
|
331
|
-
// Evaluate attribute value.
|
|
332
349
|
let value = attributes[key];
|
|
333
350
|
// Transform bool attribute values
|
|
334
351
|
if (value === false) {
|
|
@@ -340,7 +357,13 @@ class Component extends View {
|
|
|
340
357
|
if (value === null || typeof value === 'undefined') value = '';
|
|
341
358
|
|
|
342
359
|
add[key] = value;
|
|
343
|
-
html.push(`${key}="${value}"`);
|
|
360
|
+
html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// Remove attributes that were in previousAttributes but not in current attributes.
|
|
364
|
+
Object.keys(previousAttributes).forEach(key => {
|
|
365
|
+
if (!(key in attributes)) {
|
|
366
|
+
remove[key] = true;
|
|
344
367
|
}
|
|
345
368
|
});
|
|
346
369
|
|
|
@@ -467,13 +490,11 @@ class Component extends View {
|
|
|
467
490
|
* });
|
|
468
491
|
*/
|
|
469
492
|
partial(strings, ...expressions) {
|
|
470
|
-
return
|
|
471
|
-
|
|
472
|
-
addPlaceholders(strings, expressions), expressions
|
|
473
|
-
),
|
|
474
|
-
)
|
|
475
|
-
return renderExpression(item, this);
|
|
476
|
-
}).flat();
|
|
493
|
+
return utils_deepFlat(
|
|
494
|
+
splitPlaceholders(
|
|
495
|
+
expandComponents(addPlaceholders(strings, expressions), expressions), expressions
|
|
496
|
+
).map(item => getExpressionResult(item, this))
|
|
497
|
+
);
|
|
477
498
|
}
|
|
478
499
|
|
|
479
500
|
getRecyclePlaceholder() {
|
|
@@ -633,7 +654,6 @@ class Component extends View {
|
|
|
633
654
|
* It instantiate the Component view using options,
|
|
634
655
|
* appends its element into the DOM (if `el` is provided).
|
|
635
656
|
* And returns the view instance.
|
|
636
|
-
* <br><br> ⚠ **Security Notice:** `Component` utilizes `innerHTML` on a document fragment for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated content is properly sanitized before inserting it into the DOM. For best practices on secure data handling, refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
|
|
637
657
|
* @static
|
|
638
658
|
* @param {object} options The view options.
|
|
639
659
|
* @param {node} el Dom element to append the view element.
|
|
@@ -659,38 +679,96 @@ class Component extends View {
|
|
|
659
679
|
}
|
|
660
680
|
|
|
661
681
|
/**
|
|
662
|
-
* Takes a tagged template
|
|
663
|
-
* and returns a new `Component` class.
|
|
682
|
+
* Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
|
|
664
683
|
* - The template outer tag and attributes will be used to create the view's root element.
|
|
665
|
-
* - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
|
|
666
|
-
* - 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.
|
|
667
684
|
* - The template inner HTML will be used as the view's template.
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* - If the function returns an
|
|
685
|
+
* ```javascript
|
|
686
|
+
* const Button = Component.create`<button class="button">Click me</button>`;
|
|
687
|
+
* ```
|
|
688
|
+
* - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it. If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
|
|
689
|
+
* ```javascript
|
|
690
|
+
* const Button = Component.create`
|
|
691
|
+
* <button class="${({ options }) => options.className}">
|
|
692
|
+
* ${({ options }) => options.renderChildren()}
|
|
693
|
+
* </button>
|
|
694
|
+
* `;
|
|
695
|
+
* ```
|
|
696
|
+
* - Event handlers should be passed, at the root element as camelized attributes, in the format `onEventName=${{'selector' : listener }}`. They will be transformed to an event object and delegated to the root element. See {@link #module_view__delegateevents View.delegateEvents}.
|
|
697
|
+
* - Boolean attributes should be passed in the form of `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
|
|
698
|
+
* ```javascript
|
|
699
|
+
* const Input = Component.create`
|
|
700
|
+
* <input type="text" disabled=${({ options }) => options.disabled} />
|
|
701
|
+
* `;
|
|
702
|
+
* ```
|
|
703
|
+
* - If the interpolated function returns a component instance, it will be added as a child component.
|
|
704
|
+
* - If the interpolated function returns an array, each item will be evaluated as above.
|
|
705
|
+
* ```javascript
|
|
706
|
+
* // Create a button component.
|
|
707
|
+
* const Button = Component.create`
|
|
708
|
+
* <button class="button">
|
|
709
|
+
* ${({ options }) => options.renderChildren()}
|
|
710
|
+
* </button>
|
|
711
|
+
* `;
|
|
712
|
+
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
713
|
+
* const Navigation = Component.create`
|
|
714
|
+
* <nav>
|
|
715
|
+
* ${({ options }) => options.items.map(
|
|
716
|
+
* item => Button.mount({ renderChildren: () => item.label })
|
|
717
|
+
* )}
|
|
718
|
+
* </nav>
|
|
719
|
+
* `;
|
|
720
|
+
* // Create a header component. Add navigation as a child.
|
|
721
|
+
* const Header = Component.create`
|
|
722
|
+
* <header>
|
|
723
|
+
* ${({ options }) => Navigation.mount({ items : options.items})}
|
|
724
|
+
* </header>
|
|
725
|
+
* `;
|
|
726
|
+
* ```
|
|
727
|
+
* - Child components can be added using a component tag.
|
|
728
|
+
* ```javascript
|
|
729
|
+
* // Create a button component.
|
|
730
|
+
* const Button = Component.create`
|
|
731
|
+
* <button class="button">
|
|
732
|
+
* ${({ options }) => options.renderChildren()}
|
|
733
|
+
* </button>
|
|
734
|
+
* `;
|
|
735
|
+
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
736
|
+
* const Navigation = Component.create`
|
|
737
|
+
* <nav>
|
|
738
|
+
* ${self => self.options.items.map(
|
|
739
|
+
* item => self.partial`<${Button}>${item.label}</${Button}>`
|
|
740
|
+
* )}
|
|
741
|
+
* </nav>
|
|
742
|
+
* `;
|
|
743
|
+
* // Create a header component. Add navigation as a child.
|
|
744
|
+
* const Header = Component.create`
|
|
745
|
+
* <header>
|
|
746
|
+
* <${Navigation} items="${({ options }) => options.items}" />
|
|
747
|
+
* </header>
|
|
748
|
+
* `;
|
|
749
|
+
* ```
|
|
672
750
|
* - If the tagged template contains only one expression that mounts a component, or the tags are references to a component, the component will be considered a <b>container</b>. It will render a single component as a child. `this.el` will be a reference to that child component's element.
|
|
751
|
+
* ```javascript
|
|
752
|
+
* // Create a button component.
|
|
753
|
+
* const Button = Component.create`
|
|
754
|
+
* <button class="${({ options }) => options.className}">
|
|
755
|
+
* ${self => self.renderChildren()}
|
|
756
|
+
* </button>
|
|
757
|
+
* `;
|
|
758
|
+
* // Create a container using the button component
|
|
759
|
+
* const ButtonOk = Component.create`
|
|
760
|
+
* <${Button} className="ok">Ok</${Button}>
|
|
761
|
+
* `;
|
|
762
|
+
* // Create a button component using a function
|
|
763
|
+
* const ButtonCancel = Component.create(() => Button.mount({
|
|
764
|
+
* className: 'cancel',
|
|
765
|
+
* renderChildren: () => 'Cancel'
|
|
766
|
+
* }));
|
|
767
|
+
* ```
|
|
673
768
|
* @static
|
|
674
769
|
* @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
|
|
675
770
|
* @param {...*} expressions - The expressions to be interpolated within the template.
|
|
676
771
|
* @return {Rasti.Component} The newly created component class.
|
|
677
|
-
* @example
|
|
678
|
-
* import { Component } from 'rasti';
|
|
679
|
-
* // Create a button component.
|
|
680
|
-
* const Button = Component.create`
|
|
681
|
-
* <button class="${({ options }) => options.className}">
|
|
682
|
-
* ${self => self.renderChildren()}
|
|
683
|
-
* </button>
|
|
684
|
-
* `;
|
|
685
|
-
* // Create a container using the button component
|
|
686
|
-
* const ButtonOk = Component.create`
|
|
687
|
-
* <${Button} className="ok">Ok</${Button}>
|
|
688
|
-
* `;
|
|
689
|
-
* // Create a button component using a function
|
|
690
|
-
* const ButtonCancel = Component.create(() => Button.mount({
|
|
691
|
-
* className: 'cancel',
|
|
692
|
-
* renderChildren: () => 'Cancel'
|
|
693
|
-
* }));
|
|
694
772
|
*/
|
|
695
773
|
static create(strings, ...expressions) {
|
|
696
774
|
const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
|
|
@@ -713,18 +791,18 @@ class Component extends View {
|
|
|
713
791
|
const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
|
|
714
792
|
// Get tag, attributes.
|
|
715
793
|
tag = function() {
|
|
716
|
-
return
|
|
794
|
+
return View.sanitize(getExpressionResult(tagExpression, this));
|
|
717
795
|
};
|
|
718
796
|
// Get attributes.
|
|
719
797
|
attributes = function() {
|
|
720
|
-
return expandAttributes(attributesAndEvents, value =>
|
|
798
|
+
return expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).attributes;
|
|
721
799
|
};
|
|
722
800
|
// Get events.
|
|
723
801
|
events = function() {
|
|
724
|
-
const onlyEvents = expandAttributes(attributesAndEvents, value =>
|
|
802
|
+
const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
|
|
725
803
|
|
|
726
804
|
return Object.keys(onlyEvents).reduce((out, key) => {
|
|
727
|
-
const typeListeners =
|
|
805
|
+
const typeListeners = getExpressionResult(onlyEvents[key], this);
|
|
728
806
|
|
|
729
807
|
Object.keys(typeListeners).forEach(selector => {
|
|
730
808
|
out[`${key}${selector === '&' ? '' : ` ${selector}`}`] = typeListeners[selector];
|
|
@@ -737,9 +815,11 @@ class Component extends View {
|
|
|
737
815
|
if (close) {
|
|
738
816
|
const list = inner ? splitPlaceholders(inner, expressions) : [];
|
|
739
817
|
template = function(addChild) {
|
|
740
|
-
return list.map(item =>
|
|
818
|
+
return utils_deepFlat(list.map(item => getExpressionResult(item, this))).map(item => {
|
|
741
819
|
if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
|
|
742
|
-
|
|
820
|
+
if (item instanceof SafeHTML) return item;
|
|
821
|
+
if (item instanceof Component) return addChild(item);
|
|
822
|
+
return View.sanitize(`${item}`);
|
|
743
823
|
}
|
|
744
824
|
return '';
|
|
745
825
|
}).join('');
|
|
@@ -753,7 +833,7 @@ class Component extends View {
|
|
|
753
833
|
// If there is only one expression and no tag, is a container.
|
|
754
834
|
template = function(addChild) {
|
|
755
835
|
// Replace expressions.
|
|
756
|
-
return addChild(
|
|
836
|
+
return addChild(getExpressionResult(expressions[match[1]], this)).toString();
|
|
757
837
|
};
|
|
758
838
|
} else {
|
|
759
839
|
throw new SyntaxError('Invalid component');
|
package/lib/Emitter.cjs
CHANGED
|
@@ -43,7 +43,8 @@ class Emitter {
|
|
|
43
43
|
* @param {string} type Type of the event (e.g. `change`).
|
|
44
44
|
* @param {function} listener Callback function to be called when the event is emitted.
|
|
45
45
|
* @example
|
|
46
|
-
*
|
|
46
|
+
* // Re render when model changes.
|
|
47
|
+
* this.model.on('change', this.render.bind(this));
|
|
47
48
|
*/
|
|
48
49
|
on(type, listener) {
|
|
49
50
|
// Validate listener.
|
|
@@ -64,6 +65,7 @@ class Emitter {
|
|
|
64
65
|
* @param {string} type Type of the event (e.g. `change`).
|
|
65
66
|
* @param {function} listener Callback function to be called when the event is emitted.
|
|
66
67
|
* @example
|
|
68
|
+
* // Log a message once when model changes.
|
|
67
69
|
* this.model.once('change', () => console.log('This will happen once'));
|
|
68
70
|
*/
|
|
69
71
|
once(type, listener) {
|
|
@@ -86,7 +88,8 @@ class Emitter {
|
|
|
86
88
|
* @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.
|
|
87
89
|
* @param {function} [listener] Callback function to be called when the event is emitted. If listener is not provided, it removes all listeners for specified type.
|
|
88
90
|
* @example
|
|
89
|
-
*
|
|
91
|
+
* // Stop listening to changes.
|
|
92
|
+
* this.model.off('change');
|
|
90
93
|
*/
|
|
91
94
|
off(type, listener) {
|
|
92
95
|
// No listeners.
|
|
@@ -115,7 +118,8 @@ class Emitter {
|
|
|
115
118
|
* @param {string} type Type of the event (e.g. `change`).
|
|
116
119
|
* @param {any} [...args] Arguments to be passed to listener.
|
|
117
120
|
* @example
|
|
118
|
-
*
|
|
121
|
+
* // Emit validation error event.
|
|
122
|
+
* this.emit('invalid');
|
|
119
123
|
*/
|
|
120
124
|
emit(type, ...args) {
|
|
121
125
|
// No listeners.
|
package/lib/Model.cjs
CHANGED
|
@@ -12,13 +12,13 @@ var utils_getResult = require('./utils/getResult.cjs');
|
|
|
12
12
|
* that contain all the necessary functions for manipulating their specific data.
|
|
13
13
|
* Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
|
|
14
14
|
* Rasti models store their attributes in `this.attributes`, which is extended from `this.defaults` and the
|
|
15
|
-
* constructor `
|
|
15
|
+
* constructor `attributes` parameter. For every attribute, a getter is generated to retrieve the model property
|
|
16
16
|
* from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
|
|
17
17
|
* and `change:attribute` events.
|
|
18
18
|
* @module
|
|
19
19
|
* @extends Rasti.Emitter
|
|
20
|
-
* @param {object}
|
|
21
|
-
* @property {object} defaults Object containing default attributes for the model. It will extend `this.attributes`. If a function is passed, it will be called to get the defaults. It will be bound to the model instance.
|
|
20
|
+
* @param {object} attributes Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributes`, in order to emit `change` events.
|
|
21
|
+
* @property {object|function} defaults Object containing default attributes for the model. It will extend `this.attributes`. If a function is passed, it will be called to get the defaults. It will be bound to the model instance.
|
|
22
22
|
* @property {object} previous Object containing previous attributes when a change occurs.
|
|
23
23
|
* @example
|
|
24
24
|
* import { Model } from 'rasti';
|
|
@@ -51,14 +51,14 @@ var utils_getResult = require('./utils/getResult.cjs');
|
|
|
51
51
|
* product.setDiscount(10); // Output: "New Price: 900"
|
|
52
52
|
*/
|
|
53
53
|
class Model extends Emitter {
|
|
54
|
-
constructor(
|
|
54
|
+
constructor(attributes = {}) {
|
|
55
55
|
super();
|
|
56
56
|
// Call preinitialize.
|
|
57
57
|
this.preinitialize.apply(this, arguments);
|
|
58
58
|
// Get defaults. If `this.defaults` is a function, call it.
|
|
59
59
|
const defaults = utils_getResult(this.defaults, this) || {};
|
|
60
60
|
// Set attributes object with defaults and passed attributes.
|
|
61
|
-
this.attributes = Object.assign({}, defaults,
|
|
61
|
+
this.attributes = Object.assign({}, defaults, attributes);
|
|
62
62
|
// Object to store previous attributes when a change occurs.
|
|
63
63
|
this.previous = {};
|
|
64
64
|
// Generate getters/setters for every attribute.
|
|
@@ -67,7 +67,7 @@ class Model extends Emitter {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* If you define a preinitialize method, it will be invoked when the Model is first created, before any instantiation logic is run for the Model.
|
|
70
|
-
* @param {object}
|
|
70
|
+
* @param {object} attributes Object containing model attributes to extend `this.attributes`.
|
|
71
71
|
*/
|
|
72
72
|
preinitialize() {}
|
|
73
73
|
|