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/lib/View.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"View.cjs","sources":["../src/View.js"],"sourcesContent":["import Emitter from './Emitter.js';\nimport getResult from './utils/getResult.js';\nimport validateListener from './utils/validateListener.js';\n\n/*\n * These option keys will be extended on the view instance.\n */\nconst viewOptions = ['el', 'tag', 'attributes', 'events', 'model', 'template', 'onDestroy'];\n\n/**\n * - Listens for changes and renders the UI.\n * - Handles user input and interactivity.\n * - Sends captured input to the model.\n *\n * A `View` is an atomic unit of the user interface that can render data from a specific model or multiple models.\n * However, views can also be independent and have no associated data. \n * Models must be unaware of views. Views, on the other hand, may render model data and listen to the change events \n * emitted by the models to re-render themselves based on changes. \n * Each `View` has a root element, `this.el`, which is used for event delegation. \n * All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it. \n * If `this.el` is not present, an element will be created using `this.tag` (defaulting to `div`) and `this.attributes`.\n * @module\n * @extends Emitter\n * @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.\n * @property {node|Function} el Every view has a root DOM element stored at `this.el`. If not present, it will be created. If `this.el` is a function, it will be called to get the element at `this.ensureElement`, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {string|Function} tag If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. Default is `div`. If it is a function, it will be called to get the tag, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {object|Function} attributes If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. If it is a function, it will be called to get the attributes object, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {object|Function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.\n * @property {object} model A model or any object containing data and business logic.\n * @property {Function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}. \n * @property {number} uid Unique identifier for the view instance. This can be used to generate unique IDs for elements within the view. It is automatically generated and should not be set manually.\n * @example\n * import { View, Model } from 'rasti';\n * \n * class Timer extends View {\n * constructor(options) {\n * super(options);\n * // Create model to store internal state. Set `seconds` attribute to 0.\n * this.model = new Model({ seconds : 0 });\n * // Listen to changes in model `seconds` attribute and re-render.\n * this.model.on('change:seconds', this.render.bind(this));\n * // Increment model `seconds` attribute every 1000 milliseconds.\n * this.interval = setInterval(() => this.model.seconds++, 1000);\n * }\n *\n * template(model) {\n * return `Seconds: <span>${model.seconds}</span>`;\n * }\n * }\n * // Render view and append view's element into the body.\n * document.body.appendChild(new Timer().render().el);\n */\nexport default class View extends Emitter {\n constructor(options = {}) {\n super();\n // Call preinitialize.\n this.preinitialize.apply(this, arguments);\n // Store delegated event listeners,\n // so they can be unbound later.\n this.delegatedEventListeners = [];\n // Store child views,\n // so they can be destroyed.\n this.children = [];\n // Mutable array to store handlers to be called on destroy.\n this.destroyQueue = [];\n this.viewOptions = [];\n // Extend \"this\" with options.\n viewOptions.forEach(key => {\n if (key in options) {\n this[key] = options[key];\n this.viewOptions.push(key);\n }\n });\n // Ensure that the view has a unique id at `this.uid`.\n this.ensureUid();\n // Ensure that the view has a root element at `this.el`.\n this.ensureElement();\n }\n\n /**\n * If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.\n * @param {object} options The view options.\n */\n preinitialize() {}\n\n /**\n * Returns the first element that matches the selector, \n * scoped to DOM elements within the current view's root element (`this.el`).\n * @param {string} selector CSS selector.\n * @return {node} Element matching selector within the view's root element (`this.el`).\n */\n $(selector) {\n return this.el.querySelector(selector);\n }\n\n /**\n * Returns a list of elements that match the selector, \n * scoped to DOM elements within the current view's root element (`this.el`).\n * @param {string} selector CSS selector.\n * @return {node[]} List of elements matching selector within the view's root element (`this.el`).\n */\n $$(selector) {\n return this.el.querySelectorAll(selector);\n }\n\n /**\n * Destroy the view.\n * Destroy children views if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.\n * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.\n * @return {View} Return `this` for chaining.\n */\n destroy() {\n // Call destroy on children.\n this.destroyChildren();\n // Undelegate `this.el` event listeners\n this.undelegateEvents();\n // Stop listening to events.\n this.stopListening();\n // Unbind `this` events.\n this.off();\n // Call destroy queue.\n this.destroyQueue.forEach(fn => fn());\n this.destroyQueue = [];\n // Call onDestroy lifecycle method\n this.onDestroy.apply(this, arguments);\n // Set destroyed flag.\n this.destroyed = true;\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * `onDestroy` lifecycle method is called after the view is destroyed.\n * Override with your code. Useful to stop listening to model's events.\n * @param {object} options Options object or any arguments passed to `destroy` method.\n */\n onDestroy() {}\n\n /**\n * Add a view as a child.\n * Children views are stored at `this.children`, and destroyed when the parent is destroyed.\n * Returns the child for chaining.\n * @param {View} child\n * @return {View}\n */\n addChild(child) {\n this.children.push(child);\n return child;\n }\n\n /**\n * Call destroy method on children views.\n */\n destroyChildren() {\n this.children.forEach(child => child.destroy());\n this.children = [];\n }\n\n /**\n * Ensure that the view has a unique id at `this.uid`.\n */\n ensureUid() {\n if (!this.uid) this.uid = `r${++View.uid}`;\n }\n\n /**\n * Ensure that the view has a root element at `this.el`.\n * You shouldn't call this method directly. It's called from the constructor.\n * You may override it if you want to use a different logic or to \n * postpone element creation.\n */\n ensureElement() {\n // Element is already present.\n if (this.el) {\n // If \"this.el\" is a function, call it to get the element.\n this.el = getResult(this.el, this);\n } else {\n // If \"this.el\" is not present,\n // create a new element according \"this.tag\"\n // and \"this.attributes\".\n const tag = getResult(this.tag, this);\n const attrs = getResult(this.attributes, this);\n this.el = this.createElement(tag, attrs);\n }\n // Delegate events on element.\n this.delegateEvents();\n }\n\n /**\n * Create an element.\n * Called from the constructor if `this.el` is undefined, to ensure\n * the view has a root element.\n * @param {string} tag Tag for the element. Default to `div`\n * @param {object} attributes Attributes for the element.\n * @return {node} The created element.\n */\n createElement(tag = 'div', attributes = {}) {\n // Create DOM element.\n let el = document.createElement(tag);\n // Add element attributes.\n Object.keys(attributes)\n .forEach(key => el.setAttribute(key, attributes[key]));\n\n return el;\n }\n\n /**\n * Remove `this.el` from the DOM.\n * @return {View} Return `this` for chaining.\n */\n removeElement() {\n this.el.parentNode.removeChild(this.el);\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Provide declarative listeners for DOM events within a view. If an events object is not provided, \n * it defaults to using `this.events`. If `this.events` is a function, it will be called to get the events object.\n * \n * The events object should follow the format `{'event selector': 'listener'}`:\n * - `event`: The type of event (e.g., 'click').\n * - `selector`: A CSS selector to match the event target. If omitted, the event is bound to the root element.\n * - `listener`: A function or a string representing a method name on the view. The method will be called with `this` bound to the view instance.\n * \n * By default, `delegateEvents` is called within the View's constructor. If you have a simple events object, \n * all of your DOM events will be connected automatically, and you will not need to call this function manually.\n * \n * All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.\n * When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.\n * \n * **Listener signature:** `(event, view, matched)`\n * - `event`: The native DOM event object.\n * - `view`: The current view instance (`this`).\n * - `matched`: The element that satisfies the selector. If no selector is provided, it will be the view's root element (`this.el`).\n *\n * If more than one ancestor between `event.target` and the view's root element matches the selector, the listener will be\n * invoked **once for each matched element** (from inner to outer).\n *\n * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.\n * @return {View} Returns `this` for chaining.\n * @example\n * // Using prototype (recommended for static events)\n * class Modal extends View {\n * onClickOk(event, view, matched) {\n * // matched === the button.ok element that was clicked\n * this.close();\n * }\n * \n * onClickCancel() {\n * this.destroy();\n * }\n * }\n * Modal.prototype.events = {\n * 'click button.ok': 'onClickOk',\n * 'click button.cancel': 'onClickCancel',\n * 'submit form': 'onSubmit'\n * };\n * \n * // Using a function for dynamic events\n * class DynamicView extends View {\n * events() {\n * return {\n * [`click .${this.model.buttonClass}`]: 'onButtonClick',\n * 'click': 'onRootClick'\n * };\n * }\n * }\n */\n delegateEvents(events) {\n if (!events) events = getResult(this.events, this);\n if (!events) return this;\n\n if (this.delegatedEventListeners.length) this.undelegateEvents();\n\n // Store events by type i.e.: \"click\", \"submit\", etc.\n let eventTypes = {};\n\n Object.keys(events).forEach(key => {\n const keyParts = key.split(' ');\n const type = keyParts.shift();\n const selector = keyParts.join(' ');\n\n let listener = events[key];\n // Listener may be a string representing a method name on the view, or a function.\n if (typeof listener === 'string') listener = this[listener];\n // Validate listener is a function.\n validateListener(listener);\n\n if (!eventTypes[type]) eventTypes[type] = [];\n\n eventTypes[type].push({ selector, listener });\n });\n\n Object.keys(eventTypes).forEach(type => {\n // Listener for the type of event.\n const typeListener = (event) => {\n // Iterate and run every individual listener if the selector matches.\n eventTypes[type].forEach(({ selector, listener }) => {\n // No selector provided: invoke listener once with root element.\n if (!selector) {\n listener.call(this, event, this, this.el);\n return;\n }\n\n let node = event.target;\n // Traverse ancestors until reaching the view root (`this.el`).\n while (node && node !== this.el) {\n if (node.matches && node.matches(selector)) {\n listener.call(this, event, this, node);\n }\n node = node.parentElement;\n }\n });\n };\n\n this.delegatedEventListeners.push({ type, listener : typeListener });\n this.el.addEventListener(type, typeListener);\n });\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Removes all of the view's delegated events. \n * Useful if you want to disable or remove a view from the DOM temporarily. \n * Called automatically when the view is destroyed and when `delegateEvents` is called again.\n * @return {View} Return `this` for chaining.\n */\n undelegateEvents() {\n this.delegatedEventListeners.forEach(({ type, listener }) => {\n this.el.removeEventListener(type, listener);\n });\n\n this.delegatedEventListeners = [];\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Renders the view. \n * This method should be overridden with custom logic.\n * The only convention is to manipulate the DOM within the scope of `this.el`,\n * and to return `this` for chaining. \n * If you add any child views, you should call `this.destroyChildren` before re-rendering. \n * The default implementation updates `this.el`'s innerHTML with the result\n * of calling `this.template`, passing `this.model` as the argument.\n * <br><br> &#9888; **Security Notice:** The default implementation utilizes `innerHTML`, which may introduce Cross-Site Scripting (XSS) risks. \n * Ensure that any user-generated content is properly sanitized before inserting it into the DOM. \n * You can use the {@link #module_view_sanitize View.sanitize} static method to escape HTML entities in a string. \n * For best practices on secure data handling, refer to the \n * [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>\n * @return {View} Returns `this` for chaining.\n */\n render() {\n if (this.template) this.el.innerHTML = this.template(this.model);\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Escape HTML entities in a string.\n * Use this method to sanitize user-generated content before inserting it into the DOM.\n * Override this method to provide a custom escape function.\n * This method is inherited by {@link #module_component Component} and used to escape template interpolations.\n * @static\n * @param {string} value String to escape.\n * @return {string} Escaped string.\n */\n static sanitize(value) {\n return `${value}`.replace(/[&<>\"']/g, match => ({\n '&' : '&amp;',\n '<' : '&lt;',\n '>' : '&gt;',\n '\"' : '&quot;',\n '\\'' : '&#039;'\n }[match]));\n }\n\n /**\n * Reset the unique ID counter to 0.\n * This is useful for server-side rendering scenarios where you want to ensure that\n * the generated unique IDs match those on the client, enabling seamless hydration of components.\n * This method is inherited by {@link #module_component Component}.\n * @static\n */\n static resetUid() {\n View.uid = 0;\n }\n}\n\n/**\n * Counter for generating unique IDs for view instances. \n * This is primarily used to assign unique identifiers to each view instance (`this.uid`), which can be helpful for tasks like \n * generating element IDs. \n * {@link #module_component Component}s use `this.uid` to generate data attributes for their elements, to be looked up on hydration. \n * For server-side rendering, this counter should be reset to `0` on every request to ensure that the generated \n * unique IDs match those on the client, enabling seamless hydration of components. \n * @static\n * @type {number}\n * @default 0\n */\nView.uid = 0;\n"],"names":["getResult","validateListener"],"mappings":"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"}
1
+ {"version":3,"file":"View.cjs","sources":["../src/View.js"],"sourcesContent":["import Emitter from './Emitter.js';\nimport getResult from './utils/getResult.js';\nimport validateListener from './utils/validateListener.js';\n\n/*\n * These option keys will be extended on the view instance.\n */\nconst viewOptions = ['el', 'tag', 'attributes', 'events', 'model', 'template', 'onDestroy'];\n\n/**\n * - Listens for changes and renders the UI.\n * - Handles user input and interactivity.\n * - Sends captured input to the model.\n *\n * A `View` is an atomic unit of the user interface that can render data from a specific model or multiple models.\n * However, views can also be independent and have no associated data. \n * Models must be unaware of views. Views, on the other hand, may render model data and listen to the change events \n * emitted by the models to re-render themselves based on changes. \n * Each `View` has a root element, `this.el`, which is used for event delegation. \n * All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it. \n * If `this.el` is not present, an element will be created using `this.tag` (defaulting to `div`) and `this.attributes`.\n * @module\n * @extends Emitter\n * @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.\n * @property {node|Function} el Every view has a root DOM element stored at `this.el`. If not present, it will be created. If `this.el` is a function, it will be called to get the element at `this.ensureElement`, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {string|Function} tag If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. Default is `div`. If it is a function, it will be called to get the tag, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {object|Function} attributes If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. If it is a function, it will be called to get the attributes object, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.\n * @property {object|Function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.\n * @property {object} model A model or any object containing data and business logic.\n * @property {Function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}. \n * @property {number} uid Unique identifier for the view instance. This can be used to generate unique IDs for elements within the view. It is automatically generated and should not be set manually.\n * @example\n * import { View, Model } from 'rasti';\n * \n * class Timer extends View {\n * constructor(options) {\n * super(options);\n * // Create model to store internal state. Set `seconds` attribute to 0.\n * this.model = new Model({ seconds : 0 });\n * // Listen to changes in model `seconds` attribute and re-render.\n * this.model.on('change:seconds', this.render.bind(this));\n * // Increment model `seconds` attribute every 1000 milliseconds.\n * this.interval = setInterval(() => this.model.seconds++, 1000);\n * }\n *\n * template(model) {\n * return `Seconds: <span>${View.sanitize(model.seconds)}</span>`;\n * }\n *\n * render() {\n * if (this.template) {\n * this.el.innerHTML = this.template(this.model);\n * }\n * return this;\n * }\n * }\n * // Render view and append view's element into the body.\n * document.body.appendChild(new Timer().render().el);\n */\nexport default class View extends Emitter {\n constructor(options = {}) {\n super();\n // Call preinitialize.\n this.preinitialize.apply(this, arguments);\n // Store delegated event listeners,\n // so they can be unbound later.\n this.delegatedEventListeners = [];\n // Store child views,\n // so they can be destroyed.\n this.children = [];\n // Mutable array to store handlers to be called on destroy.\n this.destroyQueue = [];\n this.viewOptions = [];\n // Extend \"this\" with options.\n viewOptions.forEach(key => {\n if (key in options) {\n this[key] = options[key];\n this.viewOptions.push(key);\n }\n });\n // Ensure that the view has a unique id at `this.uid`.\n this.ensureUid();\n // Ensure that the view has a root element at `this.el`.\n this.ensureElement();\n }\n\n /**\n * If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.\n * @param {object} options The view options.\n */\n preinitialize() {}\n\n /**\n * Returns the first element that matches the selector, \n * scoped to DOM elements within the current view's root element (`this.el`).\n * @param {string} selector CSS selector.\n * @return {node} Element matching selector within the view's root element (`this.el`).\n */\n $(selector) {\n return this.el.querySelector(selector);\n }\n\n /**\n * Returns a list of elements that match the selector, \n * scoped to DOM elements within the current view's root element (`this.el`).\n * @param {string} selector CSS selector.\n * @return {node[]} List of elements matching selector within the view's root element (`this.el`).\n */\n $$(selector) {\n return this.el.querySelectorAll(selector);\n }\n\n /**\n * Destroy the view.\n * Destroy children views if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.\n * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.\n * @return {View} Return `this` for chaining.\n */\n destroy() {\n // Call destroy on children.\n this.destroyChildren();\n // Undelegate `this.el` event listeners\n this.undelegateEvents();\n // Stop listening to events.\n this.stopListening();\n // Unbind `this` events.\n this.off();\n // Call destroy queue.\n this.destroyQueue.forEach(fn => fn());\n this.destroyQueue = [];\n // Call onDestroy lifecycle method\n this.onDestroy.apply(this, arguments);\n // Set destroyed flag.\n this.destroyed = true;\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * `onDestroy` lifecycle method is called after the view is destroyed.\n * Override with your code. Useful to stop listening to model's events.\n * @param {object} options Options object or any arguments passed to `destroy` method.\n */\n onDestroy() {}\n\n /**\n * Add a view as a child.\n * Children views are stored at `this.children`, and destroyed when the parent is destroyed.\n * Returns the child for chaining.\n * @param {View} child\n * @return {View}\n */\n addChild(child) {\n this.children.push(child);\n return child;\n }\n\n /**\n * Call destroy method on children views.\n */\n destroyChildren() {\n this.children.forEach(child => child.destroy());\n this.children = [];\n }\n\n /**\n * Ensure that the view has a unique id at `this.uid`.\n */\n ensureUid() {\n if (!this.uid) this.uid = `r${++View.uid}`;\n }\n\n /**\n * Ensure that the view has a root element at `this.el`.\n * You shouldn't call this method directly. It's called from the constructor.\n * You may override it if you want to use a different logic or to \n * postpone element creation.\n */\n ensureElement() {\n // Element is already present.\n if (this.el) {\n // If \"this.el\" is a function, call it to get the element.\n this.el = getResult(this.el, this);\n } else {\n // If \"this.el\" is not present,\n // create a new element according \"this.tag\"\n // and \"this.attributes\".\n const tag = getResult(this.tag, this);\n const attrs = getResult(this.attributes, this);\n this.el = this.createElement(tag, attrs);\n }\n // Delegate events on element.\n this.delegateEvents();\n }\n\n /**\n * Create an element.\n * Called from the constructor if `this.el` is undefined, to ensure\n * the view has a root element.\n * @param {string} tag Tag for the element. Default to `div`\n * @param {object} attributes Attributes for the element.\n * @return {node} The created element.\n */\n createElement(tag = 'div', attributes = {}) {\n // Create DOM element.\n let el = document.createElement(tag);\n // Add element attributes.\n Object.keys(attributes)\n .forEach(key => el.setAttribute(key, attributes[key]));\n\n return el;\n }\n\n /**\n * Remove `this.el` from the DOM.\n * @return {View} Return `this` for chaining.\n */\n removeElement() {\n this.el.parentNode.removeChild(this.el);\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Provide declarative listeners for DOM events within a view. If an events object is not provided, \n * it defaults to using `this.events`. If `this.events` is a function, it will be called to get the events object.\n * \n * The events object should follow the format `{'event selector': 'listener'}`:\n * - `event`: The type of event (e.g., 'click').\n * - `selector`: A CSS selector to match the event target. If omitted, the event is bound to the root element.\n * - `listener`: A function or a string representing a method name on the view. The method will be called with `this` bound to the view instance.\n * \n * By default, `delegateEvents` is called within the View's constructor. If you have a simple events object, \n * all of your DOM events will be connected automatically, and you will not need to call this function manually.\n * \n * All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.\n * When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.\n * \n * **Listener signature:** `(event, view, matched)`\n * - `event`: The native DOM event object.\n * - `view`: The current view instance (`this`).\n * - `matched`: The element that satisfies the selector. If no selector is provided, it will be the view's root element (`this.el`).\n *\n * If more than one ancestor between `event.target` and the view's root element matches the selector, the listener will be\n * invoked **once for each matched element** (from inner to outer).\n *\n * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.\n * @return {View} Returns `this` for chaining.\n * @example\n * // Using prototype (recommended for static events)\n * class Modal extends View {\n * onClickOk(event, view, matched) {\n * // matched === the button.ok element that was clicked\n * this.close();\n * }\n * \n * onClickCancel() {\n * this.destroy();\n * }\n * }\n * Modal.prototype.events = {\n * 'click button.ok': 'onClickOk',\n * 'click button.cancel': 'onClickCancel',\n * 'submit form': 'onSubmit'\n * };\n * \n * // Using a function for dynamic events\n * class DynamicView extends View {\n * events() {\n * return {\n * [`click .${this.model.buttonClass}`]: 'onButtonClick',\n * 'click': 'onRootClick'\n * };\n * }\n * }\n */\n delegateEvents(events) {\n if (!events) events = getResult(this.events, this);\n if (!events) return this;\n\n if (this.delegatedEventListeners.length) this.undelegateEvents();\n\n // Store events by type i.e.: \"click\", \"submit\", etc.\n let eventTypes = {};\n\n Object.keys(events).forEach(key => {\n const keyParts = key.split(' ');\n const type = keyParts.shift();\n const selector = keyParts.join(' ');\n\n let listener = events[key];\n // Listener may be a string representing a method name on the view, or a function.\n if (typeof listener === 'string') listener = this[listener];\n // Validate listener is a function.\n validateListener(listener);\n\n if (!eventTypes[type]) eventTypes[type] = [];\n\n eventTypes[type].push({ selector, listener });\n });\n\n Object.keys(eventTypes).forEach(type => {\n // Listener for the type of event.\n const typeListener = (event) => {\n // Iterate and run every individual listener if the selector matches.\n eventTypes[type].forEach(({ selector, listener }) => {\n // No selector provided: invoke listener once with root element.\n if (!selector) {\n listener.call(this, event, this, this.el);\n return;\n }\n\n let node = event.target;\n // Traverse ancestors until reaching the view root (`this.el`).\n while (node && node !== this.el) {\n if (node.matches && node.matches(selector)) {\n listener.call(this, event, this, node);\n }\n node = node.parentElement;\n }\n });\n };\n\n this.delegatedEventListeners.push({ type, listener : typeListener });\n this.el.addEventListener(type, typeListener);\n });\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Removes all of the view's delegated events. \n * Useful if you want to disable or remove a view from the DOM temporarily. \n * Called automatically when the view is destroyed and when `delegateEvents` is called again.\n * @return {View} Return `this` for chaining.\n */\n undelegateEvents() {\n this.delegatedEventListeners.forEach(({ type, listener }) => {\n this.el.removeEventListener(type, listener);\n });\n\n this.delegatedEventListeners = [];\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * `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`. \n * 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. \n * If you add any child views, you should call `this.destroyChildren` before re-rendering.\n * \n * @return {View} Returns `this` for chaining.\n * @example\n * class UserView extends View {\n * render() {\n * if (this.template) {\n * const model = this.model;\n * // Sanitize model attributes to prevent XSS attacks.\n * const safeData = {\n * name : View.sanitize(model.name),\n * email : View.sanitize(model.email),\n * bio : View.sanitize(model.bio)\n * };\n * this.el.innerHTML = this.template(safeData);\n * }\n * return this;\n * }\n * }\n */\n render() {\n return this;\n }\n\n /**\n * Escape HTML entities in a string.\n * Use this method to sanitize user-generated content before inserting it into the DOM.\n * Override this method to provide a custom escape function.\n * This method is inherited by {@link #module_component Component} and used to escape template interpolations.\n * @static\n * @param {string} value String to escape.\n * @return {string} Escaped string.\n */\n static sanitize(value) {\n return `${value}`.replace(/[&<>\"']/g, match => ({\n '&' : '&amp;',\n '<' : '&lt;',\n '>' : '&gt;',\n '\"' : '&quot;',\n '\\'' : '&#039;'\n }[match]));\n }\n\n /**\n * Reset the unique ID counter to 0.\n * This is useful for server-side rendering scenarios where you want to ensure that\n * the generated unique IDs match those on the client, enabling seamless hydration of components.\n * This method is inherited by {@link #module_component Component}.\n * @static\n */\n static resetUid() {\n View.uid = 0;\n }\n}\n\n/**\n * Counter for generating unique IDs for view instances. \n * This is primarily used to assign unique identifiers to each view instance (`this.uid`), which can be helpful for tasks like \n * generating element IDs. \n * {@link #module_component Component}s use `this.uid` to generate data attributes for their elements, to be looked up on hydration. \n * For server-side rendering, this counter should be reset to `0` on every request to ensure that the generated \n * unique IDs match those on the client, enabling seamless hydration of components. \n * @static\n * @type {number}\n * @default 0\n */\nView.uid = 0;\n"],"names":["getResult","validateListener"],"mappings":"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"}
package/lib/utils/dev.cjs CHANGED
@@ -7,6 +7,7 @@
7
7
  * - UMD dev: replaced with true
