rasti 4.0.0-alpha.9 → 4.0.1
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 +15 -9
- package/dist/rasti.js +236 -104
- package/dist/rasti.js.map +1 -1
- package/dist/rasti.min.js +1 -1
- package/dist/rasti.min.js.map +1 -1
- package/es/Component.js +140 -53
- package/es/Component.js.map +1 -1
- package/es/View.js +58 -42
- package/es/View.js.map +1 -1
- package/es/utils/dev.js +1 -0
- package/es/utils/dev.js.map +1 -1
- package/es/utils/formatTemplateSource.js +1 -1
- package/es/utils/formatTemplateSource.js.map +1 -1
- package/es/utils/replaceNode.js +28 -11
- package/es/utils/replaceNode.js.map +1 -1
- package/lib/Component.cjs +140 -53
- package/lib/Component.cjs.map +1 -1
- package/lib/View.cjs +58 -42
- package/lib/View.cjs.map +1 -1
- package/lib/utils/dev.cjs +1 -0
- package/lib/utils/dev.cjs.map +1 -1
- package/lib/utils/formatTemplateSource.cjs +1 -1
- package/lib/utils/formatTemplateSource.cjs.map +1 -1
- package/lib/utils/replaceNode.cjs +28 -11
- package/lib/utils/replaceNode.cjs.map +1 -1
- package/package.json +8 -9
- package/src/Component.js +138 -51
- package/src/View.js +58 -39
- package/src/utils/dev.js +1 -0
- package/src/utils/formatTemplateSource.js +1 -1
- package/src/utils/replaceNode.js +28 -11
- /package/{LICENSE.md → LICENSE} +0 -0
package/dist/rasti.js
CHANGED
|
@@ -71,6 +71,18 @@
|
|
|
71
71
|
.join('\n');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Development mode flag.
|
|
76
|
+
* This will be replaced during build:
|
|
77
|
+
* - ESM/CJS: replaced with process.env.NODE_ENV !== 'production'
|
|
78
|
+
* - UMD dev: replaced with true
|
|
79
|
+
* - UMD prod: replaced with false
|
|
80
|
+
* @type {boolean}
|
|
81
|
+
* @module
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
const __DEV__ = true;
|
|
85
|
+
|
|
74
86
|
/**
|
|
75
87
|
* Validates that the listener is a function.
|
|
76
88
|
* @param {Function} listener The listener to validate.
|
|
@@ -782,7 +794,14 @@
|
|
|
782
794
|
* }
|
|
783
795
|
*
|
|
784
796
|
* template(model) {
|
|
785
|
-
* return `Seconds: <span>${model.seconds}</span>`;
|
|
797
|
+
* return `Seconds: <span>${View.sanitize(model.seconds)}</span>`;
|
|
798
|
+
* }
|
|
799
|
+
*
|
|
800
|
+
* render() {
|
|
801
|
+
* if (this.template) {
|
|
802
|
+
* this.el.innerHTML = this.template(this.model);
|
|
803
|
+
* }
|
|
804
|
+
* return this;
|
|
786
805
|
* }
|
|
787
806
|
* }
|
|
788
807
|
* // Render view and append view's element into the body.
|
|
@@ -1012,12 +1031,17 @@
|
|
|
1012
1031
|
if (this.delegatedEventListeners.length) this.undelegateEvents();
|
|
1013
1032
|
|
|
1014
1033
|
// Store events by type i.e.: "click", "submit", etc.
|
|
1015
|
-
|
|
1034
|
+
const eventTypes = {};
|
|
1016
1035
|
|
|
1017
1036
|
Object.keys(events).forEach(key => {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1037
|
+
const match = key.match(/^(\w+)(?:\s+(.+))*$/);
|
|
1038
|
+
|
|
1039
|
+
if (!match) {
|
|
1040
|
+
const message = `Invalid event format: ${key}`;
|
|
1041
|
+
throw new Error(createDevelopmentErrorMessage(message) );
|
|
1042
|
+
}
|
|
1043
|
+
// Extract type and selector from the event key.
|
|
1044
|
+
const [,type, selector] = match;
|
|
1021
1045
|
|
|
1022
1046
|
let listener = events[key];
|
|
1023
1047
|
// Listener may be a string representing a method name on the view, or a function.
|
|
@@ -1027,32 +1051,30 @@
|
|
|
1027
1051
|
|
|
1028
1052
|
if (!eventTypes[type]) eventTypes[type] = [];
|
|
1029
1053
|
|
|
1030
|
-
eventTypes[type].push(
|
|
1054
|
+
eventTypes[type].push([selector, listener]);
|
|
1031
1055
|
});
|
|
1032
1056
|
|
|
1033
1057
|
Object.keys(eventTypes).forEach(type => {
|
|
1034
1058
|
// Listener for the type of event.
|
|
1035
1059
|
const typeListener = (event) => {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (
|
|
1040
|
-
listener
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
while (node && node !== this.el) {
|
|
1047
|
-
if (node.matches && node.matches(selector)) {
|
|
1048
|
-
listener.call(this, event, this, node);
|
|
1049
|
-
}
|
|
1050
|
-
node = node.parentElement;
|
|
1060
|
+
let node = event.target;
|
|
1061
|
+
// Traverse ancestors until reaching the view root (`this.el`).
|
|
1062
|
+
while (node) {
|
|
1063
|
+
if (node.matches) {
|
|
1064
|
+
// Iterate and run every individual listener if the selector matches.
|
|
1065
|
+
eventTypes[type].forEach(([selector, listener]) => {
|
|
1066
|
+
if ((node === this.el && !selector) || (node !== this.el && node.matches(selector))) {
|
|
1067
|
+
listener.call(this, event, this, node);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1051
1070
|
}
|
|
1052
|
-
|
|
1071
|
+
// Continue traversing ancestors until reaching the view root (`this.el`) or stopping propagation.
|
|
1072
|
+
node = node === this.el || event.cancelBubble ? null : node.parentElement;
|
|
1073
|
+
}
|
|
1053
1074
|
};
|
|
1054
|
-
|
|
1055
|
-
this.delegatedEventListeners.push(
|
|
1075
|
+
// Store the type and listener in the delegated event listeners array.
|
|
1076
|
+
this.delegatedEventListeners.push([type, typeListener]);
|
|
1077
|
+
// Add the event listener to the element.
|
|
1056
1078
|
this.el.addEventListener(type, typeListener);
|
|
1057
1079
|
});
|
|
1058
1080
|
// Return `this` for chaining.
|
|
@@ -1066,7 +1088,7 @@
|
|
|
1066
1088
|
* @return {View} Return `this` for chaining.
|
|
1067
1089
|
*/
|
|
1068
1090
|
undelegateEvents() {
|
|
1069
|
-
this.delegatedEventListeners.forEach((
|
|
1091
|
+
this.delegatedEventListeners.forEach(([type, listener]) => {
|
|
1070
1092
|
this.el.removeEventListener(type, listener);
|
|
1071
1093
|
});
|
|
1072
1094
|
|
|
@@ -1076,23 +1098,29 @@
|
|
|
1076
1098
|
}
|
|
1077
1099
|
|
|
1078
1100
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
1084
|
-
* The default implementation updates `this.el`'s innerHTML with the result
|
|
1085
|
-
* of calling `this.template`, passing `this.model` as the argument.
|
|
1086
|
-
* <br><br> ⚠ **Security Notice:** The default implementation utilizes `innerHTML`, which may introduce Cross-Site Scripting (XSS) risks.
|
|
1087
|
-
* Ensure that any user-generated content is properly sanitized before inserting it into the DOM.
|
|
1088
|
-
* You can use the {@link #module_view_sanitize View.sanitize} static method to escape HTML entities in a string.
|
|
1089
|
-
* For best practices on secure data handling, refer to the
|
|
1090
|
-
* [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
|
|
1101
|
+
* `render` is the core function that your view should override, in order to populate its element (`this.el`), with the appropriate HTML. The convention is for `render` to always return `this`.
|
|
1102
|
+
* Views are low-level building blocks for creating user interfaces. For most use cases, we recommend using {@link #module_component Component} instead, which provides a more declarative template syntax, automatic DOM updates, and a more efficient render pipeline.
|
|
1103
|
+
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
1104
|
+
*
|
|
1091
1105
|
* @return {View} Returns `this` for chaining.
|
|
1106
|
+
* @example
|
|
1107
|
+
* class UserView extends View {
|
|
1108
|
+
* render() {
|
|
1109
|
+
* if (this.template) {
|
|
1110
|
+
* const model = this.model;
|
|
1111
|
+
* // Sanitize model attributes to prevent XSS attacks.
|
|
1112
|
+
* const safeData = {
|
|
1113
|
+
* name : View.sanitize(model.name),
|
|
1114
|
+
* email : View.sanitize(model.email),
|
|
1115
|
+
* bio : View.sanitize(model.bio)
|
|
1116
|
+
* };
|
|
1117
|
+
* this.el.innerHTML = this.template(safeData);
|
|
1118
|
+
* }
|
|
1119
|
+
* return this;
|
|
1120
|
+
* }
|
|
1121
|
+
* }
|
|
1092
1122
|
*/
|
|
1093
1123
|
render() {
|
|
1094
|
-
if (this.template) this.el.innerHTML = this.template(this.model);
|
|
1095
|
-
// Return `this` for chaining.
|
|
1096
1124
|
return this;
|
|
1097
1125
|
}
|
|
1098
1126
|
|
|
@@ -1702,9 +1730,24 @@
|
|
|
1702
1730
|
return html.join(' ');
|
|
1703
1731
|
}
|
|
1704
1732
|
|
|
1733
|
+
let isChrome, moveBeforeSupported, preserveFocus, resetFocus;
|
|
1734
|
+
|
|
1735
|
+
// Browser compatibility notes (as of 2025):
|
|
1736
|
+
// - Safari: Does not support moveBefore.
|
|
1737
|
+
// - Firefox: moveBefore preserves focus but loses scroll position.
|
|
1738
|
+
// - Chrome: moveBefore preserves scroll position but loses focus.
|
|
1739
|
+
if (typeof document !== 'undefined') {
|
|
1740
|
+
isChrome = !!navigator.userAgent.match(/Chrome/);
|
|
1741
|
+
moveBeforeSupported = !!Element.prototype.moveBefore;
|
|
1742
|
+
preserveFocus = !moveBeforeSupported || isChrome;
|
|
1743
|
+
// When using moveBefore, Chrome resets the focus but preserves the active element.
|
|
1744
|
+
// So we need to blur the active element before setting the focus again.
|
|
1745
|
+
resetFocus = moveBeforeSupported && isChrome;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1705
1748
|
/**
|
|
1706
1749
|
* Replaces an existing DOM node with a new node, preserving internal DOM state.
|
|
1707
|
-
* Uses moveBefore if available, otherwise falls back to
|
|
1750
|
+
* Uses moveBefore if available, otherwise falls back to insertBefore.
|
|
1708
1751
|
*
|
|
1709
1752
|
* @param {Node} oldNode The existing DOM node to replace.
|
|
1710
1753
|
* @param {Node} newNode The new DOM node to replace the old node with.
|
|
@@ -1712,16 +1755,18 @@
|
|
|
1712
1755
|
* @private
|
|
1713
1756
|
*/
|
|
1714
1757
|
function replaceNode(oldNode, newNode) {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1758
|
+
const activeElement = preserveFocus &&
|
|
1759
|
+
document.activeElement &&
|
|
1760
|
+
newNode.contains(document.activeElement) ?
|
|
1761
|
+
document.activeElement : null;
|
|
1762
|
+
|
|
1763
|
+
if (activeElement && resetFocus) activeElement.blur();
|
|
1764
|
+
|
|
1765
|
+
oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);
|
|
1766
|
+
oldNode.parentNode.removeChild(oldNode);
|
|
1767
|
+
|
|
1768
|
+
if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
|
|
1769
|
+
activeElement.focus();
|
|
1725
1770
|
}
|
|
1726
1771
|
}
|
|
1727
1772
|
|
|
@@ -1836,7 +1881,7 @@
|
|
|
1836
1881
|
// If error expression is multi-line, show full details.
|
|
1837
1882
|
if (typeof errorExpression === 'function') {
|
|
1838
1883
|
const fullSource = errorExpression.toString();
|
|
1839
|
-
if (fullSource.
|
|
1884
|
+
if (fullSource.match(/\n/)) {
|
|
1840
1885
|
formattedLines.push('');
|
|
1841
1886
|
formattedLines.push(' | Expression details:');
|
|
1842
1887
|
fullSource.split('\n').forEach(line => {
|
|
@@ -1859,19 +1904,35 @@
|
|
|
1859
1904
|
*/
|
|
1860
1905
|
const getExpressionResult = (expression, context, meta) => {
|
|
1861
1906
|
try {
|
|
1862
|
-
|
|
1907
|
+
if (typeof expression !== 'function') return expression;
|
|
1908
|
+
// In development, detect uninstantiated Component classes and provide a helpful error.
|
|
1909
|
+
// This typically happens when a component tag is malformed and not properly expanded.
|
|
1910
|
+
if (__DEV__ && expression.prototype instanceof Component) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
`Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` +
|
|
1913
|
+
'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' +
|
|
1914
|
+
'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.'
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
return expression.call(context, context);
|
|
1863
1919
|
} catch (error) {
|
|
1864
|
-
if (meta && !error.
|
|
1920
|
+
if (meta && !error._rasti) {
|
|
1865
1921
|
let message;
|
|
1866
1922
|
|
|
1867
1923
|
{
|
|
1868
1924
|
const formattedSource = formatTemplateSource(context.source, expression);
|
|
1869
|
-
message = createDevelopmentErrorMessage(
|
|
1925
|
+
message = createDevelopmentErrorMessage(
|
|
1926
|
+
`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`
|
|
1927
|
+
);
|
|
1870
1928
|
}
|
|
1929
|
+
|
|
1871
1930
|
const enhancedError = new Error(message, { cause : error });
|
|
1872
|
-
enhancedError.
|
|
1931
|
+
enhancedError._rasti = true;
|
|
1932
|
+
|
|
1873
1933
|
throw enhancedError;
|
|
1874
1934
|
}
|
|
1935
|
+
|
|
1875
1936
|
throw error;
|
|
1876
1937
|
}
|
|
1877
1938
|
};
|
|
@@ -1886,13 +1947,12 @@
|
|
|
1886
1947
|
const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT] && el.dataset[Component.DATASET_ELEMENT].endsWith('-1'));
|
|
1887
1948
|
|
|
1888
1949
|
/**
|
|
1889
|
-
* Check if an element contains a
|
|
1890
|
-
* It checks if the element is a component root element or if it contains a component.
|
|
1950
|
+
* Check if an element contains (or is) a dynamic element.
|
|
1891
1951
|
* @param {Element} el The element to check.
|
|
1892
|
-
* @return {boolean} True if the element contains a
|
|
1952
|
+
* @return {boolean} True if the element contains (or is) a dynamic element.
|
|
1893
1953
|
* @private
|
|
1894
1954
|
*/
|
|
1895
|
-
const containsElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) ||
|
|
1955
|
+
const containsElement = (el) => !!(el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`))));
|
|
1896
1956
|
|
|
1897
1957
|
/**
|
|
1898
1958
|
* Generate string with placeholders for interpolated expressions.
|
|
@@ -2035,8 +2095,8 @@
|
|
|
2035
2095
|
}
|
|
2036
2096
|
// Match component tags with backreference to ensure correct pairing.
|
|
2037
2097
|
return main.replace(
|
|
2038
|
-
new RegExp(`<(${PH})([^>]*)
|
|
2039
|
-
(match,
|
|
2098
|
+
new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'),
|
|
2099
|
+
(match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => {
|
|
2040
2100
|
let tag, attributesStr, innerList;
|
|
2041
2101
|
|
|
2042
2102
|
if (openTag) {
|
|
@@ -2066,7 +2126,7 @@
|
|
|
2066
2126
|
// Add `renderChildren` function to options.
|
|
2067
2127
|
if (innerList) {
|
|
2068
2128
|
// Evaluate items in parent context and create Partial.
|
|
2069
|
-
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this)));
|
|
2129
|
+
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children')));
|
|
2070
2130
|
}
|
|
2071
2131
|
// Mount component.
|
|
2072
2132
|
return tag.mount(options);
|
|
@@ -2281,7 +2341,7 @@
|
|
|
2281
2341
|
const PH = Component.PLACEHOLDER('(\\d+)');
|
|
2282
2342
|
const attributes = [];
|
|
2283
2343
|
// Parse attributes string with support for placeholders in both names and values.
|
|
2284
|
-
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s
|
|
2344
|
+
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g');
|
|
2285
2345
|
|
|
2286
2346
|
let attributeMatch;
|
|
2287
2347
|
while ((attributeMatch = regExp.exec(attributesStr)) !== null) {
|
|
@@ -2309,7 +2369,7 @@
|
|
|
2309
2369
|
/*
|
|
2310
2370
|
* These option keys will be extended on the component instance.
|
|
2311
2371
|
*/
|
|
2312
|
-
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate'];
|
|
2372
|
+
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate'];
|
|
2313
2373
|
|
|
2314
2374
|
/**
|
|
2315
2375
|
* @lends module:Component
|
|
@@ -2465,12 +2525,12 @@
|
|
|
2465
2525
|
element.hydrate(this.el);
|
|
2466
2526
|
}
|
|
2467
2527
|
});
|
|
2468
|
-
// Delegate events.
|
|
2469
|
-
this.delegateEvents();
|
|
2470
2528
|
// Get references for interpolation marker comments
|
|
2471
2529
|
this.template.interpolations.forEach(interpolation => interpolation.hydrate(this.el));
|
|
2472
2530
|
this.children.forEach(child => child.hydrate(this.el));
|
|
2473
2531
|
}
|
|
2532
|
+
// Delegate events.
|
|
2533
|
+
this.delegateEvents();
|
|
2474
2534
|
// Call `onHydrate` lifecycle method.
|
|
2475
2535
|
this.onHydrate.call(this);
|
|
2476
2536
|
// Return `this` for chaining.
|
|
@@ -2480,22 +2540,34 @@
|
|
|
2480
2540
|
/**
|
|
2481
2541
|
* Used internally on the render process.
|
|
2482
2542
|
* Reuse a `Component` by replacing the placeholder comment with the real nodes.
|
|
2483
|
-
*
|
|
2484
|
-
* @param parent {node} The parent node.
|
|
2485
|
-
* @param props {object} The props to set on the recycled component.
|
|
2543
|
+
* Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur.
|
|
2544
|
+
* @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved.
|
|
2486
2545
|
* @return {Component} The component instance.
|
|
2487
2546
|
* @private
|
|
2488
2547
|
*/
|
|
2489
|
-
recycle(parent
|
|
2548
|
+
recycle(parent) {
|
|
2549
|
+
// Call `onBeforeRecycle` lifecycle method.
|
|
2550
|
+
this.onBeforeRecycle.call(this);
|
|
2551
|
+
// No parent means the node is already in the correct position. So we don't need to replace it.
|
|
2490
2552
|
if (parent) {
|
|
2491
2553
|
// Locate the placeholder comment and replace it with the real nodes
|
|
2492
2554
|
const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
|
|
2493
2555
|
replaceNode(placeholder, this.el);
|
|
2494
2556
|
}
|
|
2495
|
-
//
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2557
|
+
// Return `this` for chaining.
|
|
2558
|
+
return this;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/**
|
|
2562
|
+
* Update the component's props.
|
|
2563
|
+
* Sets the props and calls the `onRecycle` lifecycle method.
|
|
2564
|
+
* @param props {object} The props to set on the component.
|
|
2565
|
+
* @return {Component} The component instance.
|
|
2566
|
+
* @private
|
|
2567
|
+
*/
|
|
2568
|
+
updateProps(props) {
|
|
2569
|
+
// Set the props.
|
|
2570
|
+
this.props.set(props);
|
|
2499
2571
|
// Call `onRecycle` lifecycle method.
|
|
2500
2572
|
this.onRecycle.call(this);
|
|
2501
2573
|
// Return `this` for chaining.
|
|
@@ -2526,7 +2598,7 @@
|
|
|
2526
2598
|
* import { Component } from 'rasti';
|
|
2527
2599
|
* // Create a Title component.
|
|
2528
2600
|
* const Title = Component.create`
|
|
2529
|
-
* <h1>${({ props }) => props.
|
|
2601
|
+
* <h1>${({ props }) => props.renderChildren()}</h1>
|
|
2530
2602
|
* `;
|
|
2531
2603
|
* // Create Main component.
|
|
2532
2604
|
* const Main = Component.create`
|
|
@@ -2621,7 +2693,7 @@
|
|
|
2621
2693
|
|
|
2622
2694
|
/**
|
|
2623
2695
|
* Render the component as a string.
|
|
2624
|
-
* Used internally on the render process.
|
|
2696
|
+
* Used internally on the render process.
|
|
2625
2697
|
* Use it for server-side rendering or static site generation.
|
|
2626
2698
|
* @return {string} The rendered component.
|
|
2627
2699
|
* @example
|
|
@@ -2638,10 +2710,10 @@
|
|
|
2638
2710
|
* const app = new App();
|
|
2639
2711
|
*
|
|
2640
2712
|
* console.log(app.toString());
|
|
2641
|
-
* // <div data-
|
|
2713
|
+
* // <div data-rst-el="r1-1"><!--rst-s-r1-1--><button class="button" data-rst-el="r2-1">Click me</button><!--rst-e-r1-1--></div>
|
|
2642
2714
|
*
|
|
2643
2715
|
* console.log(`${app}`);
|
|
2644
|
-
* // <div data-
|
|
2716
|
+
* // <div data-rst-el="r1-1"><!--rst-s-r1-1--><button class="button" data-rst-el="r2-1">Click me</button><!--rst-e-r1-1--></div>
|
|
2645
2717
|
*/
|
|
2646
2718
|
toString() {
|
|
2647
2719
|
// Normally there won't be any children, but if there are, destroy them.
|
|
@@ -2660,14 +2732,42 @@
|
|
|
2660
2732
|
}
|
|
2661
2733
|
|
|
2662
2734
|
/**
|
|
2663
|
-
* Render the `Component`.
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2735
|
+
* Render the `Component`.
|
|
2736
|
+
*
|
|
2737
|
+
* **First render (when `this.el` is not present):**
|
|
2738
|
+
* This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated,
|
|
2739
|
+
* making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM.
|
|
2740
|
+
* The `onHydrate` lifecycle method will be called.
|
|
2741
|
+
*
|
|
2742
|
+
* **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()`
|
|
2743
|
+
* handles this process automatically, creating the component instance, rendering it, and appending it to the DOM.
|
|
2744
|
+
*
|
|
2745
|
+
* **Update render (when `this.el` is present):**
|
|
2746
|
+
* This indicates the component is being updated. The method will:
|
|
2747
|
+
* - Update only the attributes of the root element and child elements
|
|
2748
|
+
* - Update only the content of interpolations (the dynamic parts of the template)
|
|
2749
|
+
* - For container components (components that render a single child component), update the single interpolation
|
|
2750
|
+
*
|
|
2751
|
+
* The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end.
|
|
2752
|
+
*
|
|
2753
|
+
* **Child component handling:**
|
|
2754
|
+
* When rendering child components, they can be either recreated or recycled:
|
|
2755
|
+
*
|
|
2756
|
+
* - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component
|
|
2757
|
+
* is found for recycling.
|
|
2758
|
+
*
|
|
2759
|
+
* - **Recycling:** The same component instance is reused. Recycling happens in two ways:
|
|
2760
|
+
* - Components with a `key` are recycled if a previous child with the same key exists
|
|
2761
|
+
* - Unkeyed components are recycled if they have the same type and position in the template or partial
|
|
2762
|
+
*
|
|
2763
|
+
* When a component is recycled:
|
|
2764
|
+
* - The `onBeforeRecycle` lifecycle method is called when recycling starts
|
|
2765
|
+
* - The component's `this.props` is updated with the new props from the parent
|
|
2766
|
+
* - The `onRecycle` lifecycle method is called after props are updated
|
|
2767
|
+
*
|
|
2768
|
+
* A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model
|
|
2769
|
+
* (or even the same model as the parent) and update independently in subsequent render cycles.
|
|
2770
|
+
*
|
|
2671
2771
|
* @return {Component} The component instance.
|
|
2672
2772
|
*/
|
|
2673
2773
|
render() {
|
|
@@ -2679,14 +2779,16 @@
|
|
|
2679
2779
|
this.hydrate(fragment);
|
|
2680
2780
|
return this;
|
|
2681
2781
|
}
|
|
2782
|
+
// Call `onBeforeUpdate` lifecycle method.
|
|
2783
|
+
this.onBeforeUpdate.call(this);
|
|
2682
2784
|
// Clear event listeners.
|
|
2683
2785
|
this.eventsManager.reset();
|
|
2684
|
-
// Update elements.
|
|
2685
|
-
this.template.elements.forEach(element => element.update());
|
|
2686
2786
|
// Store previous children.
|
|
2687
2787
|
const previousChildren = this.children;
|
|
2688
2788
|
// Clear current children.
|
|
2689
2789
|
this.children = [];
|
|
2790
|
+
// Store props to update.
|
|
2791
|
+
const propsQueue = [];
|
|
2690
2792
|
// Update interpolations.
|
|
2691
2793
|
this.template.interpolations.forEach(interpolation => {
|
|
2692
2794
|
// Reset the tracker.
|
|
@@ -2730,8 +2832,10 @@
|
|
|
2730
2832
|
const rendered = this.renderTemplatePart(interpolation.expression, addChild, tracker);
|
|
2731
2833
|
|
|
2732
2834
|
const recycle = ([recycled, discarded], fragment) => {
|
|
2733
|
-
//
|
|
2734
|
-
|
|
2835
|
+
// Store props to update.
|
|
2836
|
+
propsQueue.push([recycled, discarded.props.toJSON()]);
|
|
2837
|
+
// Add child and recycle (move to new position if needed).
|
|
2838
|
+
this.addChild(recycled).recycle(fragment);
|
|
2735
2839
|
// Destroy discarded component.
|
|
2736
2840
|
discarded.destroy();
|
|
2737
2841
|
};
|
|
@@ -2760,14 +2864,21 @@
|
|
|
2760
2864
|
previousChildren.forEach(prev => {
|
|
2761
2865
|
if (this.children.indexOf(prev) < 0) prev.destroy();
|
|
2762
2866
|
});
|
|
2763
|
-
//
|
|
2867
|
+
// Update recycled children props.
|
|
2868
|
+
propsQueue.forEach(([recycled, props]) => {
|
|
2869
|
+
recycled.updateProps(props);
|
|
2870
|
+
});
|
|
2871
|
+
// If this component is a container, set el to the child element.
|
|
2872
|
+
// Otherwise, update elements attributes and delegate events.
|
|
2764
2873
|
if (this.isContainer()) {
|
|
2765
2874
|
this.el = this.children[0].el;
|
|
2766
2875
|
} else {
|
|
2767
|
-
//
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2876
|
+
// Update elements attributes.
|
|
2877
|
+
this.template.elements.forEach(element => element.update());
|
|
2878
|
+
}
|
|
2879
|
+
// If there are pending event types, delegate events again.
|
|
2880
|
+
if (this.eventsManager.hasPendingTypes()) {
|
|
2881
|
+
this.delegateEvents();
|
|
2771
2882
|
}
|
|
2772
2883
|
// Call onUpdate lifecycle method.
|
|
2773
2884
|
this.onUpdate.call(this);
|
|
@@ -2805,6 +2916,19 @@
|
|
|
2805
2916
|
*/
|
|
2806
2917
|
onHydrate() {}
|
|
2807
2918
|
|
|
2919
|
+
/**
|
|
2920
|
+
* Lifecycle method. Called before the component is recycled and reused between renders.
|
|
2921
|
+
* This method is called at the beginning of the `recycle` method, before any recycling operations occur.
|
|
2922
|
+
*
|
|
2923
|
+
* A component is recycled when:
|
|
2924
|
+
* - It has a `key` and a previous child with the same key exists
|
|
2925
|
+
* - It doesn't have a `key` but has the same type and position in the template or partial
|
|
2926
|
+
*
|
|
2927
|
+
* Use this method to perform operations that need to happen before the component is recycled,
|
|
2928
|
+
* such as storing previous state or preparing for the recycling.
|
|
2929
|
+
*/
|
|
2930
|
+
onBeforeRecycle() {}
|
|
2931
|
+
|
|
2808
2932
|
/**
|
|
2809
2933
|
* Lifecycle method. Called when the component is recycled and reused between renders.
|
|
2810
2934
|
*
|
|
@@ -2817,6 +2941,14 @@
|
|
|
2817
2941
|
*/
|
|
2818
2942
|
onRecycle() {}
|
|
2819
2943
|
|
|
2944
|
+
/**
|
|
2945
|
+
* Lifecycle method. Called before the component is updated or re-rendered.
|
|
2946
|
+
* This method is called at the beginning of the `render` method when the component's state, model, or props change and trigger a re-render.
|
|
2947
|
+
* Use this method to perform operations that need to happen before the component is updated,
|
|
2948
|
+
* such as saving previous state or preparing for the update.
|
|
2949
|
+
*/
|
|
2950
|
+
onBeforeUpdate() {}
|
|
2951
|
+
|
|
2820
2952
|
/**
|
|
2821
2953
|
* Lifecycle method. Called when the component is updated or re-rendered.
|
|
2822
2954
|
* This method is called when the component's state, model, or props change and trigger a re-render.
|
|
@@ -2938,7 +3070,7 @@
|
|
|
2938
3070
|
* ```javascript
|
|
2939
3071
|
* const Button = Component.create`
|
|
2940
3072
|
* <button class="${({ props }) => props.className}">
|
|
2941
|
-
* ${({ props }) => props.
|
|
3073
|
+
* ${({ props }) => props.renderChildren()}
|
|
2942
3074
|
* </button>
|
|
2943
3075
|
* `;
|
|
2944
3076
|
* ```
|
|
@@ -2984,14 +3116,14 @@
|
|
|
2984
3116
|
* // Create a button component.
|
|
2985
3117
|
* const Button = Component.create`
|
|
2986
3118
|
* <button class="button">
|
|
2987
|
-
* ${({ props }) => props.
|
|
3119
|
+
* ${({ props }) => props.renderChildren()}
|
|
2988
3120
|
* </button>
|
|
2989
3121
|
* `;
|
|
2990
3122
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
2991
3123
|
* const Navigation = Component.create`
|
|
2992
3124
|
* <nav>
|
|
2993
3125
|
* ${({ props }) => props.items.map(
|
|
2994
|
-
* item => Button.mount({
|
|
3126
|
+
* item => Button.mount({ renderChildren : () => item.label })
|
|
2995
3127
|
* )}
|
|
2996
3128
|
* </nav>
|
|
2997
3129
|
* `;
|
|
@@ -3007,7 +3139,7 @@
|
|
|
3007
3139
|
* // Create a button component.
|
|
3008
3140
|
* const Button = Component.create`
|
|
3009
3141
|
* <button class="button">
|
|
3010
|
-
* ${({ props }) => props.
|
|
3142
|
+
* ${({ props }) => props.renderChildren()}
|
|
3011
3143
|
* </button>
|
|
3012
3144
|
* `;
|
|
3013
3145
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
@@ -3030,7 +3162,7 @@
|
|
|
3030
3162
|
* // Create a button component.
|
|
3031
3163
|
* const Button = Component.create`
|
|
3032
3164
|
* <button class="${({ props }) => props.className}">
|
|
3033
|
-
* ${({ props }) => props.
|
|
3165
|
+
* ${({ props }) => props.renderChildren()}
|
|
3034
3166
|
* </button>
|
|
3035
3167
|
* `;
|
|
3036
3168
|
* // Create a container that renders a Button component.
|
|
@@ -3040,7 +3172,7 @@
|
|
|
3040
3172
|
* // Create a container that renders a Button component, using a function.
|
|
3041
3173
|
* const ButtonCancel = Component.create(() => Button.mount({
|
|
3042
3174
|
* className : 'cancel',
|
|
3043
|
-
*
|
|
3175
|
+
* renderChildren : () => 'Cancel'
|
|
3044
3176
|
* }));
|
|
3045
3177
|
* ```
|
|
3046
3178
|
* @static
|
|
@@ -3130,7 +3262,7 @@
|
|
|
3130
3262
|
* 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.
|
|
3131
3263
|
* @module
|
|
3132
3264
|
* @extends View
|
|
3133
|
-
* @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onRecycle, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`.
|
|
3265
|
+
* @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onBeforeRecycle, onRecycle, onBeforeUpdate, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`.
|
|
3134
3266
|
* @property {string} [key] A unique key to identify the component. Components with keys are recycled when the same key is found in the previous render. Unkeyed components are recycled based on type and position.
|
|
3135
3267
|
* @property {Model} [model] A `Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method.
|
|
3136
3268
|
* @property {Model} [state] A `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.
|
|
@@ -3151,9 +3283,9 @@
|
|
|
3151
3283
|
* // Increment `model.seconds` every second.
|
|
3152
3284
|
* setInterval(() => model.seconds++, 1000);
|
|
3153
3285
|
*/
|
|
3154
|
-
var
|
|
3286
|
+
var Component_default = Component.create`<div></div>`;
|
|
3155
3287
|
|
|
3156
|
-
exports.Component =
|
|
3288
|
+
exports.Component = Component_default;
|
|
3157
3289
|
exports.Emitter = Emitter;
|
|
3158
3290
|
exports.Model = Model;
|
|
3159
3291
|
exports.View = View;
|