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