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/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
- return getResult(expression, context, context);
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.cause) {
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(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`);
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.stack = error.stack;
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) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) || !!el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`);
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})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'),
217
- (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => {
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*/?[>"']))+.))?\\3)?`, 'g');
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
- * Call `onRecycle` lifecycle method.
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, props) {
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
- // Update props.
678
- if (props) {
679
- this.props.set(props);
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.children}</h1>
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
- * - If `this.el` is not present, the `Component` will be rendered as a string inside a `DocumentFragment` and hydrated, making `this.el` available. The `onHydrate` lifecycle method will be called.
847
- * - If `this.el` is present, the method will update the attributes and inner HTML of the element, or recreate its child component in the case of a container. The `onUpdate` lifecycle method will be called.
848
- * - When rendering child components, recycling happens in two ways:
849
- * - Components with a `key` are recycled if a previous child with the same key exists.
850
- * - Unkeyed components are recycled if they have the same type and position in the template or partial.
851
- * A recycled `Component` will call the `onRecycle` lifecycle method.
852
- * - If the active element is inside the component, it will retain focus after the render.
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
- // Add child, update props and recycle (move to new position if needed).
916
- this.addChild(recycled).recycle(fragment, discarded.props.toJSON());
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
- // If container, set el to the child element.
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.children}
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.children}
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({ children : item.label })
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.children}
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.children}
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
- * children : 'Cancel'
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.