rasti 4.1.2 → 4.1.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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';\nimport createDevelopmentErrorMessage from './utils/createDevelopmentErrorMessage.js';\nimport createProductionErrorMessage from './utils/createProductionErrorMessage.js';\nimport __DEV__ from './utils/dev.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 {string} 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 const eventTypes = {};\n\n Object.keys(events).forEach(key => {\n const match = key.match(/^(\\w+)(?:\\s+(.+))*$/);\n\n if (!match) {\n const message = `Invalid event format: ${key}`;\n throw new Error(__DEV__ ? createDevelopmentErrorMessage(message) : createProductionErrorMessage(message));\n }\n // Extract type and selector from the event key.\n const [,type, selector] = match;\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 let node = event.target;\n // Traverse ancestors until reaching the view root (`this.el`).\n while (node) {\n if (node.matches) {\n // Iterate and run every individual listener if the selector matches.\n eventTypes[type].forEach(([selector, listener]) => {\n if ((node === this.el && !selector) || (node !== this.el && node.matches(selector))) {\n listener.call(this, event, this, node);\n }\n });\n }\n // Continue traversing ancestors until reaching the view root (`this.el`) or stopping propagation.\n node = node === this.el || event.cancelBubble ? null : node.parentElement;\n }\n };\n // Store the type and listener in the delegated event listeners array.\n this.delegatedEventListeners.push([type, typeListener]);\n // Add the event listener to the element.\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 * @memberof module:View\n * @name uid\n * @type {number}\n * @default 0\n */\nView.uid = 0;\n"],"names":["getResult","__DEV__","createDevelopmentErrorMessage","createProductionErrorMessage","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 defineOwn from './utils/defineOwn.js';\nimport validateListener from './utils/validateListener.js';\nimport createDevelopmentErrorMessage from './utils/createDevelopmentErrorMessage.js';\nimport createProductionErrorMessage from './utils/createProductionErrorMessage.js';\nimport __DEV__ from './utils/dev.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 {string} 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, as own properties so an option overrides\n // a getter declared by a subclass instead of being assigned through it.\n viewOptions.forEach(key => {\n if (key in options) {\n defineOwn(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 const eventTypes = {};\n\n Object.keys(events).forEach(key => {\n const match = key.match(/^(\\w+)(?:\\s+(.+))*$/);\n\n if (!match) {\n const message = `Invalid event format: ${key}`;\n throw new Error(__DEV__ ? createDevelopmentErrorMessage(message) : createProductionErrorMessage(message));\n }\n // Extract type and selector from the event key.\n const [,type, selector] = match;\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 let node = event.target;\n // Traverse ancestors until reaching the view root (`this.el`).\n while (node) {\n if (node.matches) {\n // Iterate and run every individual listener if the selector matches.\n eventTypes[type].forEach(([selector, listener]) => {\n if ((node === this.el && !selector) || (node !== this.el && node.matches(selector))) {\n listener.call(this, event, this, node);\n }\n });\n }\n // Continue traversing ancestors until reaching the view root (`this.el`) or stopping propagation.\n node = node === this.el || event.cancelBubble ? null : node.parentElement;\n }\n };\n // Store the type and listener in the delegated event listeners array.\n this.delegatedEventListeners.push([type, typeListener]);\n // Add the event listener to the element.\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 * @memberof module:View\n * @name uid\n * @type {number}\n * @default 0\n */\nView.uid = 0;\n"],"names":["defineOwn","getResult","__DEV__","createDevelopmentErrorMessage","createProductionErrorMessage","validateListener"],"mappings":"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"}
package/lib/index.cjs CHANGED
@@ -11,6 +11,7 @@ require('./utils/padEnd.cjs');
11
11
  require('./utils/createProductionErrorMessage.cjs');
12
12
  require('./utils/dev.cjs');
13
13
  require('./utils/getResult.cjs');
14
+ require('./utils/defineOwn.cjs');
14
15
  require('./core/SafeHTML.cjs');
15
16
  require('./core/Partial.cjs');
16
17
  require('./core/EventsManager.cjs');
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Define `key` on `target` as an own data property. Unlike an assignment, it does not go
5
+ * through an accessor declared on the prototype, so it shadows a getter with no setter
6
+ * instead of throwing.
7
+ * @param {object} target Object to define the property on.
8
+ * @param {string} key Property name.
9
+ * @param {any} value Property value.
10
+ * @return {object} The target object.
11
+ * @module
12
+ * @private
13
+ */
14
+ const defineOwn = (target, key, value) => Object.defineProperty(target, key, {
15
+ value, writable : true, enumerable : true, configurable : true
16
+ });
17
+
18
+ module.exports = defineOwn;
19
+ //# sourceMappingURL=defineOwn.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defineOwn.cjs","sources":["../../src/utils/defineOwn.js"],"sourcesContent":["/**\n * Define `key` on `target` as an own data property. Unlike an assignment, it does not go\n * through an accessor declared on the prototype, so it shadows a getter with no setter\n * instead of throwing.\n * @param {object} target Object to define the property on.\n * @param {string} key Property name.\n * @param {any} value Property value.\n * @return {object} The target object.\n * @module\n * @private\n */\nconst defineOwn = (target, key, value) => Object.defineProperty(target, key, {\n value, writable : true, enumerable : true, configurable : true\n});\n\nexport default defineOwn;\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,SAAS,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,KAAK,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE;AAC7E,IAAI,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,UAAU,GAAG,IAAI,EAAE,YAAY,GAAG;AAC9D,CAAC;;;;"}
@@ -2,6 +2,10 @@
2
2
 
3
3
  /**
4
4
  * Parse HTML string to a DocumentFragment.
5
+ *
6
+ * The string is parsed as written. A component's markup already reaches here
7
+ * without surrounding whitespace, and the content of an interpolation is the
8
+ * rendered value itself, whose leading and trailing whitespace is significant.
5
9
  * @param {string} html The HTML string to parse.
6
10
  * @return {DocumentFragment} The parsed DocumentFragment.
7
11
  * @module
@@ -9,7 +13,7 @@
9
13
  */
10
14
  function parseHTML(html) {
11
15
  const fragment = document.createElement('template');
12
- fragment.innerHTML = `${html}`.trim();
16
+ fragment.innerHTML = `${html}`;
13
17
  return fragment.content;
14
18
  }
15
19
 
@@ -1 +1 @@
1
- {"version":3,"file":"parseHTML.cjs","sources":["../../src/utils/parseHTML.js"],"sourcesContent":["/**\n * Parse HTML string to a DocumentFragment.\n * @param {string} html The HTML string to parse.\n * @return {DocumentFragment} The parsed DocumentFragment.\n * @module\n * @private\n */\nexport default function parseHTML(html) {\n const fragment = document.createElement('template');\n fragment.innerHTML = `${html}`.trim();\n return fragment.content;\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,SAAS,CAAC,IAAI,EAAE;AACxC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC;AACvD,IAAI,QAAQ,CAAC,SAAS,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE;AACzC,IAAI,OAAO,QAAQ,CAAC,OAAO;AAC3B;;;;"}
1
+ {"version":3,"file":"parseHTML.cjs","sources":["../../src/utils/parseHTML.js"],"sourcesContent":["/**\n * Parse HTML string to a DocumentFragment.\n *\n * The string is parsed as written. A component's markup already reaches here\n * without surrounding whitespace, and the content of an interpolation is the\n * rendered value itself, whose leading and trailing whitespace is significant.\n * @param {string} html The HTML string to parse.\n * @return {DocumentFragment} The parsed DocumentFragment.\n * @module\n * @private\n */\nexport default function parseHTML(html) {\n const fragment = document.createElement('template');\n fragment.innerHTML = `${html}`;\n return fragment.content;\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,SAAS,CAAC,IAAI,EAAE;AACxC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC;AACvD,IAAI,QAAQ,CAAC,SAAS,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;AAClC,IAAI,OAAO,QAAQ,CAAC,OAAO;AAC3B;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasti",
3
- "version": "4.1.2",
3
+ "version": "4.1.3-alpha.1",
4
4
  "description": "Modern MVC for building user interfaces",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
package/src/Component.js CHANGED
@@ -7,6 +7,7 @@ import Element from './core/Element.js';
7
7
  import Interpolation from './core/Interpolation.js';
8
8
  import validateListener from './utils/validateListener.js';
9
9
  import getResult from './utils/getResult.js';
10
+ import defineOwn from './utils/defineOwn.js';
10
11
  import deepFlat from './utils/deepFlat.js';
11
12
  import parseHTML from './utils/parseHTML.js';
12
13
  import findComment from './utils/findComment.js';
@@ -506,10 +507,11 @@ class Component extends View {
506
507
  constructor(options = {}) {
507
508
  super(...arguments);
508
509
  this.componentOptions = [];
509
- // Extend "this" with options.
510
+ // Extend "this" with options, as own properties so an option overrides
511
+ // a getter declared by a subclass instead of being assigned through it.
510
512
  componentOptions.forEach(key => {
511
513
  if (key in options) {
512
- this[key] = options[key];
514
+ defineOwn(this, key, options[key]);
513
515
  this.componentOptions.push(key);
514
516
  }
515
517
  });
@@ -1305,6 +1307,17 @@ class Component extends View {
1305
1307
  * renderChildren : () => 'Cancel'
1306
1308
  * }));
1307
1309
  * ```
1310
+ * - Called on a subclass, the new component extends it, so the template can use its methods.
1311
+ * ```javascript
1312
+ * class ListBase extends Component {
1313
+ * renderItems() {
1314
+ * return this.props.items.map(item => this.partial`<li>${item}</li>`);
1315
+ * }
1316
+ * }
1317
+ * const List = ListBase.create`
1318
+ * <ul>${(self) => self.renderItems()}</ul>
1319
+ * `;
1320
+ * ```
1308
1321
  * @static
1309
1322
  * @param {string|Function} strings - HTML template for the component or a function that mounts a sub component.
1310
1323
  * @param {...*} expressions - The expressions to be interpolated within the template.
package/src/View.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Emitter from './Emitter.js';
2
2
  import getResult from './utils/getResult.js';
3
+ import defineOwn from './utils/defineOwn.js';
3
4
  import validateListener from './utils/validateListener.js';
4
5
  import createDevelopmentErrorMessage from './utils/createDevelopmentErrorMessage.js';
5
6
  import createProductionErrorMessage from './utils/createProductionErrorMessage.js';
@@ -74,10 +75,11 @@ export default class View extends Emitter {
74
75
  // Mutable array to store handlers to be called on destroy.
75
76
  this.destroyQueue = [];
76
77
  this.viewOptions = [];
77
- // Extend "this" with options.
78
+ // Extend "this" with options, as own properties so an option overrides
79
+ // a getter declared by a subclass instead of being assigned through it.
78
80
  viewOptions.forEach(key => {
79
81
  if (key in options) {
80
- this[key] = options[key];
82
+ defineOwn(this, key, options[key]);
81
83
  this.viewOptions.push(key);
82
84
  }
83
85
  });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Define `key` on `target` as an own data property. Unlike an assignment, it does not go
3
+ * through an accessor declared on the prototype, so it shadows a getter with no setter
4
+ * instead of throwing.
5
+ * @param {object} target Object to define the property on.
6
+ * @param {string} key Property name.
7
+ * @param {any} value Property value.
8
+ * @return {object} The target object.
9
+ * @module
10
+ * @private
11
+ */
12
+ const defineOwn = (target, key, value) => Object.defineProperty(target, key, {
13
+ value, writable : true, enumerable : true, configurable : true
14
+ });
15
+
16
+ export default defineOwn;
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Parse HTML string to a DocumentFragment.
3
+ *
4
+ * The string is parsed as written. A component's markup already reaches here
5
+ * without surrounding whitespace, and the content of an interpolation is the
6
+ * rendered value itself, whose leading and trailing whitespace is significant.
3
7
  * @param {string} html The HTML string to parse.
4
8
  * @return {DocumentFragment} The parsed DocumentFragment.
5
9
  * @module
@@ -7,6 +11,6 @@
7
11
  */
8
12
  export default function parseHTML(html) {
9
13
  const fragment = document.createElement('template');
10
- fragment.innerHTML = `${html}`.trim();
14
+ fragment.innerHTML = `${html}`;
11
15
  return fragment.content;
12
16
  }
@@ -1,7 +1,7 @@
1
- import View, { ViewOptions } from './View.js';
1
+ import View, { ViewOptions, Resolvable } from './View.js';
2
2
  import Model from './Model.js';
3
3
 
4
- export interface ComponentReservedOptions<S = any, M = any> extends ViewOptions<M> {
4
+ export interface ComponentReservedOptions<S = any, M = any> extends ViewOptions<M, Component<any, S, M>> {
5
5
  /**
6
6
  * A unique key to identify the component.
7
7
  * Components with keys are recycled when the same key is found in the previous render.
@@ -14,22 +14,22 @@ export interface ComponentReservedOptions<S = any, M = any> extends ViewOptions<
14
14
  */
15
15
  state?: S;
16
16
  /** Lifecycle hook called at the end of the constructor. */
17
- onCreate?: (...args: any[]) => void;
17
+ onCreate?: (this: Component<any, S, M>, ...args: any[]) => void;
18
18
  /** Lifecycle hook called when `model`, `state` or `props` emits `change`. */
19
- onChange?: (...args: any[]) => void;
19
+ onChange?: (this: Component<any, S, M>, ...args: any[]) => void;
20
20
  /** Lifecycle hook called after the first render (client only). */
21
- onHydrate?: () => void;
21
+ onHydrate?: (this: Component<any, S, M>) => void;
22
22
  /** Lifecycle hook called at the start of `recycle`, before any recycling happens. */
23
- onBeforeRecycle?: () => void;
23
+ onBeforeRecycle?: (this: Component<any, S, M>) => void;
24
24
  /** Lifecycle hook called after the component is recycled and props are updated. */
25
- onRecycle?: () => void;
25
+ onRecycle?: (this: Component<any, S, M>) => void;
26
26
  /** Lifecycle hook called at the start of `render` on update. */
27
- onBeforeUpdate?: () => void;
27
+ onBeforeUpdate?: (this: Component<any, S, M>) => void;
28
28
  /** Lifecycle hook called at the end of `render` on update. */
29
- onUpdate?: () => void;
29
+ onUpdate?: (this: Component<any, S, M>) => void;
30
30
  }
31
31
 
32
- export type ComponentOptions<P = {}, S = any, M = any> = P & ComponentReservedOptions<S, M>;
32
+ export type ComponentOptions<P = Record<string, any>, S = any, M = any> = P & ComponentReservedOptions<S, M>;
33
33
 
34
34
  /** Marker type for strings that are safe to inject as HTML without sanitization. */
35
35
  export interface SafeHTML {
@@ -127,7 +127,7 @@ export type ExtendedComponent<T extends new (...args: any[]) => any, O> =
127
127
  * Timer.mount({ model }, document.body);
128
128
  * setInterval(() => model.seconds++, 1000);
129
129
  */
130
- declare class Component<P = {}, S = any, M = any> extends View<M> {
130
+ declare class Component<P = Record<string, any>, S = any, M = any> extends View<M> {
131
131
  /**
132
132
  * Mark a string as safe HTML to be rendered.
133
133
  * Rasti marks string literals as safe automatically when a component is created or when
@@ -185,6 +185,7 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
185
185
  * - DOM event handlers via camelCased attributes (`onClick=${handler}`), delegated to the root.
186
186
  * - Returning a component instance (or array of them) adds it as a child.
187
187
  * - Use `<${Sub}>…</${Sub}>` syntax for child component tags.
188
+ * - Called on a subclass, the new component extends it, so the template can use its methods.
188
189
  *
189
190
  * @example
190
191
  * const Button = Component.create`
@@ -194,6 +195,15 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
194
195
  * </button>
195
196
  * `;
196
197
  */
198
+ static create<T extends new (...args: any[]) => Component<any, any, any>>(
199
+ this: T,
200
+ strings: string | TemplateStringsArray | ((...args: any[]) => any),
201
+ ...expressions: any[]
202
+ ): T;
203
+ /**
204
+ * Creates a component from a template, typing its props, state and model explicitly.
205
+ * See the overload above for the template syntax.
206
+ */
197
207
  static create<P = Record<string, any>, S = any, M = any>(
198
208
  strings: string | TemplateStringsArray | ((...args: any[]) => any),
199
209
  ...expressions: any[]
@@ -220,8 +230,13 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
220
230
  /** The original options object passed to the constructor. */
221
231
  options: ComponentOptions<P, S, M>;
222
232
 
223
- /** Template function returning the view's inner HTML. */
224
- template: (...args: any[]) => string;
233
+ /**
234
+ * Internal. Not a view-style template function returning HTML: `Component.create`
235
+ * installs a method returning the parsed structure the render pipeline walks, and the
236
+ * member is replaced by that structure when the instance is created. Define a
237
+ * component's markup with {@link Component.create}, not by assigning here.
238
+ */
239
+ template: any;
225
240
 
226
241
  /**
227
242
  * @param options Component options. Keys `model`, `state`, `key`, `onCreate`, `onChange`,
@@ -281,6 +296,33 @@ declare class Component<P = {}, S = any, M = any> extends View<M> {
281
296
  onUpdate(): void;
282
297
  }
283
298
 
299
+ /**
300
+ * Declared apart from the class body so a subclass can provide `events` as a getter. See
301
+ * {@link View}'s own declaration for why the form matters.
302
+ */
303
+ interface Component<P = Record<string, any>, S = any, M = any> {
304
+ /**
305
+ * A component builds this member from the template's `onEvent` handlers, delegating them
306
+ * through the data attributes they are rendered with. Overriding it replaces them: call
307
+ * the inherited member and merge its result to keep them, or leave it out to use
308
+ * declarative delegation alone.
309
+ *
310
+ * `Component` installs it as a method, so merging the inherited handlers means calling
311
+ * that function with the component as `this`: `super.events` from a getter in the class
312
+ * body, or `Component.prototype.events` from an override on the prototype or through
313
+ * `extend`. Either one needs a cast to the function form, which the declared value or
314
+ * function union does not narrow on its own.
315
+ */
316
+ events?: Resolvable<Record<string, string | Function>>;
317
+ }
318
+
319
+ /**
320
+ * The module's default export is a component created from `<div></div>`, not the class, so it
321
+ * is declared as a value of the constructor's shape. The type alias gives the same name the
322
+ * instance type, as a class declaration would.
323
+ */
284
324
  declare const _default: typeof Component;
325
+ type _default<P = Record<string, any>, S = any, M = any> = Component<P, S, M>;
326
+
285
327
  export default _default;
286
328
  export { Component };
package/types/Model.d.ts CHANGED
@@ -34,7 +34,7 @@ export type ModelEvents<A> =
34
34
  * }
35
35
  * }
36
36
  */
37
- export default class Model<A = any> extends Emitter<ModelEvents<A>> {
37
+ declare class Model<A = any> extends Emitter<ModelEvents<A>> {
38
38
  /**
39
39
  * Static property that defines a prefix for generated getters/setters.
40
40
  * When set, all attribute properties will be prefixed (e.g., 'attr_name' instead of 'name').
@@ -42,14 +42,6 @@ export default class Model<A = any> extends Emitter<ModelEvents<A>> {
42
42
  */
43
43
  static attributePrefix: string;
44
44
 
45
- /**
46
- * Default attributes for the model, merged into `this.attributes` during construction.
47
- * Can be a plain object, or a function (called bound to the instance) that returns the
48
- * defaults. Assign it on the prototype, or via `this.defaults` inside
49
- * `preinitialize`.
50
- */
51
- defaults?: Partial<A> | (() => Partial<A>);
52
-
53
45
  /** Primary data object holding the model attributes. */
54
46
  attributes: A;
55
47
 
@@ -121,3 +113,21 @@ export default class Model<A = any> extends Emitter<ModelEvents<A>> {
121
113
  */
122
114
  toJSON(): A;
123
115
  }
116
+
117
+ /**
118
+ * Declared apart from the class body so a subclass can provide `defaults` as a getter: a
119
+ * getter lives on the prototype, where it is in place before the constructor reads the
120
+ * member, and TypeScript rejects an accessor that overrides a member declared in a base
121
+ * class body.
122
+ */
123
+ interface Model<A = any> {
124
+ /**
125
+ * Default attributes for the model, merged into `this.attributes` during construction.
126
+ * An object, or a function returning one, called bound to the model. Provide it on the
127
+ * prototype, as a getter, or via `this.defaults` inside `preinitialize`: the constructor
128
+ * reads it before a class field would be assigned.
129
+ */
130
+ defaults?: Partial<A> | (() => Partial<A>);
131
+ }
132
+
133
+ export default Model;
package/types/View.d.ts CHANGED
@@ -1,13 +1,29 @@
1
1
  import Emitter from './Emitter.js';
2
2
 
3
- export interface ViewOptions<M = any> {
4
- el?: HTMLElement | (() => HTMLElement);
5
- tag?: string | (() => string);
6
- attributes?: Record<string, any> | (() => Record<string, any>);
7
- events?: Record<string, string | Function> | (() => Record<string, string | Function>);
3
+ /** A value provided directly, or as a function returning it (called bound to the view). */
4
+ export type Resolvable<T> = T | (() => T);
5
+
6
+ /**
7
+ * A `Resolvable` on the options side, where the function form is called with the view as
8
+ * `this`. It carries `this` explicitly because an option has no assignment target for
9
+ * TypeScript to infer it from, unlike the matching member on the instance.
10
+ */
11
+ export type ResolvableOption<T, V> = T | ((this: V) => T);
12
+
13
+ /**
14
+ * Options merged into the view instance. `V` is the instance the function forms are called
15
+ * with as `this`, so a subclass with its own options (see `ComponentReservedOptions`) can
16
+ * substitute its own instance type.
17
+ */
18
+ export interface ViewOptions<M = any, V = View<M>> {
19
+ el?: ResolvableOption<HTMLElement, V>;
20
+ tag?: ResolvableOption<string, V>;
21
+ attributes?: ResolvableOption<Record<string, any>, V>;
22
+ events?: ResolvableOption<Record<string, string | Function>, V>;
8
23
  model?: M;
9
- template?: (...args: any[]) => string;
10
- onDestroy?: (...args: any[]) => void;
24
+ /** Function for the view's own `render` to call. `View` never reads it itself. */
25
+ template?: (this: V, ...args: any[]) => any;
26
+ onDestroy?: (this: V, ...args: any[]) => void;
11
27
  }
12
28
 
13
29
  /**
@@ -36,7 +52,7 @@ export interface ViewOptions<M = any> {
36
52
  * }
37
53
  * }
38
54
  */
39
- export default class View<M = any> extends Emitter {
55
+ declare class View<M = any> extends Emitter {
40
56
  /**
41
57
  * Counter for generating unique IDs for view instances.
42
58
  * For server-side rendering, reset it to `0` on every request (see `resetUid`) so the
@@ -58,23 +74,23 @@ export default class View<M = any> extends Emitter {
58
74
  */
59
75
  static resetUid(): void;
60
76
 
61
- /** Root DOM element of the view. */
77
+ /**
78
+ * Root DOM element of the view. `ensureElement` resolves it when the view is created,
79
+ * so the member reads as the element. To provide it lazily, pass the function form as
80
+ * the `el` option, which is where the declaration carries it.
81
+ */
62
82
  el: HTMLElement;
63
83
 
64
84
  /** A model or any object containing data and business logic. */
65
85
  model?: M;
66
86
 
67
- /** Tag used to create the root element when `el` is not provided (default `div`). */
68
- tag?: string | (() => string);
69
-
70
- /** Attributes used to create the root element when `el` is not provided. */
71
- attributes?: Record<string, any> | (() => Record<string, any>);
72
-
73
- /** Declarative DOM event listeners in the form `{'event selector': listener}`. */
74
- events?: Record<string, string | Function> | (() => Record<string, string | Function>);
75
-
76
- /** Function returning the view's inner HTML, used by `render`. */
77
- template?: (...args: any[]) => string;
87
+ /**
88
+ * Function for your own `render` to call. A view is render-agnostic: `View` never reads
89
+ * this member, so what it returns is whatever your `render` does with it — hence `any`.
90
+ * Declared as a method, the form it always takes; the function can also be assigned to
91
+ * the instance, to the prototype or as a class field.
92
+ */
93
+ template?(...args: any[]): any;
78
94
 
79
95
  /** Unique identifier for the view instance. */
80
96
  uid: string;
@@ -179,3 +195,37 @@ export default class View<M = any> extends Emitter {
179
195
  */
180
196
  render(): this;
181
197
  }
198
+
199
+ /**
200
+ * Members the view resolves while it is being created, declared apart from the class body so
201
+ * a subclass can provide them as a getter: a getter lives on the prototype, where it is in
202
+ * place before the constructor reads the member, and TypeScript rejects an accessor that
203
+ * overrides a member declared in a base class body.
204
+ */
205
+ interface View<M = any> {
206
+ /**
207
+ * Tag used to create the root element when `el` is not provided (default `div`).
208
+ * A string, or a function returning one, called bound to the view. Provide it on the
209
+ * prototype, as a getter, or via `this.tag` inside `preinitialize`: the constructor reads
210
+ * it before a class field would be assigned. The `tag` option still overrides it.
211
+ */
212
+ tag?: Resolvable<string>;
213
+
214
+ /**
215
+ * Attributes used to create the root element when `el` is not provided.
216
+ * An object, or a function returning one, called bound to the view. Provide it on the
217
+ * prototype, as a getter, or via `this.attributes` inside `preinitialize`: the constructor
218
+ * reads it before a class field would be assigned. The `attributes` option still overrides it.
219
+ */
220
+ attributes?: Resolvable<Record<string, any>>;
221
+
222
+ /**
223
+ * Declarative DOM event listeners in the form `{'event selector': listener}`.
224
+ * An object, or a function returning one, called bound to the view. Provide it on the
225
+ * prototype, as a getter, or via `this.events` inside `preinitialize`: `delegateEvents`
226
+ * reads it before a class field would be assigned. The `events` option still overrides it.
227
+ */
228
+ events?: Resolvable<Record<string, string | Function>>;
229
+ }
230
+
231
+ export default View;
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { default as Emitter, EventMap } from './Emitter.js';
2
2
  export { default as Model, ModelEvents, ModelAttrs } from './Model.js';
3
- export { default as View, ViewOptions } from './View.js';
3
+ export { default as View, ViewOptions, Resolvable, ResolvableOption } from './View.js';
4
4
  export {
5
5
  default as Component,
6
6
  ComponentOptions,