rasti 3.0.0-alpha.0 → 3.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -65
- package/dist/rasti.js +237 -107
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +140 -60
- package/es/Emitter.js +7 -3
- package/es/Model.js +6 -6
- package/es/View.js +74 -38
- package/es/index.js +1 -0
- package/es/utils/deepFlat.js +12 -0
- package/lib/Component.cjs +140 -60
- package/lib/Emitter.cjs +7 -3
- package/lib/Model.cjs +6 -6
- package/lib/View.cjs +74 -38
- package/lib/index.cjs +1 -0
- package/lib/utils/deepFlat.cjs +14 -0
- package/package.json +2 -2
- package/src/Component.js +141 -61
- package/src/Emitter.js +7 -3
- package/src/Model.js +6 -6
- package/src/View.js +74 -38
- package/src/utils/deepFlat.js +12 -0
package/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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 `
|
|
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}
|
|
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(
|
|
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,
|
|
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}
|
|
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
|
|
346
|
-
* @param {object} options Object containing options. The following keys will be merged
|
|
347
|
-
* @property {node} el Every view has a root element
|
|
348
|
-
* @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`. If a function
|
|
349
|
-
* @property {object} attributes If `this.el` is not present, an element will be created using `this.attributes`. If a function
|
|
350
|
-
* @property {object} events Object in the format `{'event selector' : 'listener'}`.
|
|
351
|
-
* @property {object} model A
|
|
352
|
-
* @property {function} template A function that
|
|
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}
|
|
505
|
+
* @param {object} attributes Attributes for the element.
|
|
502
506
|
* @return {node} The created element.
|
|
503
507
|
*/
|
|
504
|
-
createElement(tag = 'div',
|
|
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(
|
|
509
|
-
.forEach(key => el.setAttribute(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
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* The
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* are
|
|
537
|
-
*
|
|
538
|
-
*
|
|
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
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
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
|
|
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
|
|
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> ⚠ **Security Notice:** The default implementation utilizes `innerHTML` on the root element
|
|
614
|
-
* for rendering, which may introduce Cross
|
|
615
|
-
* content is properly sanitized before inserting it into the DOM.
|
|
616
|
-
*
|
|
617
|
-
*
|
|
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
|
+
'&' : '&',
|
|
658
|
+
'<' : '<',
|
|
659
|
+
'>' : '>',
|
|
660
|
+
'"' : '"',
|
|
661
|
+
'\'' : '''
|
|
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
|
|
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
|
|
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,
|
|
754
|
+
const expandAttributes = (attributes, getExpressionResult) => {
|
|
691
755
|
const out = attributes.reduce((out, pair) => {
|
|
692
|
-
const attribute =
|
|
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 =
|
|
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 =>
|
|
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 =>
|
|
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
|
|
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
|
|
1095
|
-
|
|
1096
|
-
addPlaceholders(strings, expressions), expressions
|
|
1097
|
-
),
|
|
1098
|
-
)
|
|
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> ⚠ **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
|
|
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
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
* - If the function returns an
|
|
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
|
|
1468
|
+
return View.sanitize(getExpressionResult(tagExpression, this));
|
|
1341
1469
|
};
|
|
1342
1470
|
// Get attributes.
|
|
1343
1471
|
attributes = function() {
|
|
1344
|
-
return expandAttributes(attributesAndEvents, value =>
|
|
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 =>
|
|
1476
|
+
const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
|
|
1349
1477
|
|
|
1350
1478
|
return Object.keys(onlyEvents).reduce((out, key) => {
|
|
1351
|
-
const typeListeners =
|
|
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 =>
|
|
1492
|
+
return deepFlat(list.map(item => getExpressionResult(item, this))).map(item => {
|
|
1365
1493
|
if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
|
|
1366
|
-
|
|
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(
|
|
1510
|
+
return addChild(getExpressionResult(expressions[match[1]], this)).toString();
|
|
1381
1511
|
};
|
|
1382
1512
|
} else {
|
|
1383
1513
|
throw new SyntaxError('Invalid component');
|