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/dist/rasti.js CHANGED
@@ -47,7 +47,8 @@
47
47
  * @param {string} type Type of the event (e.g. `change`).
48
48
  * @param {function} listener Callback function to be called when the event is emitted.
49
49
  * @example
50
- * this.model.on('change', this.render.bind(this)); // Re render when model changes.
50
+ * // Re render when model changes.
51
+ * this.model.on('change', this.render.bind(this));
51
52
  */
52
53
  on(type, listener) {
53
54
  // Validate listener.
@@ -68,6 +69,7 @@
68
69
  * @param {string} type Type of the event (e.g. `change`).
69
70
  * @param {function} listener Callback function to be called when the event is emitted.
70
71
  * @example
72
+ * // Log a message once when model changes.
71
73
  * this.model.once('change', () => console.log('This will happen once'));
72
74
  */
73
75
  once(type, listener) {
@@ -90,7 +92,8 @@
90
92
  * @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.
91
93
  * @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.
92
94
  * @example
93
- * this.model.off('change'); // Stop listening to changes.
95
+ * // Stop listening to changes.
96
+ * this.model.off('change');
94
97
  */
95
98
  off(type, listener) {
96
99
  // No listeners.
@@ -119,7 +122,8 @@
119
122
  * @param {string} type Type of the event (e.g. `change`).
120
123
  * @param {any} [...args] Arguments to be passed to listener.
121
124
  * @example
122
- * this.emit('invalid'); // Emit validation error event.
125
+ * // Emit validation error event.
126
+ * this.emit('invalid');
123
127
  */
124
128
  emit(type, ...args) {
125
129
  // No listeners.
@@ -155,13 +159,13 @@
155
159
  * that contain all the necessary functions for manipulating their specific data.
156
160
  * Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
157
161
  * Rasti models store their attributes in `this.attributes`, which is extended from `this.defaults` and the
158
- * constructor `attrs` parameter. For every attribute, a getter is generated to retrieve the model property
162
+ * constructor `attributes` parameter. For every attribute, a getter is generated to retrieve the model property
159
163
  * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
160
164
  * and `change:attribute` events.
161
165
  * @module
162
166
  * @extends Rasti.Emitter
163
- * @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.
164
- * @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.
167
+ * @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.
168
+ * @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.
165
169
  * @property {object} previous Object containing previous attributes when a change occurs.
166
170
  * @example
167
171
  * import { Model } from 'rasti';
@@ -194,14 +198,14 @@
194
198
  * product.setDiscount(10); // Output: "New Price: 900"
195
199
  */
196
200
  class Model extends Emitter {
197
- constructor(attrs = {}) {
201
+ constructor(attributes = {}) {
198
202
  super();
199
203
  // Call preinitialize.
200
204
  this.preinitialize.apply(this, arguments);
201
205
  // Get defaults. If `this.defaults` is a function, call it.
202
206
  const defaults = getResult(this.defaults, this) || {};
203
207
  // Set attributes object with defaults and passed attributes.
204
- this.attributes = Object.assign({}, defaults, attrs);
208
+ this.attributes = Object.assign({}, defaults, attributes);
205
209
  // Object to store previous attributes when a change occurs.
206
210
  this.previous = {};
207
211
  // Generate getters/setters for every attribute.
@@ -210,7 +214,7 @@
210
214
 
211
215
  /**
212
216
  * 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.
213
- * @param {object} attrs Object containing model attributes to extend `this.attributes`.
217
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`.
214
218
  */
215
219
  preinitialize() {}
216
220
 
@@ -330,7 +334,7 @@
330
334
  };
331
335
 
332
336
  /**
333
- * - Listens for changes and renders UI.
337
+ * - Listens for changes and renders the UI.
334
338
  * - Handles user input and interactivity.
335
339
  * - Sends captured input to the model.
336
340
  *
@@ -340,16 +344,16 @@
340
344
  * emitted by the models to re-render themselves based on changes.
341
345
  * Each `View` has a root element, `this.el`, which is used for event delegation.
342
346
  * All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it.
343
- * If `this.el` is not present, an element will be created using `this.tag` (defaulting to div) and `this.attributes`.
347
+ * If `this.el` is not present, an element will be created using `this.tag` (defaulting to `div`) and `this.attributes`.
344
348
  * @module
345
- * @extends Rasti.Emitter
346
- * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
347
- * @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.
348
- * @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.
349
- * @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.
350
- * @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.
351
- * @property {object} model A `Rasti.Model` or any object containing data and business logic.
352
- * @property {function} template A function that receives data and returns a markup string (e.g., HTML).
349
+ * @extends Emitter
350
+ * @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.
351
+ * @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}.
352
+ * @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}.
353
+ * @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}.
354
+ * @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}.
355
+ * @property {object} model A model or any object containing data and business logic.
356
+ * @property {function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
353
357
  * @example
354
358
  * import { View } from 'rasti';
355
359
  *
@@ -498,15 +502,15 @@
498
502
  * Called from the constructor if `this.el` is undefined, to ensure
499
503
  * the view has a root element.
500
504
  * @param {string} tag Tag for the element. Default to `div`
501
- * @param {object} attrs Attributes for the element.
505
+ * @param {object} attributes Attributes for the element.
502
506
  * @return {node} The created element.
503
507
  */
504
- createElement(tag = 'div', attrs = {}) {
508
+ createElement(tag = 'div', attributes = {}) {
505
509
  // Create DOM element.
506
510
  let el = document.createElement(tag);
507
511
  // Add element attributes.
508
- Object.keys(attrs)
509
- .forEach(key => el.setAttribute(key, attrs[key]));
512
+ Object.keys(attributes)
513
+ .forEach(key => el.setAttribute(key, attributes[key]));
510
514
 
511
515
  return el;
512
516
  }
@@ -522,24 +526,39 @@
522
526
  }
523
527
 
524
528
  /**
525
- * Provide declarative listeners for DOM events within a view. If an events hash is not passed directly,
526
- * uses `this.events` as the source.
527
- * Events are written in the format `{'event selector' : 'listener'}`.
528
- * The listener may be either the name of a method on the view, or a direct function body.
529
- * Omitting the selector causes the event to be bound to the view's root element (`this.el`).
530
- * By default, `delegateEvents` is called within the View's constructor,
531
- * so if you have a simple events hash, all of your DOM events will always already be connected,
532
- * and you will never have to call this function yourself.
533
- * All attached listeners are bound to the view automatically, so when the listeners are invoked,
534
- * `this` continues to refer to the view object.
535
- * When `delegateEvents` is run again, perhaps with a different events hash, all listeners
536
- * are removed and delegated afresh.
537
- * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.
538
- * @return {Rasti.View} Return `this` for chaining.
529
+ * Provide declarative listeners for DOM events within a view. If an events object is not provided,
530
+ * it defaults to using `this.events`. If `this.events` is a function, it will be called to get the events object.
531
+ *
532
+ * The events object should follow the format `{'event selector': 'listener'}`:
533
+ * - `event`: The type of event (e.g., 'click').
534
+ * - `selector`: A CSS selector to match the event target. If omitted, the event is bound to the root element.
535
+ * - `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.
536
+ *
537
+ * By default, `delegateEvents` is called within the View's constructor. If you have a simple events object,
538
+ * all of your DOM events will be connected automatically, and you will not need to call this function manually.
539
+ *
540
+ * All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.
541
+ * When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.
542
+ *
543
+ * The listeners will be invoked with the event and the view as arguments.
544
+ *
545
+ * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
546
+ * @return {Rasti.View} Returns `this` for chaining.
539
547
  * @example
540
- * MyView.prototype.events = {
541
- * 'click button.ok' : 'onClickOkButton',
542
- * 'click button.cancel' : function() {}
548
+ * // Using a function.
549
+ * class Modal extends View {
550
+ * events() {
551
+ * return {
552
+ * 'click button.ok': 'onClickOkButton',
553
+ * 'click button.cancel': function() {}
554
+ * };
555
+ * }
556
+ * }
557
+ *
558
+ * // Using an object.
559
+ * Modal.prototype.events = {
560
+ * 'click button.ok' : 'onClickOkButton',
561
+ * 'click button.cancel' : function() {}
543
562
  * };
544
563
  */
545
564
  delegateEvents(events) {
@@ -589,7 +608,7 @@
589
608
  /**
590
609
  * Removes all of the view's delegated events.
591
610
  * Useful if you want to disable or remove a view from the DOM temporarily.
592
- * Called automatically when the view is destroyed.
611
+ * Called automatically when the view is destroyed and when `delegateEvents` is called again.
593
612
  * @return {Rasti.View} Return `this` for chaining.
594
613
  */
595
614
  undelegateEvents() {
@@ -605,22 +624,43 @@
605
624
  /**
606
625
  * Renders the view.
607
626
  * This method should be overridden with custom logic.
608
- * The convention is to only manipulate the DOM within the scope of `this.el`,
627
+ * The only convention is to manipulate the DOM within the scope of `this.el`,
609
628
  * and to return `this` for chaining.
610
- * If you add any child views, you must call `this.destroyChildren`.
629
+ * If you add any child views, you should call `this.destroyChildren` before re-rendering.
611
630
  * The default implementation sets the innerHTML of `this.el` with the result
612
631
  * of calling `this.template`, passing `this.model` as an argument.
613
632
  * <br><br> &#9888; **Security Notice:** The default implementation utilizes `innerHTML` on the root element
614
- * for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated
615
- * content is properly sanitized before inserting it into the DOM. For best practices on secure data handling,
616
- * refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
617
- * @return {Rasti.View} Return `this` for chaining.
633
+ * for rendering, which may introduce Cross-Site Scripting (XSS) risks. Ensure that any user-generated
634
+ * content is properly sanitized before inserting it into the DOM. You can use the @link{#module_view_sanitize View.sanitize}
635
+ * static method to escape HTML entities in a string.
636
+ * For best practices on secure data handling, refer to the
637
+ * [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
638
+ * @return {Rasti.View} Returns `this` for chaining.
618
639
  */
619
640
  render() {
620
641
  if (this.template) this.el.innerHTML = this.template(this.model);
621
642
  // Return `this` for chaining.
622
643
  return this;
623
644
  }
645
+
646
+ /**
647
+ * Escape HTML entities in a string.
648
+ * Use method to sanitize user-generated content before inserting it into the DOM.
649
+ * Override this method to provide a custom escape function.
650
+ * This method is used by `Component` to escape template interpolations.
651
+ * @static
652
+ * @param {string} str String to escape.
653
+ * @return {string} Escaped string.
654
+ */
655
+ static sanitize(value) {
656
+ return `${value}`.replace(/[&<>"']/g, match => ({
657
+ '&' : '&amp;',
658
+ '<' : '&lt;',
659
+ '>' : '&gt;',
660
+ '"' : '&quot;',
661
+ '\'' : '&#039;'
662
+ }[match]));
663
+ }
624
664
  }
625
665
 
626
666
  /*
@@ -628,6 +668,30 @@
628
668
  */
629
669
  View.uid = 0;
630
670
 
671
+ /*
672
+ * Flatten an array recursively
673
+ * @param {Array} arr Array to flat recursively
674
+ * @return {Array} Flat array
675
+ */
676
+ const deepFlat = (arr) => arr.reduce((acc, val) => {
677
+ if (Array.isArray(val)) acc.push(...deepFlat(val));
678
+ else acc.push(val);
679
+ return acc;
680
+ }, []);
681
+
682
+ /*
683
+ * Wrapper class for HTML strings marked as safe.
684
+ */
685
+ class SafeHTML {
686
+ constructor(value) {
687
+ this.value = value;
688
+ }
689
+
690
+ toString() {
691
+ return this.value;
692
+ }
693
+ }
694
+
631
695
  /*
632
696
  * Same as getResult, but pass context as argument to the expression.
633
697
  * Used to evaluate expressions in the context of a component.
@@ -635,7 +699,7 @@
635
699
  * @param {any} context The context to call the expression with.
636
700
  * @return {any} The result of the evaluated expression.
637
701
  */
638
- const renderExpression = (expression, context) => getResult(expression, context, context);
702
+ const getExpressionResult = (expression, context) => getResult(expression, context, context);
639
703
 
640
704
  /*
641
705
  * Generate string with placeholders for interpolated expressions.
@@ -670,10 +734,10 @@
670
734
  // so all the components are added as children by the parent component.
671
735
  while ((match = regExp.exec(main)) !== null) {
672
736
  const before = main.slice(lastIndex, match.index);
673
- out.push(before, expressions[match[1]]);
737
+ out.push(new SafeHTML(before), expressions[match[1]]);
674
738
  lastIndex = match.index + match[0].length;
675
739
  }
676
- out.push(main.slice(lastIndex));
740
+ out.push(new SafeHTML(main.slice(lastIndex)));
677
741
 
678
742
  return out;
679
743
  };
@@ -681,15 +745,15 @@
681
745
  /*
682
746
  * Expand attributes.
683
747
  * @param attributes {array} Array of attributes as key, value pairs.
684
- * @param renderExpression {function} Function to render expressions.
748
+ * @param getExpressionResult {function} Function to render expressions.
685
749
  * @return {object}
686
750
  * @property {object} all All attributes.
687
751
  * @property {object} events Event listeners.
688
752
  * @property {object} attributes Attributes.
689
753
  */
690
- const expandAttributes = (attributes, renderExpression) => {
754
+ const expandAttributes = (attributes, getExpressionResult) => {
691
755
  const out = attributes.reduce((out, pair) => {
692
- const attribute = renderExpression(pair[0]);
756
+ const attribute = getExpressionResult(pair[0]);
693
757
  // Attribute without value.
694
758
  if (pair.length === 1) {
695
759
  if (typeof attribute === 'object') {
@@ -701,7 +765,7 @@
701
765
  }
702
766
  } else {
703
767
  // Attribute with value.
704
- const value = renderExpression(pair[1]);
768
+ const value = getExpressionResult(pair[1]);
705
769
  out.all[attribute] = value;
706
770
  }
707
771
 
@@ -752,12 +816,12 @@
752
816
  const list = splitPlaceholders(expandComponents(inner, expressions), expressions);
753
817
  // Create renderChildren function.
754
818
  renderChildren = function() {
755
- return list.map(item => renderExpression(item, this)).flat();
819
+ return deepFlat(list.map(item => getExpressionResult(item, this)));
756
820
  };
757
821
  }
758
822
  // Create mount function.
759
823
  const mount = function() {
760
- const options = expandAttributes(attributes, value => renderExpression(value, this)).all;
824
+ const options = expandAttributes(attributes, value => getExpressionResult(value, this)).all;
761
825
  // Add renderChildren function to options.
762
826
  if (renderChildren) options.renderChildren = renderChildren.bind(this);
763
827
  // Mount component.
@@ -821,6 +885,14 @@
821
885
  return data;
822
886
  };
823
887
 
888
+ /*
889
+ * HTML tags that are self closing.
890
+ */
891
+ const selfClosingTags = {
892
+ area : true, base : true, br : true, col : true, embed : true, hr : true,
893
+ img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
894
+ };
895
+
824
896
  /*
825
897
  * These option keys will be extended on the component instance.
826
898
  */
@@ -832,26 +904,19 @@
832
904
  onRender : true
833
905
  };
834
906
 
835
- /*
836
- * HTML tags that are self closing.
837
- */
838
- const selfClosingTags = {
839
- area : true, base : true, br : true, col : true, embed : true, hr : true,
840
- img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
841
- };
842
-
843
907
  /**
844
908
  * Components are a special kind of `View` that is designed to be easily composable,
845
909
  * making it simple to add child views and build complex user interfaces.
846
910
  * Unlike views, which are render-agnostic, components have a specific set of rendering
847
911
  * guidelines that allow for a more declarative development style.
848
- * Components are defined with the `create` static method, which takes a tagged template.
912
+ * 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.
849
913
  * @module
850
914
  * @extends Rasti.View
851
915
  * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
852
916
  * @property {string} key A unique key to identify the component. Used to recycle child components.
853
917
  * @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.
854
918
  * @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.
919
+ * @see {@link #module_component_create Component.create}
855
920
  * @example
856
921
  * import { Component, Model } from 'rasti';
857
922
  * // Create Timer component.
@@ -950,9 +1015,11 @@
950
1015
  const attributes = { [Component.DATA_ATTRIBUTE_UID] : this.uid };
951
1016
 
952
1017
  if (this.attributes) Object.assign(attributes, getResult(this.attributes, this));
1018
+ // Store previous attributes.
1019
+ const previousAttributes = this.previousAttributes || {};
1020
+ this.previousAttributes = attributes;
953
1021
 
954
1022
  Object.keys(attributes).forEach(key => {
955
- // Evaluate attribute value.
956
1023
  let value = attributes[key];
957
1024
  // Transform bool attribute values
958
1025
  if (value === false) {
@@ -964,7 +1031,13 @@
964
1031
  if (value === null || typeof value === 'undefined') value = '';
965
1032
 
966
1033
  add[key] = value;
967
- html.push(`${key}="${value}"`);
1034
+ html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
1035
+ }
1036
+ });
1037
+ // Remove attributes that were in previousAttributes but not in current attributes.
1038
+ Object.keys(previousAttributes).forEach(key => {
1039
+ if (!(key in attributes)) {
1040
+ remove[key] = true;
968
1041
  }
969
1042
  });
970
1043
 
@@ -1091,13 +1164,11 @@
1091
1164
  * });
1092
1165
  */
1093
1166
  partial(strings, ...expressions) {
1094
- return splitPlaceholders(
1095
- expandComponents(
1096
- addPlaceholders(strings, expressions), expressions
1097
- ), expressions
1098
- ).map(item => {
1099
- return renderExpression(item, this);
1100
- }).flat();
1167
+ return deepFlat(
1168
+ splitPlaceholders(
1169
+ expandComponents(addPlaceholders(strings, expressions), expressions), expressions
1170
+ ).map(item => getExpressionResult(item, this))
1171
+ );
1101
1172
  }
1102
1173
 
1103
1174
  getRecyclePlaceholder() {
@@ -1257,7 +1328,6 @@
1257
1328
  * It instantiate the Component view using options,
1258
1329
  * appends its element into the DOM (if `el` is provided).
1259
1330
  * And returns the view instance.
1260
- * <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>
1261
1331
  * @static
1262
1332
  * @param {object} options The view options.
1263
1333
  * @param {node} el Dom element to append the view element.
@@ -1283,38 +1353,96 @@
1283
1353
  }
1284
1354
 
1285
1355
  /**
1286
- * Takes a tagged template containing an HTML string,
1287
- * and returns a new `Component` class.
1356
+ * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
1288
1357
  * - The template outer tag and attributes will be used to create the view's root element.
1289
- * - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
1290
- * - 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.
1291
1358
  * - The template inner HTML will be used as the view's template.
1292
- * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it.
1293
- * - If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
1294
- * - If the function returns a component instance, it will be added as a child component.
1295
- * - If the function returns an array, each item will be evaluated as above.
1359
+ * ```javascript
1360
+ * const Button = Component.create`<button class="button">Click me</button>`;
1361
+ * ```
1362
+ * - 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.
1363
+ * ```javascript
1364
+ * const Button = Component.create`
1365
+ * <button class="${({ options }) => options.className}">
1366
+ * ${({ options }) => options.renderChildren()}
1367
+ * </button>
1368
+ * `;
1369
+ * ```
1370
+ * - 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}.
1371
+ * - 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.
1372
+ * ```javascript
1373
+ * const Input = Component.create`
1374
+ * <input type="text" disabled=${({ options }) => options.disabled} />
1375
+ * `;
1376
+ * ```
1377
+ * - If the interpolated function returns a component instance, it will be added as a child component.
1378
+ * - If the interpolated function returns an array, each item will be evaluated as above.
1379
+ * ```javascript
1380
+ * // Create a button component.
1381
+ * const Button = Component.create`
1382
+ * <button class="button">
1383
+ * ${({ options }) => options.renderChildren()}
1384
+ * </button>
1385
+ * `;
1386
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1387
+ * const Navigation = Component.create`
1388
+ * <nav>
1389
+ * ${({ options }) => options.items.map(
1390
+ * item => Button.mount({ renderChildren: () => item.label })
1391
+ * )}
1392
+ * </nav>
1393
+ * `;
1394
+ * // Create a header component. Add navigation as a child.
1395
+ * const Header = Component.create`
1396
+ * <header>
1397
+ * ${({ options }) => Navigation.mount({ items : options.items})}
1398
+ * </header>
1399
+ * `;
1400
+ * ```
1401
+ * - Child components can be added using a component tag.
1402
+ * ```javascript
1403
+ * // Create a button component.
1404
+ * const Button = Component.create`
1405
+ * <button class="button">
1406
+ * ${({ options }) => options.renderChildren()}
1407
+ * </button>
1408
+ * `;
1409
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1410
+ * const Navigation = Component.create`
1411
+ * <nav>
1412
+ * ${self => self.options.items.map(
1413
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
1414
+ * )}
1415
+ * </nav>
1416
+ * `;
1417
+ * // Create a header component. Add navigation as a child.
1418
+ * const Header = Component.create`
1419
+ * <header>
1420
+ * <${Navigation} items="${({ options }) => options.items}" />
1421
+ * </header>
1422
+ * `;
1423
+ * ```
1296
1424
  * - 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.
1425
+ * ```javascript
1426
+ * // Create a button component.
1427
+ * const Button = Component.create`
1428
+ * <button class="${({ options }) => options.className}">
1429
+ * ${self => self.renderChildren()}
1430
+ * </button>
1431
+ * `;
1432
+ * // Create a container using the button component
1433
+ * const ButtonOk = Component.create`
1434
+ * <${Button} className="ok">Ok</${Button}>
1435
+ * `;
1436
+ * // Create a button component using a function
1437
+ * const ButtonCancel = Component.create(() => Button.mount({
1438
+ * className: 'cancel',
1439
+ * renderChildren: () => 'Cancel'
1440
+ * }));
1441
+ * ```
1297
1442
  * @static
1298
1443
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
1299
1444
  * @param {...*} expressions - The expressions to be interpolated within the template.
1300
1445
  * @return {Rasti.Component} The newly created component class.
1301
- * @example
1302
- * import { Component } from 'rasti';
1303
- * // Create a button component.
1304
- * const Button = Component.create`
1305
- * <button class="${({ options }) => options.className}">
1306
- * ${self => self.renderChildren()}
1307
- * </button>
1308
- * `;
1309
- * // Create a container using the button component
1310
- * const ButtonOk = Component.create`
1311
- * <${Button} className="ok">Ok</${Button}>
1312
- * `;
1313
- * // Create a button component using a function
1314
- * const ButtonCancel = Component.create(() => Button.mount({
1315
- * className: 'cancel',
1316
- * renderChildren: () => 'Cancel'
1317
- * }));
1318
1446
  */
1319
1447
  static create(strings, ...expressions) {
1320
1448
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -1337,18 +1465,18 @@
1337
1465
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
1338
1466
  // Get tag, attributes.
1339
1467
  tag = function() {
1340
- return renderExpression(tagExpression, this);
1468
+ return View.sanitize(getExpressionResult(tagExpression, this));
1341
1469
  };
1342
1470
  // Get attributes.
1343
1471
  attributes = function() {
1344
- return expandAttributes(attributesAndEvents, value => renderExpression(value, this)).attributes;
1472
+ return expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).attributes;
1345
1473
  };
1346
1474
  // Get events.
1347
1475
  events = function() {
1348
- const onlyEvents = expandAttributes(attributesAndEvents, value => renderExpression(value, this)).events;
1476
+ const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
1349
1477
 
1350
1478
  return Object.keys(onlyEvents).reduce((out, key) => {
1351
- const typeListeners = renderExpression(onlyEvents[key], this);
1479
+ const typeListeners = getExpressionResult(onlyEvents[key], this);
1352
1480
 
1353
1481
  Object.keys(typeListeners).forEach(selector => {
1354
1482
  out[`${key}${selector === '&' ? '' : ` ${selector}`}`] = typeListeners[selector];
@@ -1361,9 +1489,11 @@
1361
1489
  if (close) {
1362
1490
  const list = inner ? splitPlaceholders(inner, expressions) : [];
1363
1491
  template = function(addChild) {
1364
- return list.map(item => renderExpression(item, this)).flat().map(item => {
1492
+ return deepFlat(list.map(item => getExpressionResult(item, this))).map(item => {
1365
1493
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
1366
- return item instanceof Component ? addChild(item) : item;
1494
+ if (item instanceof SafeHTML) return item;
1495
+ if (item instanceof Component) return addChild(item);
1496
+ return View.sanitize(`${item}`);
1367
1497
  }
1368
1498
  return '';
1369
1499
  }).join('');
@@ -1377,7 +1507,7 @@
1377
1507
  // If there is only one expression and no tag, is a container.
1378
1508
  template = function(addChild) {
1379
1509
  // Replace expressions.
1380
- return addChild(renderExpression(expressions[match[1]], this)).toString();
1510
+ return addChild(getExpressionResult(expressions[match[1]], this)).toString();
1381
1511
  };
1382
1512
  } else {
1383
1513
  throw new SyntaxError('Invalid component');