8
8
  * - UMD prod: replaced with false
9
9
  * @type {boolean}
10
+ * @module
10
11
  * @private
11
12
  */
12
13
  const __DEV__ = process.env.NODE_ENV !== 'production';
@@ -1 +1 @@
1
- {"version":3,"file":"dev.cjs","sources":["../../src/utils/dev.js"],"sourcesContent":["/**\n * Development mode flag.\n * This will be replaced during build:\n * - ESM/CJS: replaced with process.env.NODE_ENV !== 'production'\n * - UMD dev: replaced with true\n * - UMD prod: replaced with false\n * @type {boolean}\n * @private\n */\nconst __DEV__ = true;\n\nexport default __DEV__;\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAA,OAAA,GAAA,OAAA,CAAA,GAAA,CAAA,QAAA,KAAA;;;;"}
1
+ {"version":3,"file":"dev.cjs","sources":["../../src/utils/dev.js"],"sourcesContent":["/**\n * Development mode flag.\n * This will be replaced during build:\n * - ESM/CJS: replaced with process.env.NODE_ENV !== 'production'\n * - UMD dev: replaced with true\n * - UMD prod: replaced with false\n * @type {boolean}\n * @module\n * @private\n */\nconst __DEV__ = true;\n\nexport default __DEV__;\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAA,OAAA,GAAA,OAAA,CAAA,GAAA,CAAA,QAAA,KAAA;;;;"}
@@ -101,7 +101,7 @@ function formatTemplateSource(source, errorExpression) {
101
101
  // If error expression is multi-line, show full details.
102
102
  if (typeof errorExpression === 'function') {
103
103
  const fullSource = errorExpression.toString();
104
- if (fullSource.includes('\n')) {
104
+ if (fullSource.match(/\n/)) {
105
105
  formattedLines.push('');
106
106
  formattedLines.push(' | Expression details:');
107
107
  fullSource.split('\n').forEach(line => {
@@ -1 +1 @@
1
- {"version":3,"file":"formatTemplateSource.cjs","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.includes('\\n')) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":["padStart","repeat"],"mappings":"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"}
1
+ {"version":3,"file":"formatTemplateSource.cjs","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.match(/\\n/)) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":["padStart","repeat"],"mappings":";;;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,kBAAkB,CAAC,IAAI,EAAE;AAClC,IAAI,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE;AAC7C,QAAQ,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE;AACtC;AACA,QAAQ,OAAO,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,GAAG;AAC5F,IAAI;AACJ,IAAI,OAAO,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG;AACpC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,oBAAoB,CAAC,MAAM,EAAE,eAAe,EAAE;AACtE,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,eAAe,EAAE,OAAO,EAAE;;AAExF,IAAI,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM;;AAE3C;AACA,IAAI,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,CAAC,eAAe,CAAC;AAChE,IAAI,IAAI,eAAe,KAAK,EAAE,EAAE,OAAO,EAAE;;AAEzC;AACA,IAAI,IAAI,cAAc,GAAG,EAAE;AAC3B,IAAI,IAAI,aAAa,GAAG,EAAE;;AAE1B,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAC7C,QAAQ,cAAc,IAAI,OAAO,CAAC,CAAC,CAAC;;AAEpC,QAAQ,IAAI,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE;AACpC;AACA,YAAY,IAAI,CAAC,KAAK,eAAe,EAAE;AACvC,gBAAgB,aAAa,GAAG,cAAc,CAAC,MAAM;AACrD,YAAY;AACZ,YAAY,cAAc,IAAI,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;AAChE,QAAQ;AACR,IAAI;;AAEJ,IAAI,IAAI,aAAa,KAAK,EAAE,EAAE,OAAO,EAAE;;AAEvC;AACA,IAAI,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC;AAC5C,IAAI,MAAM,cAAc,GAAG,EAAE;AAC7B,IAAI,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM;;AAEvD,IAAI,IAAI,YAAY,GAAG,EAAE;AACzB,IAAI,IAAI,aAAa,GAAG,EAAE;AAC1B,IAAI,IAAI,SAAS,GAAG,CAAC;;AAErB,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAC3C,QAAQ,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAC/C,QAAQ,IAAI,SAAS,GAAG,UAAU,GAAG,aAAa,EAAE;AACpD,YAAY,YAAY,GAAG,CAAC;AAC5B,YAAY,aAAa,GAAG,aAAa,GAAG,SAAS;AACrD,YAAY;AACZ,QAAQ;AACR,QAAQ,SAAS,IAAI,UAAU;AAC/B,IAAI;;AAEJ,IAAI,IAAI,YAAY,KAAK,EAAE,EAAE,OAAO,EAAE;;AAEtC;AACA,IAAI,MAAM,aAAa,GAAG,kBAAkB,CAAC,eAAe,CAAC;;AAE7D;AACA,IAAI,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC;AACtD,IAAI,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC;;AAEnE,IAAI,KAAK,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,EAAE,EAAE;AACrD,QAAQ,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC;AAC7B,QAAQ,MAAM,UAAU,GAAGA,cAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,GAAG,CAAC;AAC1E,QAAQ,MAAM,WAAW,GAAG,CAAC,KAAK,YAAY;AAC9C,QAAQ,MAAM,UAAU,GAAG,WAAW,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC;;AAElF,QAAQ,cAAc,CAAC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;;AAElD;AACA,QAAQ,IAAI,WAAW,EAAE;AACzB,YAAY,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,GAAG,aAAa;AAC/D,YAAY,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM;AAClD,YAAY,MAAM,WAAW,GAAGC,YAAM,CAAC,GAAG,EAAE,SAAS,CAAC,GAAGA,YAAM,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,kBAAkB;AACpG,YAAY,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;AAC5C,QAAQ;AACR,IAAI;;AAEJ;AACA,IAAI,IAAI,OAAO,eAAe,KAAK,UAAU,EAAE;AAC/C,QAAQ,MAAM,UAAU,GAAG,eAAe,CAAC,QAAQ,EAAE;AACrD,QAAQ,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;AACpC,YAAY,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;AACnC,YAAY,cAAc,CAAC,IAAI,CAAC,4BAA4B,CAAC;AAC7D,YAAY,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI;AACnD,gBAAgB,cAAc,CAAC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;AACzD,YAAY,CAAC,CAAC;AACd,QAAQ;AACR,IAAI;;AAEJ,IAAI,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;AACpC;;;;"}
@@ -1,8 +1,23 @@
1
1
  'use strict';
2
2
 
3
+ let isChrome, moveBeforeSupported, preserveFocus, resetFocus;
4
+
5
+ // Browser compatibility notes (as of 2025):
6
+ // - Safari: Does not support moveBefore.
7
+ // - Firefox: moveBefore preserves focus but loses scroll position.
8
+ // - Chrome: moveBefore preserves scroll position but loses focus.
9
+ if (typeof document !== 'undefined') {
10
+ isChrome = !!navigator.userAgent.match(/Chrome/);
11
+ moveBeforeSupported = !!Element.prototype.moveBefore;
12
+ preserveFocus = !moveBeforeSupported || isChrome;
13
+ // When using moveBefore, Chrome resets the focus but preserves the active element.
14
+ // So we need to blur the active element before setting the focus again.
15
+ resetFocus = moveBeforeSupported && isChrome;
16
+ }
17
+
3
18
  /**
4
19
  * Replaces an existing DOM node with a new node, preserving internal DOM state.
5
- * Uses moveBefore if available, otherwise falls back to before.
20
+ * Uses moveBefore if available, otherwise falls back to insertBefore.
6
21
  *
7
22
  * @param {Node} oldNode The existing DOM node to replace.
8
23
  * @param {Node} newNode The new DOM node to replace the old node with.
@@ -10,16 +25,18 @@
10
25
  * @private
11
26
  */
12
27
  function replaceNode(oldNode, newNode) {
13
- if (Element.prototype.moveBefore) {
14
- oldNode.parentNode.moveBefore(newNode, oldNode);
15
- oldNode.parentNode.removeChild(oldNode);
16
- } else {
17
- const activeElement = document.activeElement;
18
- oldNode.parentNode.insertBefore(newNode, oldNode);
19
- oldNode.parentNode.removeChild(oldNode);
20
- if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
21
- activeElement.focus();
22
- }
28
+ const activeElement = preserveFocus &&
29
+ document.activeElement &&
30
+ newNode.contains(document.activeElement) ?
31
+ document.activeElement : null;
32
+
33
+ if (activeElement && resetFocus) activeElement.blur();
34
+
35
+ oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);
36
+ oldNode.parentNode.removeChild(oldNode);
37
+
38
+ if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
39
+ activeElement.focus();
23
40
  }
24
41
  }
25
42
 
@@ -1 +1 @@
1
- {"version":3,"file":"replaceNode.cjs","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to before.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n if (Element.prototype.moveBefore) {\n oldNode.parentNode.moveBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n } else {\n const activeElement = document.activeElement;\n oldNode.parentNode.insertBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n }\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE;AACtC,QAAQ,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC;AACvD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,IAAI,CAAC,MAAM;AACX,QAAQ,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa;AACpD,QAAQ,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC;AACzD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,QAAQ,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AAC1G,YAAY,aAAa,CAAC,KAAK,EAAE;AACjC,QAAQ;AACR,IAAI;AACJ;;;;"}
1
+ {"version":3,"file":"replaceNode.cjs","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["let isChrome, moveBeforeSupported, preserveFocus, resetFocus;\n\n// Browser compatibility notes (as of 2025):\n// - Safari: Does not support moveBefore.\n// - Firefox: moveBefore preserves focus but loses scroll position.\n// - Chrome: moveBefore preserves scroll position but loses focus.\nif (typeof document !== 'undefined') {\n isChrome = !!navigator.userAgent.match(/Chrome/);\n moveBeforeSupported = !!Element.prototype.moveBefore;\n preserveFocus = !moveBeforeSupported || isChrome;\n // When using moveBefore, Chrome resets the focus but preserves the active element.\n // So we need to blur the active element before setting the focus again.\n resetFocus = moveBeforeSupported && isChrome;\n}\n\n/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to insertBefore.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n const activeElement = preserveFocus &&\n document.activeElement &&\n newNode.contains(document.activeElement) ?\n document.activeElement : null;\n\n if (activeElement && resetFocus) activeElement.blur();\n\n oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n}\n"],"names":[],"mappings":";;AAAA,IAAI,QAAQ,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU;;AAE5D;AACA;AACA;AACA;AACA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;AACrC,IAAI,QAAQ,GAAG,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC;AACpD,IAAI,mBAAmB,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU;AACxD,IAAI,aAAa,GAAG,CAAC,mBAAmB,IAAI,QAAQ;AACpD;AACA;AACA,IAAI,UAAU,GAAG,mBAAmB,IAAI,QAAQ;AAChD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,MAAM,aAAa,GAAG,aAAa;AACvC,QAAQ,QAAQ,CAAC,aAAa;AAC9B,QAAQ,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC;AAChD,QAAQ,QAAQ,CAAC,aAAa,GAAG,IAAI;;AAErC,IAAI,IAAI,aAAa,IAAI,UAAU,EAAE,aAAa,CAAC,IAAI,EAAE;;AAEzD,IAAI,OAAO,CAAC,UAAU,CAAC,mBAAmB,GAAG,YAAY,GAAG,cAAc,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC;AAC7F,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;;AAE3C,IAAI,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AACtG,QAAQ,aAAa,CAAC,KAAK,EAAE;AAC7B,IAAI;AACJ;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasti",
3
- "version": "4.0.0-alpha.9",
3
+ "version": "4.0.0",
4
4
  "description": "Modern MVC for building user interfaces",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -24,13 +24,13 @@
24
24
  "src"
25
25
  ],
26
26
  "scripts": {
27
- "clean": "rimraf lib dist es",
27
+ "clean": "node -e \"const fs=require('fs');['lib','dist','es'].forEach(d=>fs.rmSync(d,{recursive:true,force:true}))\"",
28
28
  "build": "rollup -c",
29
29
  "prepare": "npm run clean && npm run test && npm run build",
30
30
  "posttest": "npm run lint",
31
31
  "lint": "eslint src test",
32
32
  "test": "mocha --require jsdom-global/register --reporter nyan test/*.js",
33
- "version": "node -e \"const fs=require('fs');const v=require('./package.json').version;const re=/@v\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?/g;const t1=fs.readFileSync('README.md','utf8');fs.writeFileSync('README.md',t1.replace(re,'@v'+v));const t2=fs.readFileSync('example/todo/index.html','utf8');fs.writeFileSync('example/todo/index.html',t2.replace(re,'@v'+v));\" && git add README.md example/todo/index.html",
33
+ "version": "node -e \"const fs=require('fs');const {execSync}=require('child_process');const v=require('./package.json').version;const re=/rasti@(v)?\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?/g;const files=['README.md', 'docs/AGENTS.md', 'example/todo/index.html'];files.forEach(f=>{const t=fs.readFileSync(f,'utf8');fs.writeFileSync(f,t.replace(re,(m,p)=>'rasti@'+(p||'')+v));});execSync('git add '+files.join(' '),{stdio:'inherit'});\"",
34
34
  "docs:api": "jsdoc2md --module-index-format grouped --helper jsdoc2md/helper.cjs --partial jsdoc2md/header.hbs --partial jsdoc2md/sig-link.hbs --partial jsdoc2md/sig-link-html.hbs --partial jsdoc2md/sig-link-parent.hbs --partial jsdoc2md/link.hbs --files 'src/**/*.js' > docs/api.md"
35
35
  },
36
36
  "repository": {
@@ -65,7 +65,6 @@
65
65
  "jsdom": "^26.1.0",
66
66
  "jsdom-global": "3.0.2",
67
67
  "mocha": "^11.7.1",
68
- "rimraf": "^6.0.1",
69
68
  "rollup": "^4.48.1"
70
69
  },
71
70
  "directories": {
package/src/Component.js CHANGED
@@ -28,21 +28,37 @@ import __DEV__ from './utils/dev.js';
28
28
  */
29
29
  const getExpressionResult = (expression, context, meta) => {
30
30
  try {
31
- return getResult(expression, context, context);
31
+ if (typeof expression !== 'function') return expression;
32
+ // In development, detect uninstantiated Component classes and provide a helpful error.
33
+ // This typically happens when a component tag is malformed and not properly expanded.
34
+ if (__DEV__ && expression.prototype instanceof Component) {
35
+ throw new Error(
36
+ `Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` +
37
+ 'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' +
38
+ 'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.'
39
+ );
40
+ }
41
+
42
+ return expression.call(context, context);
32
43
  } catch (error) {
33
- if (meta && !error.cause) {
44
+ if (meta && !error._rasti) {
34
45
  let message;
35
46
 
36
47
  if (__DEV__) {
37
48
  const formattedSource = formatTemplateSource(context.source, expression);
38
- message = createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`);
49
+ message = createDevelopmentErrorMessage(
50
+ `Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`
51
+ );
39
52
  } else {
40
53
  message = createProductionErrorMessage(`Error in ${context.constructor.name}#${context.uid} expression`);
41
54
  }
55
+
42
56
  const enhancedError = new Error(message, { cause : error });
43
- enhancedError.stack = error.stack;
57
+ enhancedError._rasti = true;
58
+
44
59
  throw enhancedError;
45
60
  }
61
+
46
62
  throw error;
47
63
  }
48
64
  };
@@ -63,7 +79,9 @@ const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_
63
79
  * @return {boolean} True if the element contains a component.
64
80
  * @private
65
81
  */
66
- const containsElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) || !!el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`);
82
+ const containsElement = (el) => !!(
83
+ el && ((el.dataset && el.dataset[Component.DATASET_ELEMENT]) || (el.querySelector && el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`)))
84
+ );
67
85
 
68
86
  /**
69
87
  * Generate string with placeholders for interpolated expressions.
@@ -206,8 +224,8 @@ const expandComponents = (main, expressions, skipNormalization = false) => {
206
224
  }
207
225
  // Match component tags with backreference to ensure correct pairing.
208
226
  return main.replace(
209
- new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'),
210
- (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => {
227
+ new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'),
228
+ (match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => {
211
229
  let tag, attributesStr, innerList;
212
230
 
213
231
  if (openTag) {
@@ -237,7 +255,7 @@ const expandComponents = (main, expressions, skipNormalization = false) => {
237
255
  // Add `renderChildren` function to options.
238
256
  if (innerList) {
239
257
  // Evaluate items in parent context and create Partial.
240
- options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this)));
258
+ options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children')));
241
259
  }
242
260
  // Mount component.
243
261
  return tag.mount(options);
@@ -454,7 +472,7 @@ const parseAttributes = (attributesStr, expressions) => {
454
472
  const PH = Component.PLACEHOLDER('(\\d+)');
455
473
  const attributes = [];
456
474
  // Parse attributes string with support for placeholders in both names and values.
457
- const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))?\\3)?`, 'g');
475
+ const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g');
458
476
 
459
477
  let attributeMatch;
460
478
  while ((attributeMatch = regExp.exec(attributesStr)) !== null) {
@@ -482,7 +500,7 @@ const parseAttributes = (attributesStr, expressions) => {
482
500
  /*
483
501
  * These option keys will be extended on the component instance.
484
502
  */
485
- const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate'];
503
+ const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate'];
486
504
 
487
505
  /**
488
506
  * @lends module:Component
@@ -655,22 +673,34 @@ class Component extends View {
655
673
  /**
656
674
  * Used internally on the render process.
657
675
  * Reuse a `Component` by replacing the placeholder comment with the real nodes.
658
- * Call `onRecycle` lifecycle method.
659
- * @param parent {node} The parent node.
660
- * @param props {object} The props to set on the recycled component.
676
+ * Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur.
677
+ * @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved.
661
678
  * @return {Component} The component instance.
662
679
  * @private
663
680
  */
664
- recycle(parent, props) {
681
+ recycle(parent) {
682
+ // Call `onBeforeRecycle` lifecycle method.
683
+ this.onBeforeRecycle.call(this);
684
+ // No parent means the node is already in the correct position. So we don't need to replace it.
665
685
  if (parent) {
666
686
  // Locate the placeholder comment and replace it with the real nodes
667
687
  const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
668
688
  replaceNode(placeholder, this.el);
669
689
  }
670
- // Update props.
671
- if (props) {
672
- this.props.set(props);
673
- }
690
+ // Return `this` for chaining.
691
+ return this;
692
+ }
693
+
694
+ /**
695
+ * Update the component's props.
696
+ * Sets the props and calls the `onRecycle` lifecycle method.
697
+ * @param props {object} The props to set on the component.
698
+ * @return {Component} The component instance.
699
+ * @private
700
+ */
701
+ updateProps(props) {
702
+ // Set the props.
703
+ this.props.set(props);
674
704
  // Call `onRecycle` lifecycle method.
675
705
  this.onRecycle.call(this);
676
706
  // Return `this` for chaining.
@@ -701,7 +731,7 @@ class Component extends View {
701
731
  * import { Component } from 'rasti';
702
732
  * // Create a Title component.
703
733
  * const Title = Component.create`
704
- * <h1>${({ props }) => props.children}</h1>
734
+ * <h1>${({ props }) => props.renderChildren()}</h1>
705
735
  * `;
706
736
  * // Create Main component.
707
737
  * const Main = Component.create`
@@ -835,14 +865,42 @@ class Component extends View {
835
865
  }
836
866
 
837
867
  /**
838
- * Render the `Component`.
839
- * - 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.
840
- * - 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.
841
- * - When rendering child components, recycling happens in two ways:
842
- * - Components with a `key` are recycled if a previous child with the same key exists.
843
- * - Unkeyed components are recycled if they have the same type and position in the template or partial.
844
- * A recycled `Component` will call the `onRecycle` lifecycle method.
845
- * - If the active element is inside the component, it will retain focus after the render.
868
+ * Render the `Component`.
869
+ *
870
+ * **First render (when `this.el` is not present):**
871
+ * This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated,
872
+ * making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM.
873
+ * The `onHydrate` lifecycle method will be called.
874
+ *
875
+ * **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()`
876
+ * handles this process automatically, creating the component instance, rendering it, and appending it to the DOM.
877
+ *
878
+ * **Update render (when `this.el` is present):**
879
+ * This indicates the component is being updated. The method will:
880
+ * - Update only the attributes of the root element and child elements
881
+ * - Update only the content of interpolations (the dynamic parts of the template)
882
+ * - For container components (components that render a single child component), update the single interpolation
883
+ *
884
+ * The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end.
885
+ *
886
+ * **Child component handling:**
887
+ * When rendering child components, they can be either recreated or recycled:
888
+ *
889
+ * - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component
890
+ * is found for recycling.
891
+ *
892
+ * - **Recycling:** The same component instance is reused. Recycling happens in two ways:
893
+ * - Components with a `key` are recycled if a previous child with the same key exists
894
+ * - Unkeyed components are recycled if they have the same type and position in the template or partial
895
+ *
896
+ * When a component is recycled:
897
+ * - The `onBeforeRecycle` lifecycle method is called when recycling starts
898
+ * - The component's `this.props` is updated with the new props from the parent
899
+ * - The `onRecycle` lifecycle method is called after props are updated
900
+ *
901
+ * A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model
902
+ * (or even the same model as the parent) and update independently in subsequent render cycles.
903
+ *
846
904
  * @return {Component} The component instance.
847
905
  */
848
906
  render() {
@@ -854,14 +912,16 @@ class Component extends View {
854
912
  this.hydrate(fragment);
855
913
  return this;
856
914
  }
915
+ // Call `onBeforeUpdate` lifecycle method.
916
+ this.onBeforeUpdate.call(this);
857
917
  // Clear event listeners.
858
918
  this.eventsManager.reset();
859
- // Update elements.
860
- this.template.elements.forEach(element => element.update());
861
919
  // Store previous children.
862
920
  const previousChildren = this.children;
863
921
  // Clear current children.
864
922
  this.children = [];
923
+ // Store props to update.
924
+ const propsQueue = [];
865
925
  // Update interpolations.
866
926
  this.template.interpolations.forEach(interpolation => {
867
927
  // Reset the tracker.
@@ -905,8 +965,10 @@ class Component extends View {
905
965
  const rendered = this.renderTemplatePart(interpolation.expression, addChild, tracker);
906
966
 
907
967
  const recycle = ([recycled, discarded], fragment) => {
908
- // Add child, update props and recycle (move to new position if needed).
909
- this.addChild(recycled).recycle(fragment, discarded.props.toJSON());
968
+ // Store props to update.
969
+ propsQueue.push([recycled, discarded.props.toJSON()]);
970
+ // Add child and recycle (move to new position if needed).
971
+ this.addChild(recycled).recycle(fragment);
910
972
  // Destroy discarded component.
911
973
  discarded.destroy();
912
974
  };
@@ -935,10 +997,17 @@ class Component extends View {
935
997
  previousChildren.forEach(prev => {
936
998
  if (this.children.indexOf(prev) < 0) prev.destroy();
937
999
  });
938
- // If container, set el to the child element.
1000
+ // Update recycled children props.
1001
+ propsQueue.forEach(([recycled, props]) => {
1002
+ recycled.updateProps(props);
1003
+ });
1004
+ // If this component is a container, set el to the child element.
1005
+ // Otherwise, update elements attributes and delegate events.
939
1006
  if (this.isContainer()) {
940
1007
  this.el = this.children[0].el;
941
1008
  } else {
1009
+ // Update elements attributes.
1010
+ this.template.elements.forEach(element => element.update());
942
1011
  // If there are pending event types, delegate events again.
943
1012
  if (this.eventsManager.hasPendingTypes()) {
944
1013
  this.delegateEvents();
@@ -980,6 +1049,19 @@ class Component extends View {
980
1049
  */
981
1050
  onHydrate() {}
982
1051
 
1052
+ /**
1053
+ * Lifecycle method. Called before the component is recycled and reused between renders.
1054
+ * This method is called at the beginning of the `recycle` method, before any recycling operations occur.
1055
+ *
1056
+ * A component is recycled when:
1057
+ * - It has a `key` and a previous child with the same key exists
1058
+ * - It doesn't have a `key` but has the same type and position in the template or partial
1059
+ *
1060
+ * Use this method to perform operations that need to happen before the component is recycled,
1061
+ * such as storing previous state or preparing for the recycling.
1062
+ */
1063
+ onBeforeRecycle() {}
1064
+
983
1065
  /**
984
1066
  * Lifecycle method. Called when the component is recycled and reused between renders.
985
1067
  *
@@ -992,6 +1074,14 @@ class Component extends View {
992
1074
  */
993
1075
  onRecycle() {}
994
1076
 
1077
+ /**
1078
+ * Lifecycle method. Called before the component is updated or re-rendered.
1079
+ * 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.
1080
+ * Use this method to perform operations that need to happen before the component is updated,
1081
+ * such as saving previous state or preparing for the update.
1082
+ */
1083
+ onBeforeUpdate() {}
1084
+
995
1085
  /**
996
1086
  * Lifecycle method. Called when the component is updated or re-rendered.
997
1087
  * This method is called when the component's state, model, or props change and trigger a re-render.
@@ -1113,7 +1203,7 @@ class Component extends View {
1113
1203
  * ```javascript
1114
1204
  * const Button = Component.create`
1115
1205
  * <button class="${({ props }) => props.className}">
1116
- * ${({ props }) => props.children}
1206
+ * ${({ props }) => props.renderChildren()}
1117
1207
  * </button>
1118
1208
  * `;
1119
1209
  * ```
@@ -1159,14 +1249,14 @@ class Component extends View {
1159
1249
  * // Create a button component.
1160
1250
  * const Button = Component.create`
1161
1251
  * <button class="button">
1162
- * ${({ props }) => props.children}
1252
+ * ${({ props }) => props.renderChildren()}
1163
1253
  * </button>
1164
1254
  * `;
1165
1255
  * // Create a navigation component. Add buttons as children. Iterate over items.
1166
1256
  * const Navigation = Component.create`
1167
1257
  * <nav>
1168
1258
  * ${({ props }) => props.items.map(
1169
- * item => Button.mount({ children : item.label })
1259
+ * item => Button.mount({ renderChildren : () => item.label })
1170
1260
  * )}
1171
1261
  * </nav>
1172
1262
  * `;
@@ -1182,7 +1272,7 @@ class Component extends View {
1182
1272
  * // Create a button component.
1183
1273
  * const Button = Component.create`
1184
1274
  * <button class="button">
1185
- * ${({ props }) => props.children}
1275
+ * ${({ props }) => props.renderChildren()}
1186
1276
  * </button>
1187
1277
  * `;
1188
1278
  * // Create a navigation component. Add buttons as children. Iterate over items.
@@ -1205,7 +1295,7 @@ class Component extends View {
1205
1295
  * // Create a button component.
1206
1296
  * const Button = Component.create`
1207
1297
  * <button class="${({ props }) => props.className}">
1208
- * ${({ props }) => props.children}
1298
+ * ${({ props }) => props.renderChildren()}
1209
1299
  * </button>
1210
1300
  * `;
1211
1301
  * // Create a container that renders a Button component.
@@ -1215,7 +1305,7 @@ class Component extends View {
1215
1305
  * // Create a container that renders a Button component, using a function.
1216
1306
  * const ButtonCancel = Component.create(() => Button.mount({
1217
1307
  * className : 'cancel',
1218
- * children : 'Cancel'
1308
+ * renderChildren : () => 'Cancel'
1219
1309
  * }));
1220
1310
  * ```
1221
1311
  * @static
@@ -1305,7 +1395,7 @@ Component.MARKER_END = (uid) => `rst-e-${uid}`;
1305
1395
  * 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.
1306
1396
  * @module
1307
1397
  * @extends View
1308
- * @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`.
1398
+ * @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`.
1309
1399
  * @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.
1310
1400
  * @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.
1311
1401
  * @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.