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/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 Rasti.Emitter
31
- * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
32
- * @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.
33
- * @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.
34
- * @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.
35
- * @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.
36
- * @property {object} model A `Rasti.Model` or any object containing data and business logic.
37
- * @property {function} template A function that receives data and returns a markup string (e.g., HTML).
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} attrs Attributes for the element.
186
+ * @param {object} attributes Attributes for the element.
187
187
  * @return {node} The created element.
188
188
  */
189
- createElement(tag = 'div', attrs = {}) {
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(attrs)
194
- .forEach(key => el.setAttribute(key, attrs[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 hash is not passed directly,
211
- * uses `this.events` as the source.
212
- * Events are written in the format `{'event selector' : 'listener'}`.
213
- * The listener may be either the name of a method on the view, or a direct function body.
214
- * Omitting the selector causes the event to be bound to the view's root element (`this.el`).
215
- * By default, `delegateEvents` is called within the View's constructor,
216
- * so if you have a simple events hash, all of your DOM events will always already be connected,
217
- * and you will never have to call this function yourself.
218
- * All attached listeners are bound to the view automatically, so when the listeners are invoked,
219
- * `this` continues to refer to the view object.
220
- * When `delegateEvents` is run again, perhaps with a different events hash, all listeners
221
- * are removed and delegated afresh.
222
- * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.
223
- * @return {Rasti.View} Return `this` for chaining.
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
- * MyView.prototype.events = {
226
- * 'click button.ok' : 'onClickOkButton',
227
- * 'click button.cancel' : function() {}
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 only manipulate the DOM within the scope of `this.el`,
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 must call `this.destroyChildren`.
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> &#9888; **Security Notice:** The default implementation utilizes `innerHTML` on the root element
299
- * for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated
300
- * content is properly sanitized before inserting it into the DOM. For best practices on secure data handling,
301
- * refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
302
- * @return {Rasti.View} Return `this` for chaining.
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
+ '&' : '&amp;',
339
+ '<' : '&lt;',
340
+ '>' : '&gt;',
341
+ '"' : '&quot;',
342
+ '\'' : '&#039;'
343
+ }[match]));
344
+ }
309
345
  }
310
346
 
311
347
  /*
package/es/index.js CHANGED
@@ -3,3 +3,4 @@ export { default as Model } from './Model.js';
3
3
  export { default as View } from './View.js';
4
4
  export { default as Component } from './Component.js';
5
5
  import './utils/getResult.js';
6
+ import './utils/deepFlat.js';
@@ -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 renderExpression = (expression, context) => utils_getResult(expression, context, context);
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 renderExpression {function} Function to render expressions.
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, renderExpression) => {
80
+ const expandAttributes = (attributes, getExpressionResult) => {
67
81
  const out = attributes.reduce((out, pair) => {
68
- const attribute = renderExpression(pair[0]);
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 = renderExpression(pair[1]);
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 => renderExpression(item, this)).flat();
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 => renderExpression(value, this)).all;
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 `create` static method, which takes a tagged template.
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 splitPlaceholders(
471
- expandComponents(
472
- addPlaceholders(strings, expressions), expressions
473
- ), expressions
474
- ).map(item => {
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> &#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>
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 containing an HTML string,
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
- * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it.
669
- * - If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
670
- * - If the function returns a component instance, it will be added as a child component.
671
- * - If the function returns an array, each item will be evaluated as above.
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 renderExpression(tagExpression, this);
794
+ return View.sanitize(getExpressionResult(tagExpression, this));
717
795
  };
718
796
  // Get attributes.
719
797
  attributes = function() {
720
- return expandAttributes(attributesAndEvents, value => renderExpression(value, this)).attributes;
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 => renderExpression(value, this)).events;
802
+ const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
725
803
 
726
804
  return Object.keys(onlyEvents).reduce((out, key) => {
727
- const typeListeners = renderExpression(onlyEvents[key], this);
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 => renderExpression(item, this)).flat().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
- return item instanceof Component ? addChild(item) : item;
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(renderExpression(expressions[match[1]], this)).toString();
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
- * this.model.on('change', this.render.bind(this)); // Re render when model changes.
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
- * this.model.off('change'); // Stop listening to changes.
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
- * this.emit('invalid'); // Emit validation error event.
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 `attrs` parameter. For every attribute, a getter is generated to retrieve the model property
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} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.
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(attrs = {}) {
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, attrs);
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} attrs Object containing model attributes to extend `this.attributes`.
70
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`.
71
71
  */
72
72
  preinitialize() {}
73
73