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 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-alpha.8/docs/logo-dark.svg">
4
- <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.8/docs/logo.svg" height="120">
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** is a lightweight MVC library for building fast, reactive user interfaces. Inspired by **Backbone.js**, it retains a familiar API while removing non-essential features and introducing modern, declarative, and composable components to simplify complex UI development.
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
  [![Travis (.com)](https://img.shields.io/travis/com/8tentaculos/rasti)](https://app.travis-ci.com/8tentaculos/rasti)
15
17
  [![npm version](https://img.shields.io/npm/v/rasti.svg)](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://https://codepen.io/8tentaculos/pen/XJXVQOR?editors=0010)
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 {Rasti.View} Return `this` for chaining.
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 {Rasti.View} child
882
- * @return {Rasti.View}
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 {Rasti.View} Return `this` for chaining.
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 {Rasti.View} Returns `this` for chaining.
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 {Rasti.View} Return `this` for chaining.
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
- * 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>
1091
- * @return {Rasti.View} Returns `this` for chaining.
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
  '\'' : '&#039;'
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 before.
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
- if (Element.prototype.moveBefore) {
1705
- oldNode.parentNode.moveBefore(newNode, oldNode);
1706
- oldNode.parentNode.removeChild(oldNode);
1707
- } else {
1708
- const activeElement = document.activeElement;
1709
- oldNode.parentNode.insertBefore(newNode, oldNode);
1710
- oldNode.parentNode.removeChild(oldNode);
1711
- if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
1712
- activeElement.focus();
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.includes('\n')) {
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
- return getResult(expression, context, context);
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.cause) {
1917
+ if (meta && !error._rasti) {
1854
1918
  let message;
1855
1919
 
1856
1920
  {
1857
1921
  const formattedSource = formatTemplateSource(context.source, expression);
1858
- message = createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`);
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.stack = error.stack;
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) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) || !!el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`);
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})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'),
2028
- (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) => {
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*/?[>"']))+.))?\\3)?`, 'g');
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
- * Call `onRecycle` lifecycle method.
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, 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.
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
- // Update props.
2485
- if (props) {
2486
- this.props.set(props);
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.children}</h1>
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
- * - 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.
2654
- * - 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.
2655
- * - When rendering child components, recycling happens in two ways:
2656
- * - Components with a `key` are recycled if a previous child with the same key exists.
2657
- * - Unkeyed components are recycled if they have the same type and position in the template or partial.
2658
- * A recycled `Component` will call the `onRecycle` lifecycle method.
2659
- * - 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
+ *
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
- // Add child, update props and recycle (move to new position if needed).
2723
- 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);
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
- // 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.
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 {Rasti.Model} The model that emitted the event.
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.children}
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.children}
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({ children : item.label })
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.children}
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.children}
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
- * children : 'Cancel'
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-rasti-el';
3095
- Component.ATTRIBUTE_EVENT = (type, uid) => `data-rasti-on-${type}-${uid}`;
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 = 'rastiEl';
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) => `__RASTI_PH_${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) => `rasti-r-${uid}`;
3111
- Component.MARKER_START = (uid) => `rasti-s-${uid}`;
3112
- Component.MARKER_END = (uid) => `rasti-e-${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 {Rasti.Model} [model] A `Rasti.Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method.
3125
- * @property {Rasti.Model} [state] A `Rasti.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.
3126
- * @property {Rasti.Model} [props] Automatically created from any options not merged to the component instance. Contains props passed from parent component as a `Rasti.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.
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.