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/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 Rasti.Emitter
33
- * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
34
- * @property {node} el Every view has a root element, `this.el`. If not present it will be created. If a function is passed, it will be called to get the element. It will be bound to the view instance.
35
- * @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`. If a function is passed, it will be called to get the tag name. It will be bound to the view instance.
36
- * @property {object} attributes If `this.el` is not present, an element will be created using `this.attributes`. If a function is passed, it will be called to get the attributes. It will be bound to the view instance.
37
- * @property {object} events Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element. If a function is passed, it will be called to get the events. It will be bound to the view instance.
38
- * @property {object} model A `Rasti.Model` or any object containing data and business logic.
39
- * @property {function} template A function that receives data and returns a markup string (e.g., HTML).
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} attrs Attributes for the element.
188
+ * @param {object} attributes Attributes for the element.
189
189
  * @return {node} The created element.
190
190
  */
191
- createElement(tag = 'div', attrs = {}) {
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(attrs)
196
- .forEach(key => el.setAttribute(key, attrs[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 hash is not passed directly,
213
- * uses `this.events` as the source.
214
- * Events are written in the format `{'event selector' : 'listener'}`.
215
- * The listener may be either the name of a method on the view, or a direct function body.
216
- * Omitting the selector causes the event to be bound to the view's root element (`this.el`).
217
- * By default, `delegateEvents` is called within the View's constructor,
218
- * so if you have a simple events hash, all of your DOM events will always already be connected,
219
- * and you will never have to call this function yourself.
220
- * All attached listeners are bound to the view automatically, so when the listeners are invoked,
221
- * `this` continues to refer to the view object.
222
- * When `delegateEvents` is run again, perhaps with a different events hash, all listeners
223
- * are removed and delegated afresh.
224
- * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.
225
- * @return {Rasti.View} Return `this` for chaining.
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
- * MyView.prototype.events = {
228
- * 'click button.ok' : 'onClickOkButton',
229
- * 'click button.cancel' : function() {}
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 only manipulate the DOM within the scope of `this.el`,
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 must call `this.destroyChildren`.
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> &#9888; **Security Notice:** The default implementation utilizes `innerHTML` on the root element
301
- * for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated
302
- * content is properly sanitized before inserting it into the DOM. For best practices on secure data handling,
303
- * refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
304
- * @return {Rasti.View} Return `this` for chaining.
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
+ '&' : '&amp;',
341
+ '<' : '&lt;',
342
+ '>' : '&gt;',
343
+ '"' : '&quot;',
344
+ '\'' : '&#039;'
345
+ }[match]));
346
+ }
311
347
  }
312
348
 
313
349
  /*
package/lib/index.cjs CHANGED
@@ -5,6 +5,7 @@ var Model = require('./Model.cjs');
5
5
  var View = require('./View.cjs');
6
6
  var Component = require('./Component.cjs');
7
7
  require('./utils/getResult.cjs');
8
+ require('./utils/deepFlat.cjs');
8
9
 
9
10
 
10
11
 
@@ -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.0",
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 --module-index-format none --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"
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 renderExpression = (expression, context) => getResult(expression, context, context);
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 renderExpression {function} Function to render expressions.
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, renderExpression) => {
77
+ const expandAttributes = (attributes, getExpressionResult) => {
64
78
  const out = attributes.reduce((out, pair) => {
65
- const attribute = renderExpression(pair[0]);
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 = renderExpression(pair[1]);
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 => renderExpression(item, this)).flat();
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 => renderExpression(value, this)).all;
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 `create` static method, which takes a tagged template.
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 splitPlaceholders(
468
- expandComponents(
469
- addPlaceholders(strings, expressions), expressions
470
- ), expressions
471
- ).map(item => {
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> &#9888; **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 containing an HTML string,
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
- * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it.
666
- * - If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
667
- * - If the function returns a component instance, it will be added as a child component.
668
- * - If the function returns an array, each item will be evaluated as above.
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 renderExpression(tagExpression, this);
791
+ return View.sanitize(getExpressionResult(tagExpression, this));
714
792
  };
715
793
  // Get attributes.
716
794
  attributes = function() {
717
- return expandAttributes(attributesAndEvents, value => renderExpression(value, this)).attributes;
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 => renderExpression(value, this)).events;
799
+ const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
722
800
 
723
801
  return Object.keys(onlyEvents).reduce((out, key) => {
724
- const typeListeners = renderExpression(onlyEvents[key], this);
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 => renderExpression(item, this)).flat().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
- return item instanceof Component ? addChild(item) : item;
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(renderExpression(expressions[match[1]], this)).toString();
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
- * this.model.on('change', this.render.bind(this)); // Re render when model changes.
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
- * this.model.off('change'); // Stop listening to changes.
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
- * this.emit('invalid'); // Emit validation error event.
119
+ * // Emit validation error event.
120
+ * this.emit('invalid');
117
121
  */
118
122
  emit(type, ...args) {
119
123
  // No listeners.