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/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
- let eventTypes = {};
1034
+ const eventTypes = {};
1016
1035
 
1017
1036
  Object.keys(events).forEach(key => {
1018
- const keyParts = key.split(' ');
1019
- const type = keyParts.shift();
1020
- const selector = keyParts.join(' ');
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({ selector, listener });
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
- // Iterate and run every individual listener if the selector matches.
1037
- eventTypes[type].forEach(({ selector, listener }) => {
1038
- // No selector provided: invoke listener once with root element.
1039
- if (!selector) {
1040
- listener.call(this, event, this, this.el);
1041
- return;
1042
- }
1043
-
1044
- let node = event.target;
1045
- // Traverse ancestors until reaching the view root (`this.el`).
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({ type, listener : typeListener });
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(({ type, listener }) => {
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
- * Renders the view.
1080
- * This method should be overridden with custom logic.
1081
- * The only convention is to manipulate the DOM within the scope of `this.el`,
1082
- * and to return `this` for chaining.
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> &#9888; **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 before.
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
- if (Element.prototype.moveBefore) {
1716
- oldNode.parentNode.moveBefore(newNode, oldNode);
1717
- oldNode.parentNode.removeChild(oldNode);
1718
- } else {
1719
- const activeElement = document.activeElement;
1720
- oldNode.parentNode.insertBefore(newNode, oldNode);
1721
- oldNode.parentNode.removeChild(oldNode);
1722
- if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
1723
- activeElement.focus();
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.includes('\n')) {
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
- return getResult(expression, context, context);
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.cause) {
1920
+ if (meta && !error._rasti) {
1865
1921
  let message;
1866
1922
 
1867
1923
  {
1868
1924
  const formattedSource = formatTemplateSource(context.source, expression);
1869
- message = createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`);
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.stack = error.stack;
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 component.
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 component.
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]) || !!el.querySelector(`[${Component.ATTRIBUTE_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})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'),
2039
- (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => {
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*/?[>"']))+.))?\\3)?`, 'g');
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
- * Call `onRecycle` lifecycle method.
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, props) {
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
- // Update props.
2496
- if (props) {
2497
- this.props.set(props);
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.children}</h1>
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-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
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-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
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
- * - 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.
2665
- * - 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.
2666
- * - When rendering child components, recycling happens in two ways:
2667
- * - Components with a `key` are recycled if a previous child with the same key exists.
2668
- * - Unkeyed components are recycled if they have the same type and position in the template or partial.
2669
- * A recycled `Component` will call the `onRecycle` lifecycle method.
2670
- * - If the active element is inside the component, it will retain focus after the render.
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
- // Add child, update props and recycle (move to new position if needed).
2734
- this.addChild(recycled).recycle(fragment, discarded.props.toJSON());
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
- // If container, set el to the child element.
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
- // If there are pending event types, delegate events again.
2768
- if (this.eventsManager.hasPendingTypes()) {
2769
- this.delegateEvents();
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.children}
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.children}
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({ children : item.label })
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.children}
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.children}
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
- * children : 'Cancel'
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 Component$1 = Component.create`<div></div>`;
3286
+ var Component_default = Component.create`<div></div>`;
3155
3287
 
3156
- exports.Component = Component$1;
3288
+ exports.Component = Component_default;
3157
3289
  exports.Emitter = Emitter;
3158
3290
  exports.Model = Model;
3159
3291
  exports.View = View;