rasti 4.0.0-alpha.9 → 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 +198 -66
- 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 +129 -39
- package/es/Component.js.map +1 -1
- package/es/View.js +28 -15
- 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 +129 -39
- package/lib/Component.cjs.map +1 -1
- package/lib/View.cjs +28 -15
- 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 +129 -39
- package/src/View.js +28 -15
- 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/es/Component.js
CHANGED
|
@@ -35,21 +35,37 @@ import './utils/padStart.js';
|
|
|
35
35
|
*/
|
|
36
36
|
const getExpressionResult = (expression, context, meta) => {
|
|
37
37
|
try {
|
|
38
|
-
|
|
38
|
+
if (typeof expression !== 'function') return expression;
|
|
39
|
+
// In development, detect uninstantiated Component classes and provide a helpful error.
|
|
40
|
+
// This typically happens when a component tag is malformed and not properly expanded.
|
|
41
|
+
if (__DEV__ && expression.prototype instanceof Component) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` +
|
|
44
|
+
'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' +
|
|
45
|
+
'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return expression.call(context, context);
|
|
39
50
|
} catch (error) {
|
|
40
|
-
if (meta && !error.
|
|
51
|
+
if (meta && !error._rasti) {
|
|
41
52
|
let message;
|
|
42
53
|
|
|
43
54
|
if (__DEV__) {
|
|
44
55
|
const formattedSource = formatTemplateSource(context.source, expression);
|
|
45
|
-
message = createDevelopmentErrorMessage(
|
|
56
|
+
message = createDevelopmentErrorMessage(
|
|
57
|
+
`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`
|
|
58
|
+
);
|
|
46
59
|
} else {
|
|
47
60
|
message = createProductionErrorMessage(`Error in ${context.constructor.name}#${context.uid} expression`);
|
|
48
61
|
}
|
|
62
|
+
|
|
49
63
|
const enhancedError = new Error(message, { cause : error });
|
|
50
|
-
enhancedError.
|
|
64
|
+
enhancedError._rasti = true;
|
|
65
|
+
|
|
51
66
|
throw enhancedError;
|
|
52
67
|
}
|
|
68
|
+
|
|
53
69
|
throw error;
|
|
54
70
|
}
|
|
55
71
|
};
|
|
@@ -70,7 +86,9 @@ const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_
|
|
|
70
86
|
* @return {boolean} True if the element contains a component.
|
|
71
87
|
* @private
|
|
72
88
|
*/
|
|
73
|
-
const containsElement = (el) => !!(
|
|
89
|
+
const containsElement = (el) => !!(
|
|
90
|
+
el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`)))
|
|
91
|
+
);
|
|
74
92
|
|
|
75
93
|
/**
|
|
76
94
|
* Generate string with placeholders for interpolated expressions.
|
|
@@ -213,8 +231,8 @@ const expandComponents = (main, expressions, skipNormalization = false) => {
|
|
|
213
231
|
}
|
|
214
232
|
// Match component tags with backreference to ensure correct pairing.
|
|
215
233
|
return main.replace(
|
|
216
|
-
new RegExp(`<(${PH})([^>]*)
|
|
217
|
-
(match,
|
|
234
|
+
new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'),
|
|
235
|
+
(match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => {
|
|
218
236
|
let tag, attributesStr, innerList;
|
|
219
237
|
|
|
220
238
|
if (openTag) {
|
|
@@ -244,7 +262,7 @@ const expandComponents = (main, expressions, skipNormalization = false) => {
|
|
|
244
262
|
// Add `renderChildren` function to options.
|
|
245
263
|
if (innerList) {
|
|
246
264
|
// Evaluate items in parent context and create Partial.
|
|
247
|
-
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this)));
|
|
265
|
+
options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children')));
|
|
248
266
|
}
|
|
249
267
|
// Mount component.
|
|
250
268
|
return tag.mount(options);
|
|
@@ -461,7 +479,7 @@ const parseAttributes = (attributesStr, expressions) => {
|
|
|
461
479
|
const PH = Component.PLACEHOLDER('(\\d+)');
|
|
462
480
|
const attributes = [];
|
|
463
481
|
// Parse attributes string with support for placeholders in both names and values.
|
|
464
|
-
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s
|
|
482
|
+
const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g');
|
|
465
483
|
|
|
466
484
|
let attributeMatch;
|
|
467
485
|
while ((attributeMatch = regExp.exec(attributesStr)) !== null) {
|
|
@@ -489,7 +507,7 @@ const parseAttributes = (attributesStr, expressions) => {
|
|
|
489
507
|
/*
|
|
490
508
|
* These option keys will be extended on the component instance.
|
|
491
509
|
*/
|
|
492
|
-
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate'];
|
|
510
|
+
const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate'];
|
|
493
511
|
|
|
494
512
|
/**
|
|
495
513
|
* @lends module:Component
|
|
@@ -662,22 +680,34 @@ class Component extends View {
|
|
|
662
680
|
/**
|
|
663
681
|
* Used internally on the render process.
|
|
664
682
|
* Reuse a `Component` by replacing the placeholder comment with the real nodes.
|
|
665
|
-
*
|
|
666
|
-
* @param parent {node} The parent node.
|
|
667
|
-
* @param props {object} The props to set on the recycled component.
|
|
683
|
+
* Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur.
|
|
684
|
+
* @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved.
|
|
668
685
|
* @return {Component} The component instance.
|
|
669
686
|
* @private
|
|
670
687
|
*/
|
|
671
|
-
recycle(parent
|
|
688
|
+
recycle(parent) {
|
|
689
|
+
// Call `onBeforeRecycle` lifecycle method.
|
|
690
|
+
this.onBeforeRecycle.call(this);
|
|
691
|
+
// No parent means the node is already in the correct position. So we don't need to replace it.
|
|
672
692
|
if (parent) {
|
|
673
693
|
// Locate the placeholder comment and replace it with the real nodes
|
|
674
694
|
const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
|
|
675
695
|
replaceNode(placeholder, this.el);
|
|
676
696
|
}
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
697
|
+
// Return `this` for chaining.
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Update the component's props.
|
|
703
|
+
* Sets the props and calls the `onRecycle` lifecycle method.
|
|
704
|
+
* @param props {object} The props to set on the component.
|
|
705
|
+
* @return {Component} The component instance.
|
|
706
|
+
* @private
|
|
707
|
+
*/
|
|
708
|
+
updateProps(props) {
|
|
709
|
+
// Set the props.
|
|
710
|
+
this.props.set(props);
|
|
681
711
|
// Call `onRecycle` lifecycle method.
|
|
682
712
|
this.onRecycle.call(this);
|
|
683
713
|
// Return `this` for chaining.
|
|
@@ -708,7 +738,7 @@ class Component extends View {
|
|
|
708
738
|
* import { Component } from 'rasti';
|
|
709
739
|
* // Create a Title component.
|
|
710
740
|
* const Title = Component.create`
|
|
711
|
-
* <h1>${({ props }) => props.
|
|
741
|
+
* <h1>${({ props }) => props.renderChildren()}</h1>
|
|
712
742
|
* `;
|
|
713
743
|
* // Create Main component.
|
|
714
744
|
* const Main = Component.create`
|
|
@@ -842,14 +872,42 @@ class Component extends View {
|
|
|
842
872
|
}
|
|
843
873
|
|
|
844
874
|
/**
|
|
845
|
-
* Render the `Component`.
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
875
|
+
* Render the `Component`.
|
|
876
|
+
*
|
|
877
|
+
* **First render (when `this.el` is not present):**
|
|
878
|
+
* This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated,
|
|
879
|
+
* making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM.
|
|
880
|
+
* The `onHydrate` lifecycle method will be called.
|
|
881
|
+
*
|
|
882
|
+
* **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()`
|
|
883
|
+
* handles this process automatically, creating the component instance, rendering it, and appending it to the DOM.
|
|
884
|
+
*
|
|
885
|
+
* **Update render (when `this.el` is present):**
|
|
886
|
+
* This indicates the component is being updated. The method will:
|
|
887
|
+
* - Update only the attributes of the root element and child elements
|
|
888
|
+
* - Update only the content of interpolations (the dynamic parts of the template)
|
|
889
|
+
* - For container components (components that render a single child component), update the single interpolation
|
|
890
|
+
*
|
|
891
|
+
* The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end.
|
|
892
|
+
*
|
|
893
|
+
* **Child component handling:**
|
|
894
|
+
* When rendering child components, they can be either recreated or recycled:
|
|
895
|
+
*
|
|
896
|
+
* - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component
|
|
897
|
+
* is found for recycling.
|
|
898
|
+
*
|
|
899
|
+
* - **Recycling:** The same component instance is reused. Recycling happens in two ways:
|
|
900
|
+
* - Components with a `key` are recycled if a previous child with the same key exists
|
|
901
|
+
* - Unkeyed components are recycled if they have the same type and position in the template or partial
|
|
902
|
+
*
|
|
903
|
+
* When a component is recycled:
|
|
904
|
+
* - The `onBeforeRecycle` lifecycle method is called when recycling starts
|
|
905
|
+
* - The component's `this.props` is updated with the new props from the parent
|
|
906
|
+
* - The `onRecycle` lifecycle method is called after props are updated
|
|
907
|
+
*
|
|
908
|
+
* A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model
|
|
909
|
+
* (or even the same model as the parent) and update independently in subsequent render cycles.
|
|
910
|
+
*
|
|
853
911
|
* @return {Component} The component instance.
|
|
854
912
|
*/
|
|
855
913
|
render() {
|
|
@@ -861,14 +919,16 @@ class Component extends View {
|
|
|
861
919
|
this.hydrate(fragment);
|
|
862
920
|
return this;
|
|
863
921
|
}
|
|
922
|
+
// Call `onBeforeUpdate` lifecycle method.
|
|
923
|
+
this.onBeforeUpdate.call(this);
|
|
864
924
|
// Clear event listeners.
|
|
865
925
|
this.eventsManager.reset();
|
|
866
|
-
// Update elements.
|
|
867
|
-
this.template.elements.forEach(element => element.update());
|
|
868
926
|
// Store previous children.
|
|
869
927
|
const previousChildren = this.children;
|
|
870
928
|
// Clear current children.
|
|
871
929
|
this.children = [];
|
|
930
|
+
// Store props to update.
|
|
931
|
+
const propsQueue = [];
|
|
872
932
|
// Update interpolations.
|
|
873
933
|
this.template.interpolations.forEach(interpolation => {
|
|
874
934
|
// Reset the tracker.
|
|
@@ -912,8 +972,10 @@ class Component extends View {
|
|
|
912
972
|
const rendered = this.renderTemplatePart(interpolation.expression, addChild, tracker);
|
|
913
973
|
|
|
914
974
|
const recycle = ([recycled, discarded], fragment) => {
|
|
915
|
-
//
|
|
916
|
-
|
|
975
|
+
// Store props to update.
|
|
976
|
+
propsQueue.push([recycled, discarded.props.toJSON()]);
|
|
977
|
+
// Add child and recycle (move to new position if needed).
|
|
978
|
+
this.addChild(recycled).recycle(fragment);
|
|
917
979
|
// Destroy discarded component.
|
|
918
980
|
discarded.destroy();
|
|
919
981
|
};
|
|
@@ -942,10 +1004,17 @@ class Component extends View {
|
|
|
942
1004
|
previousChildren.forEach(prev => {
|
|
943
1005
|
if (this.children.indexOf(prev) < 0) prev.destroy();
|
|
944
1006
|
});
|
|
945
|
-
//
|
|
1007
|
+
// Update recycled children props.
|
|
1008
|
+
propsQueue.forEach(([recycled, props]) => {
|
|
1009
|
+
recycled.updateProps(props);
|
|
1010
|
+
});
|
|
1011
|
+
// If this component is a container, set el to the child element.
|
|
1012
|
+
// Otherwise, update elements attributes and delegate events.
|
|
946
1013
|
if (this.isContainer()) {
|
|
947
1014
|
this.el = this.children[0].el;
|
|
948
1015
|
} else {
|
|
1016
|
+
// Update elements attributes.
|
|
1017
|
+
this.template.elements.forEach(element => element.update());
|
|
949
1018
|
// If there are pending event types, delegate events again.
|
|
950
1019
|
if (this.eventsManager.hasPendingTypes()) {
|
|
951
1020
|
this.delegateEvents();
|
|
@@ -987,6 +1056,19 @@ class Component extends View {
|
|
|
987
1056
|
*/
|
|
988
1057
|
onHydrate() {}
|
|
989
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* Lifecycle method. Called before the component is recycled and reused between renders.
|
|
1061
|
+
* This method is called at the beginning of the `recycle` method, before any recycling operations occur.
|
|
1062
|
+
*
|
|
1063
|
+
* A component is recycled when:
|
|
1064
|
+
* - It has a `key` and a previous child with the same key exists
|
|
1065
|
+
* - It doesn't have a `key` but has the same type and position in the template or partial
|
|
1066
|
+
*
|
|
1067
|
+
* Use this method to perform operations that need to happen before the component is recycled,
|
|
1068
|
+
* such as storing previous state or preparing for the recycling.
|
|
1069
|
+
*/
|
|
1070
|
+
onBeforeRecycle() {}
|
|
1071
|
+
|
|
990
1072
|
/**
|
|
991
1073
|
* Lifecycle method. Called when the component is recycled and reused between renders.
|
|
992
1074
|
*
|
|
@@ -999,6 +1081,14 @@ class Component extends View {
|
|
|
999
1081
|
*/
|
|
1000
1082
|
onRecycle() {}
|
|
1001
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Lifecycle method. Called before the component is updated or re-rendered.
|
|
1086
|
+
* 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.
|
|
1087
|
+
* Use this method to perform operations that need to happen before the component is updated,
|
|
1088
|
+
* such as saving previous state or preparing for the update.
|
|
1089
|
+
*/
|
|
1090
|
+
onBeforeUpdate() {}
|
|
1091
|
+
|
|
1002
1092
|
/**
|
|
1003
1093
|
* Lifecycle method. Called when the component is updated or re-rendered.
|
|
1004
1094
|
* This method is called when the component's state, model, or props change and trigger a re-render.
|
|
@@ -1120,7 +1210,7 @@ class Component extends View {
|
|
|
1120
1210
|
* ```javascript
|
|
1121
1211
|
* const Button = Component.create`
|
|
1122
1212
|
* <button class="${({ props }) => props.className}">
|
|
1123
|
-
* ${({ props }) => props.
|
|
1213
|
+
* ${({ props }) => props.renderChildren()}
|
|
1124
1214
|
* </button>
|
|
1125
1215
|
* `;
|
|
1126
1216
|
* ```
|
|
@@ -1166,14 +1256,14 @@ class Component extends View {
|
|
|
1166
1256
|
* // Create a button component.
|
|
1167
1257
|
* const Button = Component.create`
|
|
1168
1258
|
* <button class="button">
|
|
1169
|
-
* ${({ props }) => props.
|
|
1259
|
+
* ${({ props }) => props.renderChildren()}
|
|
1170
1260
|
* </button>
|
|
1171
1261
|
* `;
|
|
1172
1262
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
1173
1263
|
* const Navigation = Component.create`
|
|
1174
1264
|
* <nav>
|
|
1175
1265
|
* ${({ props }) => props.items.map(
|
|
1176
|
-
* item => Button.mount({
|
|
1266
|
+
* item => Button.mount({ renderChildren : () => item.label })
|
|
1177
1267
|
* )}
|
|
1178
1268
|
* </nav>
|
|
1179
1269
|
* `;
|
|
@@ -1189,7 +1279,7 @@ class Component extends View {
|
|
|
1189
1279
|
* // Create a button component.
|
|
1190
1280
|
* const Button = Component.create`
|
|
1191
1281
|
* <button class="button">
|
|
1192
|
-
* ${({ props }) => props.
|
|
1282
|
+
* ${({ props }) => props.renderChildren()}
|
|
1193
1283
|
* </button>
|
|
1194
1284
|
* `;
|
|
1195
1285
|
* // Create a navigation component. Add buttons as children. Iterate over items.
|
|
@@ -1212,7 +1302,7 @@ class Component extends View {
|
|
|
1212
1302
|
* // Create a button component.
|
|
1213
1303
|
* const Button = Component.create`
|
|
1214
1304
|
* <button class="${({ props }) => props.className}">
|
|
1215
|
-
* ${({ props }) => props.
|
|
1305
|
+
* ${({ props }) => props.renderChildren()}
|
|
1216
1306
|
* </button>
|
|
1217
1307
|
* `;
|
|
1218
1308
|
* // Create a container that renders a Button component.
|
|
@@ -1222,7 +1312,7 @@ class Component extends View {
|
|
|
1222
1312
|
* // Create a container that renders a Button component, using a function.
|
|
1223
1313
|
* const ButtonCancel = Component.create(() => Button.mount({
|
|
1224
1314
|
* className : 'cancel',
|
|
1225
|
-
*
|
|
1315
|
+
* renderChildren : () => 'Cancel'
|
|
1226
1316
|
* }));
|
|
1227
1317
|
* ```
|
|
1228
1318
|
* @static
|
|
@@ -1312,7 +1402,7 @@ Component.MARKER_END = (uid) => `rst-e-${uid}`;
|
|
|
1312
1402
|
* 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.
|
|
1313
1403
|
* @module
|
|
1314
1404
|
* @extends View
|
|
1315
|
-
* @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`.
|
|
1405
|
+
* @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`.
|
|
1316
1406
|
* @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.
|
|
1317
1407
|
* @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.
|
|
1318
1408
|
* @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.
|