rasti 4.0.0-alpha.8 → 4.0.0
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 +11 -5
- package/dist/rasti.js +228 -85
- 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 +141 -51
- package/es/Component.js.map +1 -1
- package/es/View.js +46 -22
- 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 +141 -51
- package/lib/Component.cjs.map +1 -1
- package/lib/View.cjs +46 -22
- 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 +3 -4
- package/src/Component.js +141 -51
- package/src/View.js +46 -22
- 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/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0
|
|
4
|
-
<img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0/docs/logo-dark.svg">
|
|
4
|
+
<img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0/docs/logo.svg" height="120">
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
<b>Modern MVC for building user interfaces</b>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
**Rasti
|
|
12
|
+
**Rasti is a lightweight MVC library for building fast, reactive user interfaces.**
|
|
13
|
+
It provides declarative, composable **components** for building state-driven UIs.
|
|
14
|
+
Its low-level MVC core, inspired by **Backbone.js**’s architecture, provides **models**, **views** and **event emitters** as the fundamental building blocks.
|
|
13
15
|
|
|
14
16
|
[](https://app.travis-ci.com/8tentaculos/rasti)
|
|
15
17
|
[](https://www.npmjs.com/package/rasti)
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
- **Lightweight and Fast** ⚡
|
|
31
33
|
Minimal overhead with efficient rendering.
|
|
32
34
|
- **Legacy Compatibility** 🕰️
|
|
33
|
-
Seamlessly integrates into existing **Backbone.js** projects.
|
|
35
|
+
Seamlessly integrates into existing **Backbone.js** legacy projects.
|
|
34
36
|
- **Standards-Based** 📐
|
|
35
37
|
Built on modern web standards, no tooling required.
|
|
36
38
|
|
|
@@ -166,7 +168,7 @@ Counter.mount({ model }, document.body);
|
|
|
166
168
|
// When buttons are clicked, only the text node gets updated, not the entire component.
|
|
167
169
|
```
|
|
168
170
|
|
|
169
|
-
[Try it on CodePen](https://
|
|
171
|
+
[Try it on CodePen](https://codepen.io/8tentaculos/pen/XJXVQOR?editors=0010)
|
|
170
172
|
|
|
171
173
|
## Why Choose **Rasti**?
|
|
172
174
|
|
|
@@ -187,6 +189,10 @@ You can find a sample **TODO application** in the [example folder](https://githu
|
|
|
187
189
|
|
|
188
190
|
For detailed information on how to use **Rasti**, refer to the [API documentation](/docs/api.md).
|
|
189
191
|
|
|
192
|
+
## Working with LLMs
|
|
193
|
+
|
|
194
|
+
For those working with LLMs, there is an [AI Agents reference guide](/docs/AGENTS.md) that provides API patterns, lifecycle methods, and best practices, optimized for LLM context. You can share this guide with AI assistants to help them understand **Rasti**'s architecture and component APIs.
|
|
195
|
+
|
|
190
196
|
## Version History
|
|
191
197
|
|
|
192
198
|
We strive to minimize breaking changes between major versions. However, if you're migrating between major versions, please refer to the release notes below for details on any breaking changes and migration tips.
|
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.
|
|
@@ -845,7 +864,7 @@
|
|
|
845
864
|
* Destroy the view.
|
|
846
865
|
* Destroy children views if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.
|
|
847
866
|
* @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
|
|
848
|
-
* @return {
|
|
867
|
+
* @return {View} Return `this` for chaining.
|
|
849
868
|
*/
|
|
850
869
|
destroy() {
|
|
851
870
|
// Call destroy on children.
|
|
@@ -878,8 +897,8 @@
|
|
|
878
897
|
* Add a view as a child.
|
|
879
898
|
* Children views are stored at `this.children`, and destroyed when the parent is destroyed.
|
|
880
899
|
* Returns the child for chaining.
|
|
881
|
-
* @param {
|
|
882
|
-
* @return {
|
|
900
|
+
* @param {View} child
|
|
901
|
+
* @return {View}
|
|
883
902
|
*/
|
|
884
903
|
addChild(child) {
|
|
885
904
|
this.children.push(child);
|
|
@@ -944,7 +963,7 @@
|
|
|
944
963
|
|
|
945
964
|
/**
|
|
946
965
|
* Remove `this.el` from the DOM.
|
|
947
|
-
* @return {
|
|
966
|
+
* @return {View} Return `this` for chaining.
|
|
948
967
|
*/
|
|
949
968
|
removeElement() {
|
|
950
969
|
this.el.parentNode.removeChild(this.el);
|
|
@@ -976,7 +995,7 @@
|
|
|
976
995
|
* invoked **once for each matched element** (from inner to outer).
|
|
977
996
|
*
|
|
978
997
|
* @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
|
|
979
|
-
* @return {
|
|
998
|
+
* @return {View} Returns `this` for chaining.
|
|
980
999
|
* @example
|
|
981
1000
|
* // Using prototype (recommended for static events)
|
|
982
1001
|
* class Modal extends View {
|
|
@@ -1063,7 +1082,7 @@
|
|
|
1063
1082
|
* Removes all of the view's delegated events.
|
|
1064
1083
|
* Useful if you want to disable or remove a view from the DOM temporarily.
|
|
1065
1084
|
* Called automatically when the view is destroyed and when `delegateEvents` is called again.
|
|
1066
|
-
* @return {
|
|
1085
|
+
* @return {View} Return `this` for chaining.
|
|
1067
1086
|
*/
|
|
1068
1087
|
undelegateEvents() {
|
|
1069
1088
|
this.delegatedEventListeners.forEach(({ type, listener }) => {
|
|
@@ -1076,23 +1095,29 @@
|
|
|
1076
1095
|
}
|
|
1077
1096
|
|
|
1078
1097
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1098
|
+
* `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`.
|
|
1099
|
+
* 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.
|
|
1100
|
+
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
1101
|
+
*
|
|
1102
|
+
* @return {View} Returns `this` for chaining.
|
|
1103
|
+
* @example
|
|
1104
|
+
* class UserView extends View {
|
|
1105
|
+
* render() {
|
|
1106
|
+
* if (this.template) {
|
|
1107
|
+
* const model = this.model;
|
|
1108
|
+
* // Sanitize model attributes to prevent XSS attacks.
|
|
1109
|
+
* const safeData = {
|
|
1110
|
+
* name : View.sanitize(model.name),
|
|
1111
|
+
* email : View.sanitize(model.email),
|
|
1112
|
+
* bio : View.sanitize(model.bio)
|
|
1113
|
+
* };
|
|
1114
|
+
* this.el.innerHTML = this.template(safeData);
|
|
1115
|
+
* }
|
|
1116
|
+
* return this;
|
|
1117
|
+
* }
|
|
1118
|
+
* }
|
|
1092
1119
|
*/
|
|
1093
1120
|
render() {
|
|
1094
|
-
if (this.template) this.el.innerHTML = this.template(this.model);
|
|
1095
|
-
// Return `this` for chaining.
|
|
1096
1121
|
return this;
|
|
1097
1122
|
}
|
|
1098
1123
|
|
|
@@ -1114,6 +1139,17 @@
|
|
|
1114
1139
|
'\'' : '''
|
|
1115
1140
|
}[match]));
|
|
1116
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Reset the unique ID counter to 0.
|
|
1145
|
+
* This is useful for server-side rendering scenarios where you want to ensure that
|
|
1146
|
+
* the generated unique IDs match those on the client, enabling seamless hydration of components.
|
|
1147
|
+
* This method is inherited by {@link #module_component Component}.
|
|
1148
|
+
* @static
|
|
1149
|
+
*/
|
|
1150
|
+
static resetUid() {
|
|
1151
|
+
View.uid = 0;
|
|
1152
|
+
}
|
|
1117
1153
|
}
|
|
1118
1154
|
|
|
1119
1155
|
/**
|
|
@@ -1691,9 +1727,24 @@
|
|
|
1691
1727
|
return html.join(' ');
|
|
1692
1728
|
}
|
|
1693
1729
|
|
|
1730
|
+
let isChrome, moveBeforeSupported, preserveFocus, resetFocus;
|
|
1731
|
+
|
|
1732
|
+
// Browser compatibility notes (as of 2025):
|
|
1733
|
+
// - Safari: Does not support moveBefore.
|
|
1734
|
+
// - Firefox: moveBefore preserves focus but loses scroll position.
|
|
1735
|
+
// - Chrome: moveBefore preserves scroll position but loses focus.
|
|
1736
|
+
if (typeof document !== 'undefined') {
|
|
1737
|
+
isChrome = !!navigator.userAgent.match(/Chrome/);
|
|
1738
|
+
moveBeforeSupported = !!Element.prototype.moveBefore;
|
|
1739
|
+
preserveFocus = !moveBeforeSupported || isChrome;
|
|
1740
|
+
// When using moveBefore, Chrome resets the focus but preserves the active element.
|
|
1741
|
+
// So we need to blur the active element before setting the focus again.
|
|
1742
|
+
resetFocus = moveBeforeSupported && isChrome;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1694
1745
|
/**
|
|
1695
1746
|
* Replaces an existing DOM node with a new node, preserving internal DOM state.
|
|
1696
|
-
* Uses moveBefore if available, otherwise falls back to
|
|
1747
|
+
* Uses moveBefore if available, otherwise falls back to insertBefore.
|
|
1697
1748
|
*
|
|
1698
1749
|
* @param {Node} oldNode The existing DOM node to replace.
|
|
1699
1750
|
* @param {Node} newNode The new DOM node to replace the old node with.
|
|
@@ -1701,16 +1752,18 @@
|
|
|
1701
1752
|
* @private
|
|
1702
1753
|
*/
|
|
1703
1754
|
function replaceNode(oldNode, newNode) {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1755
|
+
const activeElement = preserveFocus &&
|
|
1756
|
+
document.activeElement &&
|
|
1757
|
+
newNode.contains(document.activeElement) ?
|
|
1758
|
+
document.activeElement : null;
|
|
1759
|
+
|
|
1760
|
+
if (activeElement && resetFocus) activeElement.blur();
|
|
1761
|
+
|
|
1762
|
+
oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);
|
|
1763
|
+
oldNode.parentNode.removeChild(oldNode);
|
|
1764
|
+
|
|
1765
|
+
if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
|
|
1766
|
+
activeElement.focus();
|
|
1714
1767
|
}
|
|
1715
1768
|
}
|
|
1716
1769
|
|
|
@@ -1825,7 +1878,7 @@
|
|
|
1825
1878
|
// If error expression is multi-line, show full details.
|
|
1826
1879
|
if (typeof errorExpression === 'function') {
|
|
1827
1880
|
const fullSource = errorExpression.toString();
|
|
1828
|
-
if (fullSource.
|
|
1881
|
+
if (fullSource.match(/\n/)) {
|
|
1829
1882
|
formattedLines.push('');
|
|
1830
1883
|
formattedLines.push(' | Expression details:');
|
|
1831
1884
|
fullSource.split('\n').forEach(line => {
|
|
@@ -1848,19 +1901,35 @@
|
|
|
1848
1901
|
*/
|
|
1849
1902
|
const getExpressionResult = (expression, context, meta) => {
|
|
1850
1903
|
try {
|
|
1851
|
-
|
|
1904
|
+
if (typeof expression !== 'function') return expression;
|
|
1905
|
+
// In development, detect uninstantiated Component classes and provide a helpful error.
|
|
1906
|
+
// This typically happens when a component tag is malformed and not properly expanded.
|
|
1907
|
+
if (__DEV__ && expression.prototype instanceof Component) {
|
|
1908
|
+
throw new Error(
|
|
1909
|
+
`Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` +
|
|
1910
|
+
'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' +
|
|
1911
|
+
'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.'
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
return expression.call(context, context);
|
|
1852
1916
|
} catch (error) {
|
|
1853
|
-
if (meta && !error.
|
|
1917
|
+
if (meta && !error._rasti) {
|
|
1854
1918
|
let message;
|
|
1855
1919
|
|
|
1856
1920
|
{
|
|
1857
1921
|
const formattedSource = formatTemplateSource(context.source, expression);
|
|
1858
|
-
message = createDevelopmentErrorMessage(
|
|
1922
|
+
message = createDevelopmentErrorMessage(
|
|
1923
|
+
`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`
|
|
1924
|
+
);
|
|
1859
1925
|
}
|
|
1926
|
+
|
|
1860
1927
|
const enhancedError = new Error(message, { cause : error });
|
|
1861
|
-
enhancedError.
|
|
1928
|
+
enhancedError._rasti = true;
|
|
1929
|
+
|
|
1862
1930
|
throw enhancedError;
|
|
1863
1931
|
}
|
|
1932
|
+
|
|
1864
1933
|
throw error;
|
|
1865
1934
|
}
|
|
1866
1935
|
};
|
|
@@ -1881,7 +1950,9 @@
|
|
|
1881
1950
|
* @return {boolean} True if the element contains a component.
|
|
1882
1951
|
* @private
|
|
1883
1952
|
*/
|
|
1884
|
-
const containsElement = (el) => !!(
|
|
1953
|
+
const containsElement = (el) => !!(
|
|
1954
|
+
el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`)))
|
|
1955
|
+
);
|
|
1885
1956
|
|
|
1886
1957
|
/**
|
|
1887
1958
|
* Generate string with placeholders for interpolated expressions.
|
|
@@ -2024,8 +2095,8 @@
|
|
|
2024
2095
|
}
|
|
2025
2096
|
// Match component tags with backreference to ensure correct pairing.
|
|
2026
2097
|
return main.replace(
|
|
2027
|
-
new RegExp(`<(${PH})([^>]*)
|
|
2028
|
-
(match,
|
|
2098
|
+
new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'),
|
|
2099
|
+
(match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => {
|
|
2029
2100
|
let tag, attributesStr, innerList;
|
|
2030
2101
|
|
|
2031
2102
|
if (openTag) {
|
|
@@ -2055,7 +2126,7 @@
|
|
|
2055
2126
|
// Add `renderChildren` function to options.
|
|
2056
2127
|
if (innerList) {
|
|
2057
2128
|
// Evaluate items in parent context and create Partial.
|
|
2058
|
-
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this)));
|
|
2129
|
+
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children')));
|
|
2059
2130
|
}
|
|
2060
2131
|
// Mount component.
|
|
2061
2132
|
return tag.mount(options);
|
|
@@ -2270,7 +2341,7 @@
|
|
|
2270
2341
|
const PH = Component.PLACEHOLDER('(\\d+)');
|
|
2271
2342
|
const attributes = [];
|
|
2272
2343
|
// Parse attributes string with support for placeholders in both names and values.
|
|
2273
|
-
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s
|
|
2344
|
+
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g');
|
|
2274
2345
|
|
|
2275
2346
|
let attributeMatch;
|
|
2276
2347
|
while ((attributeMatch = regExp.exec(attributesStr)) !== null) {
|
|
@@ -2298,7 +2369,7 @@
|
|
|
2298
2369
|
/*
|
|
2299
2370
|
* These option keys will be extended on the component instance.
|
|
2300
2371
|
*/
|
|
2301
|
-
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate'];
|
|
2372
|
+
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate'];
|
|
2302
2373
|
|
|
2303
2374
|
/**
|
|
2304
2375
|
* @lends module:Component
|
|
@@ -2469,22 +2540,34 @@
|
|
|
2469
2540
|
/**
|
|
2470
2541
|
* Used internally on the render process.
|
|
2471
2542
|
* Reuse a `Component` by replacing the placeholder comment with the real nodes.
|
|
2472
|
-
*
|
|
2473
|
-
* @param parent {node} The parent node.
|
|
2474
|
-
* @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.
|
|
2475
2545
|
* @return {Component} The component instance.
|
|
2476
2546
|
* @private
|
|
2477
2547
|
*/
|
|
2478
|
-
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.
|
|
2479
2552
|
if (parent) {
|
|
2480
2553
|
// Locate the placeholder comment and replace it with the real nodes
|
|
2481
2554
|
const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
|
|
2482
2555
|
replaceNode(placeholder, this.el);
|
|
2483
2556
|
}
|
|
2484
|
-
//
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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);
|
|
2488
2571
|
// Call `onRecycle` lifecycle method.
|
|
2489
2572
|
this.onRecycle.call(this);
|
|
2490
2573
|
// Return `this` for chaining.
|
|
@@ -2515,7 +2598,7 @@
|
|
|
2515
2598
|
* import { Component } from 'rasti';
|
|
2516
2599
|
* // Create a Title component.
|
|
2517
2600
|
* const Title = Component.create`
|
|
2518
|
-
* <h1>${({ props }) => props.
|
|
2601
|
+
* <h1>${({ props }) => props.renderChildren()}</h1>
|
|
2519
2602
|
* `;
|
|
2520
2603
|
* // Create Main component.
|
|
2521
2604
|
* const Main = Component.create`
|
|
@@ -2649,14 +2732,42 @@
|
|
|
2649
2732
|
}
|
|
2650
2733
|
|
|
2651
2734
|
/**
|
|
2652
|
-
* Render the `Component`.
|
|
2653
|
-
*
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
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
|
+
*
|
|
2660
2771
|
* @return {Component} The component instance.
|
|
2661
2772
|
*/
|
|
2662
2773
|
render() {
|
|
@@ -2668,14 +2779,16 @@
|
|
|
2668
2779
|
this.hydrate(fragment);
|
|
2669
2780
|
return this;
|
|
2670
2781
|
}
|
|
2782
|
+
// Call `onBeforeUpdate` lifecycle method.
|
|
2783
|
+
this.onBeforeUpdate.call(this);
|
|
2671
2784
|
// Clear event listeners.
|
|
2672
2785
|
this.eventsManager.reset();
|
|
2673
|
-
// Update elements.
|
|
2674
|
-
this.template.elements.forEach(element => element.update());
|
|
2675
2786
|
// Store previous children.
|
|
2676
2787
|
const previousChildren = this.children;
|
|
2677
2788
|
// Clear current children.
|
|
2678
2789
|
this.children = [];
|
|
2790
|
+
// Store props to update.
|
|
2791
|
+
const propsQueue = [];
|
|
2679
2792
|
// Update interpolations.
|
|
2680
2793
|
this.template.interpolations.forEach(interpolation => {
|
|
2681
2794
|
// Reset the tracker.
|
|
@@ -2719,8 +2832,10 @@
|
|
|
2719
2832
|
const rendered = this.renderTemplatePart(interpolation.expression, addChild, tracker);
|
|
2720
2833
|
|
|
2721
2834
|
const recycle = ([recycled, discarded], fragment) => {
|
|
2722
|
-
//
|
|
2723
|
-
|
|
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);
|
|
2724
2839
|
// Destroy discarded component.
|
|
2725
2840
|
discarded.destroy();
|
|
2726
2841
|
};
|
|
@@ -2749,10 +2864,17 @@
|
|
|
2749
2864
|
previousChildren.forEach(prev => {
|
|
2750
2865
|
if (this.children.indexOf(prev) < 0) prev.destroy();
|
|
2751
2866
|
});
|
|
2752
|
-
//
|
|
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.
|
|
2753
2873
|
if (this.isContainer()) {
|
|
2754
2874
|
this.el = this.children[0].el;
|
|
2755
2875
|
} else {
|
|
2876
|
+
// Update elements attributes.
|
|
2877
|
+
this.template.elements.forEach(element => element.update());
|
|
2756
2878
|
// If there are pending event types, delegate events again.
|
|
2757
2879
|
if (this.eventsManager.hasPendingTypes()) {
|
|
2758
2880
|
this.delegateEvents();
|
|
@@ -2779,7 +2901,7 @@
|
|
|
2779
2901
|
* This method can be extended with custom logic.
|
|
2780
2902
|
* Maybe comparing new attributes with previous ones and calling
|
|
2781
2903
|
* render when needed.
|
|
2782
|
-
* @param model {
|
|
2904
|
+
* @param model {Model} The model that emitted the event.
|
|
2783
2905
|
* @param changed {object} Object containing keys and values that has changed.
|
|
2784
2906
|
* @param [...args] {any} Any extra arguments passed to set method.
|
|
2785
2907
|
*/
|
|
@@ -2794,6 +2916,19 @@
|
|
|
2794
2916
|
*/
|
|
2795
2917
|
onHydrate() {}
|
|
2796
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
|
+
|
|
2797
2932
|
/**
|
|
2798
2933
|
* Lifecycle method. Called when the component is recycled and reused between renders.
|
|
2799
2934
|
*
|
|
@@ -2806,6 +2941,14 @@
|
|
|
2806
2941
|
*/
|
|
2807
2942
|
onRecycle() {}
|
|
2808
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
|
+
|
|
2809
2952
|
/**
|
|
2810
2953
|
* Lifecycle method. Called when the component is updated or re-rendered.
|
|
2811
2954
|
* This method is called when the component's state, model, or props change and trigger a re-render.
|
|
@@ -2927,7 +3070,7 @@
|
|
|
2927
3070
|
* ```javascript
|
|
2928
3071
|
* const Button = Component.create`
|
|
2929
3072
|
* <button class="${({ props }) => props.className}">
|
|
2930
|
-
* ${({ props }) => props.
|
|
3073
|
+
* ${({ props }) => props.renderChildren()}
|
|
2931
3074
|
* </button>
|
|
2932
3075
|
* `;
|
|
2933
3076
|
* ```
|
|
@@ -2973,14 +3116,14 @@
|
|
|
2973
3116
|
* // Create a button component.
|
|
2974
3117
|
* const Button = Component.create`
|
|
2975
3118
|
* <button class="button">
|
|
2976
|
-
* ${({ props }) => props.
|
|
3119
|
+
* ${({ props }) => props.renderChildren()}
|
|
2977
3120
|
* </button>
|
|
2978
3121
|
* `;
|
|
2979
3122
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
2980
3123
|
* const Navigation = Component.create`
|
|
2981
3124
|
* <nav>
|
|
2982
3125
|
* ${({ props }) => props.items.map(
|
|
2983
|
-
* item => Button.mount({
|
|
3126
|
+
* item => Button.mount({ renderChildren : () => item.label })
|
|
2984
3127
|
* )}
|
|
2985
3128
|
* </nav>
|
|
2986
3129
|
* `;
|
|
@@ -2996,7 +3139,7 @@
|
|
|
2996
3139
|
* // Create a button component.
|
|
2997
3140
|
* const Button = Component.create`
|
|
2998
3141
|
* <button class="button">
|
|
2999
|
-
* ${({ props }) => props.
|
|
3142
|
+
* ${({ props }) => props.renderChildren()}
|
|
3000
3143
|
* </button>
|
|
3001
3144
|
* `;
|
|
3002
3145
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
@@ -3019,7 +3162,7 @@
|
|
|
3019
3162
|
* // Create a button component.
|
|
3020
3163
|
* const Button = Component.create`
|
|
3021
3164
|
* <button class="${({ props }) => props.className}">
|
|
3022
|
-
* ${({ props }) => props.
|
|
3165
|
+
* ${({ props }) => props.renderChildren()}
|
|
3023
3166
|
* </button>
|
|
3024
3167
|
* `;
|
|
3025
3168
|
* // Create a container that renders a Button component.
|
|
@@ -3029,7 +3172,7 @@
|
|
|
3029
3172
|
* // Create a container that renders a Button component, using a function.
|
|
3030
3173
|
* const ButtonCancel = Component.create(() => Button.mount({
|
|
3031
3174
|
* className : 'cancel',
|
|
3032
|
-
*
|
|
3175
|
+
* renderChildren : () => 'Cancel'
|
|
3033
3176
|
* }));
|
|
3034
3177
|
* ```
|
|
3035
3178
|
* @static
|
|
@@ -3091,25 +3234,25 @@
|
|
|
3091
3234
|
/*
|
|
3092
3235
|
* Attributes used to identify elements and events.
|
|
3093
3236
|
*/
|
|
3094
|
-
Component.ATTRIBUTE_ELEMENT = 'data-
|
|
3095
|
-
Component.ATTRIBUTE_EVENT = (type, uid) => `data-
|
|
3237
|
+
Component.ATTRIBUTE_ELEMENT = 'data-rst-el';
|
|
3238
|
+
Component.ATTRIBUTE_EVENT = (type, uid) => `data-rst-on-${type}-${uid}`;
|
|
3096
3239
|
|
|
3097
3240
|
/*
|
|
3098
3241
|
* Dataset attribute used to identify elements.
|
|
3099
3242
|
*/
|
|
3100
|
-
Component.DATASET_ELEMENT = '
|
|
3243
|
+
Component.DATASET_ELEMENT = 'rstEl';
|
|
3101
3244
|
|
|
3102
3245
|
/*
|
|
3103
3246
|
* Placeholders used to temporarily replace expressions in the template.
|
|
3104
3247
|
*/
|
|
3105
|
-
Component.PLACEHOLDER = (idx) => `
|
|
3248
|
+
Component.PLACEHOLDER = (idx) => `__RASTI_PLACEHOLDER_${idx}__`;
|
|
3106
3249
|
|
|
3107
3250
|
/*
|
|
3108
3251
|
* Markers used to identify interpolation and recycled components.
|
|
3109
3252
|
*/
|
|
3110
|
-
Component.MARKER_RECYCLED = (uid) => `
|
|
3111
|
-
Component.MARKER_START = (uid) => `
|
|
3112
|
-
Component.MARKER_END = (uid) => `
|
|
3253
|
+
Component.MARKER_RECYCLED = (uid) => `rst-r-${uid}`;
|
|
3254
|
+
Component.MARKER_START = (uid) => `rst-s-${uid}`;
|
|
3255
|
+
Component.MARKER_END = (uid) => `rst-e-${uid}`;
|
|
3113
3256
|
|
|
3114
3257
|
/**
|
|
3115
3258
|
* Components are a special kind of `View` that is designed to be easily composable,
|
|
@@ -3119,11 +3262,11 @@
|
|
|
3119
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.
|
|
3120
3263
|
* @module
|
|
3121
3264
|
* @extends View
|
|
3122
|
-
* @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`.
|
|
3123
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.
|
|
3124
|
-
* @property {
|
|
3125
|
-
* @property {
|
|
3126
|
-
* @property {
|
|
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.
|
|
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.
|
|
3269
|
+
* @property {Model} [props] Automatically created from any options not merged to the component instance. Contains props passed from parent component as a `Model`. The component will listen to `change` events on props and call `onChange` lifecycle method. When a component with a `key` is recycled during parent re-render, new props are automatically updated and any changes trigger a re-render.
|
|
3127
3270
|
* @see {@link #module_component_create Component.create}
|
|
3128
3271
|
* @example
|
|
3129
3272
|
* import { Component, Model } from 'rasti';
|
|
@@ -3134,7 +3277,7 @@
|
|
|
3134
3277
|
* </div>
|
|
3135
3278
|
* `;
|
|
3136
3279
|
* // Create model to store seconds.
|
|
3137
|
-
* const model = new Model({ seconds: 0 });
|
|
3280
|
+
* const model = new Model({ seconds : 0 });
|
|
3138
3281
|
* // Mount timer on body.
|
|
3139
3282
|
* Timer.mount({ model }, document.body);
|
|
3140
3283
|
* // Increment `model.seconds` every second.
|