rasti 2.1.0-alpha.0 → 3.0.0-alpha.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/dist/rasti.js CHANGED
@@ -1,109 +1,1413 @@
1
- /*
2
- * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
3
- * This devtool is neither made for production nor for readable output files.
4
- * It uses "eval()" calls to create a separate source file in the browser devtools.
5
- * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
6
- * or disable the default devtool with "devtool: false".
7
- * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
8
- */
9
- (function webpackUniversalModuleDefinition(root, factory) {
10
- if(typeof exports === 'object' && typeof module === 'object')
11
- module.exports = factory();
12
- else if(typeof define === 'function' && define.amd)
13
- define([], factory);
14
- else if(typeof exports === 'object')
15
- exports["Rasti"] = factory();
16
- else
17
- root["Rasti"] = factory();
18
- })(self, () => {
19
- return /******/ (() => { // webpackBootstrap
20
- /******/ "use strict";
21
- /******/ var __webpack_modules__ = ({
22
-
23
- /***/ "./src/Component.js":
24
- /*!**************************!*\
25
- !*** ./src/Component.js ***!
26
- \**************************/
27
- /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
28
-
29
- eval("\n\nexports.__esModule = true;\nexports[\"default\"] = void 0;\nvar _View2 = _interopRequireDefault(__webpack_require__(/*! ./View.js */ \"./src/View.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nfunction _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n// This options keys will be extended on view instance.\nvar componentOptions = {\n key: true,\n state: true,\n onCreate: true,\n onChange: true,\n onRender: true\n};\n\n/*\n * Helper function. Extract attributes from html tag\n * @param text {string} html text\n * @return {object} Object with keys / values representing attributes.\n */\nvar extractAttributes = function extractAttributes(text) {\n var attributes = {};\n var re = /([\\w|data-]+)(?:=[\"']?((?:.(?![\"']?\\s+(?:\\S+)=|\\s*\\/?[>\"']))+.)[\"']?)?/g;\n var result;\n while ((result = re.exec(text)) !== null) {\n attributes[result[1]] = typeof result[2] === 'undefined' ? true : result[2];\n }\n return attributes;\n};\n\n/*\n * Helper function. If first arg is a placeholder for an expression, return the expression.\n */\nvar getExpression = function getExpression(placeholder, expressions) {\n var match = placeholder && placeholder.match && placeholder.match(new RegExp(Component.EXPRESSION_PLACEHOLDER_TEMPLATE('(\\\\d+)')));\n return match && match[1] ? expressions[match[1]] : placeholder;\n};\n\n/*\n * Helper function. If expression is a function, call it with context and args.\n */\nvar evalExpression = function evalExpression(expression, context) {\n for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n args[_key - 2] = arguments[_key];\n }\n return typeof expression === 'function' ? expression.apply(context, args) : expression;\n};\n\n/**\n * Components are a special kind of `View` that is designed to be easily composable, \n * making it simple to add child views and build complex user interfaces. \n * Unlike views, which are render-agnostic, components have a specific set of rendering \n * guidelines that allow for a more declarative development style. \n * Components are defined with the `create` static method, which takes a tagged template.\n * @module\n * @extends Rasti.View\n * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.\n * @property {string} key A unique key to identify the component. Used to recycle child components.\n * @property {object} model A `Rasti.Model` or any emitter object containing data and business logic.\n * @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state.\n * @example\n * import { Component, Model } from 'rasti';\n * // Create Timer component.\n * const Timer = Component.create`\n * <div>\n * Seconds: <span>${({ model }) => model.seconds}</span>\n * </div>\n * `;\n * // Create model to store seconds.\n * const model = new Model({ seconds: 0 });\n * // Mount timer on body.\n * Timer.mount({ model }, document.body);\n * // Increment `model.seconds` every second.\n * setInterval(() => model.seconds++, 1000);\n */\nvar Component = exports[\"default\"] = /*#__PURE__*/function (_View) {\n function Component() {\n var _this;\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n _this = _View.apply(this, arguments) || this;\n // Extend \"this\" with options, mapping componentOptions keys.\n Object.keys(options).forEach(function (key) {\n if (componentOptions[key]) _this[key] = options[key];\n });\n // Store options by default.\n _this.options = options;\n // Bind onChange to this to be used as listener.\n // Store bound version, so it can be removed on onDestroy method.\n _this.onChange = _this.onChange.bind(_this);\n // Listen to model changes and call onChange.\n if (_this.model && _this.model.on) _this.model.on('change', _this.onChange);\n if (_this.state && _this.state.on) _this.state.on('change', _this.onChange);\n // Call life cycle method.\n _this.onCreate.apply(_this, arguments);\n return _this;\n }\n\n /*\n * Tell if component is a container.\n * In which case, it will not have an element by itself.\n * It will render a single expression which is expected to return a single component as child.\n * `this.el` will be a reference to that child component's element.\n * @return {boolean}\n */\n _inheritsLoose(Component, _View);\n var _proto = Component.prototype;\n _proto.isContainer = function isContainer() {\n return !!(!this.tag && this.template && this.template.inner && this.template.expressions && this.template.expressions.length === 1);\n }\n\n /*\n * Override. We don't want to ensure an element on instantiation.\n * We will provide it later.\n */;\n _proto.ensureElement = function ensureElement() {\n // If el is provided, delegate events.\n if (this.el) {\n this.delegateEvents();\n this.id = this.el.id;\n }\n // Ensure id.\n if (!this.isContainer() && !this.id) {\n this.id = this.attributes && this.attributes.id ?\n // If id is provided, evaluate it.\n evalExpression(this.attributes.id, this, this) :\n // Generate a unique id and set it as id attribute.\n Component.ID_TEMPLATE(this.uid);\n }\n }\n\n /*\n * Find view's element on parent node, using id.\n * @param parent {node} The parent node.\n * @return {node} The component's element.\n */;\n _proto.findElement = function findElement(parent) {\n return (parent || document).querySelector(\"#\" + this.id);\n }\n\n /*\n * Eval attributes expressions.\n * @return {object} Object containing add, remove and html properties.\n */;\n _proto.getAttributes = function getAttributes() {\n var _this2 = this;\n var add = {\n id: this.id\n };\n var remove = {};\n var html = [\"id=\\\"\" + this.id + \"\\\"\"];\n if (this.attributes) {\n Object.keys(this.attributes).forEach(function (key) {\n if (key === 'id') return;\n // Evaluate attribute value.\n var value = evalExpression(_this2.attributes[key], _this2, _this2);\n\n // Transform bool attribute values\n if (value === false) {\n remove[key] = true;\n } else if (value === true) {\n add[key] = '';\n html.push(key);\n } else {\n if (value === null || typeof value === 'undefined') value = '';\n add[key] = value;\n html.push(key + \"=\\\"\" + value + \"\\\"\");\n }\n });\n }\n return {\n add: add,\n remove: remove,\n html: html.join(' ')\n };\n }\n\n /*\n * Used internally on the render process.\n * Attach the view to the dom element.\n * @param parent {node} The parent node.\n * @return {Rasti.Component} The component instance.\n */;\n _proto.hydrate = function hydrate(parent) {\n var _this3 = this;\n if (!this.isContainer()) {\n this.el = this.findElement(parent);\n this.delegateEvents();\n this.children.forEach(function (child) {\n return child.hydrate(_this3.el);\n });\n } else {\n this.children[0].hydrate(parent);\n this.el = this.children[0].el;\n }\n // Call `onRender` lifecycle method.\n this.onRender.call(this, 'hydrate');\n // Return `this` for chaining.\n return this;\n }\n\n /*\n * Used internally on the render process.\n * Reuse a view that has `key` when its parent is rendered.\n * @param parent {node} The parent node.\n * @return {Rasti.Component} The component instance.\n */;\n _proto.recycle = function recycle(parent) {\n // Find element to be replaced. It has same id.\n var toBeReplaced = (this.isContainer() ? this.children[0] : this).findElement(parent);\n // Replace it with this.el.\n toBeReplaced.replaceWith(this.el);\n // Call `onRender` lifecycle method.\n this.onRender.call(this, 'recycle');\n // Return `this` for chaining.\n return this;\n }\n\n /*\n * Override. Add some custom logic to super `destroy` method.\n * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.\n */;\n _proto.destroy = function destroy() {\n _View.prototype.destroy.apply(this, arguments);\n // Stop listening to `change`.\n if (this.model && this.model.off) this.model.off('change', this.onChange);\n if (this.state && this.state.off) this.state.off('change', this.onChange);\n // Set destroyed flag to prevent a last render after destroyed.\n this.destroyed = true;\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Lifecycle method. Called when the view is created at the end of the constructor.\n * @param options {object} The view options.\n */;\n _proto.onCreate = function onCreate() {}\n\n /**\n * Lifecycle method. Called when model emits `change` event.\n * By default calls render method.\n * This method should be extended with custom logic.\n * Maybe comparing new attributes with previous ones and calling\n * render when needed. Or doing some dom transformation.\n * @param model {Rasti.Model} The model that emitted the event.\n * @param changed {object} Object containing keys and values that has changed.\n * @param [...args] {any} Any extra arguments passed to set method.\n */;\n _proto.onChange = function onChange() {\n this.render();\n }\n\n /**\n * Lifecycle method. Called when the view is rendered.\n * @param type {string} The render type. Can be `render`, `hydrate` or `recycle`.\n */;\n _proto.onRender = function onRender() {}\n\n /**\n * Lifecycle method. Called when the view is destroyed.\n * @param {object} options Options object or any arguments passed to `destroy` method.\n */;\n _proto.onDestroy = function onDestroy() {}\n\n /*\n * Replace expressions.\n */;\n _proto.replaceExpressions = function replaceExpressions(string, addChild) {\n var _this4 = this;\n return string.replace(new RegExp(Component.EXPRESSION_PLACEHOLDER_TEMPLATE('(\\\\d+)'), 'g'), function (match) {\n var expression = getExpression(match, _this4.template.expressions);\n // Eval expression. Pass view as argument.\n var result = evalExpression(expression, _this4, _this4);\n // Treat all expressions as arrays.\n var results = result instanceof Array ? result : [result];\n // Replace expression with the result of the evaluation.\n return results.reduce(function (out, result) {\n var parsed;\n // If result is true, replace it with a placeholder.\n if (result === true) parsed = Component.TRUE_PLACEHOLDER;\n // If result is false, replace it with a placeholder.\n else if (result === false) parsed = Component.FALSE_PLACEHOLDER;\n // Replace null or undefined with empty string.\n else if (result === null || typeof result === 'undefined') parsed = '';\n // If result is a view, call addChild callback.\n else if (result && typeof result.render === 'function') parsed = addChild(result);\n // Return expression itself.\n else parsed = result;\n // Concatenate expressions.\n return out + parsed;\n }, '');\n })\n // Replace `attribute=\"true\"` with `attribute` and `attribute=\"false\"` with empty string.\n .replace(new RegExp(\"([\\\\w|data-]+)=([\\\"'])?(\" + Component.TRUE_PLACEHOLDER + \"|\" + Component.FALSE_PLACEHOLDER + \")\\\\2\", 'g'), function (match, attribute, quote, placeholder) {\n return placeholder === Component.TRUE_PLACEHOLDER ? attribute : '';\n })\n // Replace rest of true or false expressions with empty string.\n .replace(new RegExp(Component.TRUE_PLACEHOLDER + \"|\" + Component.FALSE_PLACEHOLDER, 'g'), '');\n }\n\n /*\n * Treat the whole view as a HTML string.\n */;\n _proto.toString = function toString() {\n var _this5 = this;\n // Normally there won't be any children, but if there are, destroy them.\n this.destroyChildren();\n // Replace expressions of inner template.\n var close = this.template && this.template.inner;\n var inner = close && this.replaceExpressions(this.template.inner, function (component) {\n // Add child component.\n return _this5.addChild(component);\n });\n // If component is a container, return inner which will be an expression thats return a single component.\n if (this.isContainer()) return inner;\n // Get tag name.\n var tag = this.tag || 'div';\n // Get attributes.\n var attributes = this.getAttributes().html;\n // Generate outer template.\n return close ? \"<\" + tag + \" \" + attributes + \">\" + inner + \"</\" + tag + \">\" : \"<\" + tag + \" \" + attributes + \" />\";\n }\n\n /*\n * View render method.\n */;\n _proto.render = function render() {\n var _this6 = this;\n // Prevent a last re render if view is already destroyed.\n if (this.destroyed) return this;\n if (!this.isContainer()) {\n // If `this.el` is not present, create a new `this.tag` element.\n if (!this.el) {\n this.el = this.createElement(this.tag);\n this.delegateEvents();\n }\n // Set `this.el` attributes.\n var attributes = this.getAttributes();\n // Remove attributes.\n Object.keys(attributes.remove).forEach(function (key) {\n _this6.el.removeAttribute(key);\n });\n // Add attributes.\n Object.keys(attributes.add).forEach(function (key) {\n _this6.el.setAttribute(key, attributes.add[key]);\n });\n }\n // Check for `template.inner` to see if view has innerHTML.\n if (this.template && this.template.inner) {\n // Store active element.\n var activeElement = document.activeElement;\n var nextChildren = [];\n var recycledChildren = [];\n var previousChildren = this.children;\n this.children = [];\n // Replace expressions. Set html inside of `this.el`.\n var inner = this.replaceExpressions(this.template.inner, function (component) {\n var out = component;\n // Check if child already exists.\n var found = component.key && previousChildren.find(function (previousChild) {\n return previousChild.key === component.key;\n });\n if (found) {\n var tag = found.el.tagName.toLowerCase();\n var id = found.el.id;\n // If child already exists, replace it html by its root element.\n out = \"<\" + tag + \" id=\\\"\" + id + \"\\\"></\" + tag + \">\";\n // Add child to recycled children.\n recycledChildren.push(found);\n // Destroy new child component. Use recycled one instead.\n component.destroy();\n } else {\n // Not found. Add new child component.\n nextChildren.push(component);\n }\n // Component html.\n return out;\n });\n if (this.isContainer()) {\n if (nextChildren[0]) {\n var fragment = this.createElement('template');\n fragment.innerHTML = inner;\n // Add new child to dom fragment and hydrate it.\n this.addChild(nextChildren[0]).hydrate(fragment.content);\n // Get next child element.\n var nextEl = fragment.content.children[0];\n // If `this.el` is present, replace it with nextEl.\n if (this.el) this.el.replaceWith(nextEl);\n // Set `this.el` to nextEl.\n this.el = nextEl;\n } else if (recycledChildren[0]) {\n this.addChild(recycledChildren[0]);\n }\n } else {\n this.el.innerHTML = inner;\n // Add new children. Hydrate them.\n nextChildren.forEach(function (nextChild) {\n _this6.addChild(nextChild).hydrate(_this6.el);\n });\n // Replace children root elements with recycled components.\n recycledChildren.forEach(function (recycledChild) {\n _this6.addChild(recycledChild).recycle(_this6.el);\n });\n }\n // Destroy unused children.\n previousChildren.forEach(function (previousChild) {\n var found = recycledChildren.indexOf(previousChild) > -1;\n if (!found) previousChild.destroy();\n });\n // Restore focus.\n if (this.el.contains(activeElement)) {\n activeElement.focus();\n }\n }\n // Call onRender lifecycle method.\n this.onRender.call(this, 'render');\n // Return this for chaining.\n return this;\n }\n\n /**\n * Helper method used to extend a `Component`, creating a subclass.\n * @static\n * @param {object} object Object containing methods to be added to the new `Component` subclass. Also can be a function that receives the parent prototype and returns an object.\n */;\n Component.extend = function extend(object) {\n var Current = this;\n var Extended = /*#__PURE__*/function (_Current) {\n function Extended() {\n return _Current.apply(this, arguments) || this;\n }\n _inheritsLoose(Extended, _Current);\n return Extended;\n }(Current);\n Object.assign(Extended.prototype, typeof object === 'function' ? object(Current.prototype) : object);\n return Extended;\n }\n\n /**\n * Mount the component into the dom.\n * It instantiate the Component view using options, \n * appends its element into the DOM (if `el` is provided).\n * And returns the view instance.\n * @static\n * @param {object} options The view options.\n * @param {node} el Dom element to append the view element.\n * @param {boolean} hydrate If true, the view will use existing html.\n * @return {Rasti.Component}\n */;\n Component.mount = function mount(options, el, hydrate) {\n // Instantiate view.\n var view = new this(options);\n // If `el` is passed, mount component.\n if (el) {\n if (hydrate) {\n view.toString();\n } else {\n var fragment = this.prototype.createElement('template');\n // Add html text into element inner html.\n fragment.innerHTML = view;\n // Add to dom.\n el.appendChild(fragment.content.children[0]);\n }\n view.hydrate(el);\n }\n // Return view instance.\n return view;\n }\n\n /**\n * Takes a tagged template containing an HTML string, \n * and returns a new `Component` class.\n * - The template outer tag and attributes will be used to create the view's root element.\n * - Boolean attributes should be passed in the form of `attribute=\"${() => true}\"`.\n * - Event handlers should be passed, at the root element, in the form of `onEventName=${{'selector' : listener }}`. Where `selector` is a css selector. The event will be delegated to the view's root element.\n * - The template inner HTML will be used as the view's template.\n * - Template interpolations that are functions will be evaluated on the render process. Receiving the view instance as argument. And being bound to it.\n * - If the function returns `null`, `undefined`, `false` or empty string, the interpolation won't render any content.\n * - If the function returns a component instance, it will be added as a child component.\n * - If the function returns an array, each item will be evaluated as above.\n * @static\n * @param {string} HTML template for the component. Or a function that mounts a sub component.\n * @return {Rasti.Component}\n */;\n Component.create = function create(strings) {\n for (var _len2 = arguments.length, expressions = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n expressions[_key2 - 1] = arguments[_key2];\n }\n // Containers can be created using create as a functions instead of a tagged template.\n if (typeof strings === 'function') {\n expressions = [strings];\n strings = ['', ''];\n }\n var tag, attributes, inner, events;\n var parts = [];\n // Replace functions and objects interpolations with `{number}`.\n // Where `number` is the index on expressions array.\n strings.forEach(function (string, i) {\n // Add string part.\n parts.push(string);\n // Add expression placeholder for later or expression eval.\n if (expressions[i]) {\n parts.push(typeof expressions[i] === 'function' || typeof expressions[i] === 'object' ? Component.EXPRESSION_PLACEHOLDER_TEMPLATE(i) : expressions[i]);\n }\n });\n // Create output text for main template.\n var main = parts.join('').trim().replace(/\\n/g, '');\n // Extract outer tag, attributes and inner html.\n var result = main.match(/^<([a-z]+[1-6]?)(.*?)>(.*)<\\/\\1>$/) || main.match(/^<([a-z]+)(.*?)\\/>$/);\n if (result) {\n tag = result[1];\n inner = result[3];\n // Parse attributes from html text into an object.\n attributes = extractAttributes(result[2]);\n // Events to be delegated.\n events = {};\n // Filter events. To generate events object.\n // Generate attributes object, replace placeholders with expressions.\n attributes = Object.keys(attributes).reduce(function (out, key) {\n // Is Event?\n var matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/);\n // Is placeholder for function or object?\n // Get expression or value.\n var value = getExpression(attributes[key], expressions);\n // Is event handler. Add to events object.\n if (matchKey && matchKey[1]) {\n var eventType = matchKey[1].toLowerCase();\n Object.keys(value).forEach(function (selector) {\n return events[\"\" + eventType + (selector === '&' ? '' : \" \" + selector)] = value[selector];\n });\n return out;\n }\n // Is attribute. Add to attributes object.\n out[key] = value;\n return out;\n }, {});\n } else inner = main;\n var Current = this;\n // Create subclass for this component.\n return Current.extend({\n // Set events.\n events: events,\n // Set attributes.\n attributes: attributes,\n // Set template.\n template: {\n // Template for innerHTML of root element.\n inner: inner,\n // Template expressions.\n expressions: expressions\n },\n // Set root element tag.\n tag: tag\n });\n };\n return Component;\n}(_View2[\"default\"]);\nComponent.ID_TEMPLATE = function (uid) {\n return \"rasti-component-\" + uid;\n};\nComponent.EXPRESSION_PLACEHOLDER_TEMPLATE = function (idx) {\n return \"__RASTI_EXPRESSION{\" + idx + \"}\";\n};\nComponent.TRUE_PLACEHOLDER = '__RASTI_TRUE';\nComponent.FALSE_PLACEHOLDER = '__RASTI_FALSE';\n\n//# sourceURL=webpack://Rasti/./src/Component.js?");
30
-
31
- /***/ }),
32
-
33
- /***/ "./src/Emitter.js":
34
- /*!************************!*\
35
- !*** ./src/Emitter.js ***!
36
- \************************/
37
- /***/ ((__unused_webpack_module, exports) => {
38
-
39
- eval("\n\nexports.__esModule = true;\nexports[\"default\"] = void 0;\n/**\n * `Emitter` is a class that provides an easy way to implement the observer pattern \n * in your applications. \n * It can be extended to create new classes that have the ability to emit and bind custom named events. \n * Emitter is used by `Model` and `View` classes, which inherit from it to implement \n * event-driven functionality.\n *\n * @module\n * @example\n * import { Emitter } from 'rasti';\n * // Custom cart\n * class ShoppingCart extends Emitter {\n * constructor() {\n * super();\n * this.items = [];\n * }\n *\n * addItem(item) {\n * this.items.push(item);\n * // Emit a custom event called `itemAdded`.\n * // Pass the added item as an argument to the event listener.\n * this.emit('itemAdded', item);\n * }\n * }\n * // Create an instance of ShoppingCart and Logger\n * const cart = new ShoppingCart();\n * // Listen to the `itemAdded` event and log the added item using the logger.\n * cart.on('itemAdded', (item) => {\n * console.log(`Item added to cart: ${item.name} - Price: $${item.price}`);\n * });\n * // Simulate adding items to the cart\n * const item1 = { name : 'Smartphone', price : 1000 };\n * const item2 = { name : 'Headphones', price : 150 };\n *\n * cart.addItem(item1); // Output: \"Item added to cart: Smartphone - Price: $1000\"\n * cart.addItem(item2); // Output: \"Item added to cart: Headphones - Price: $150\"\n */\nvar Emitter = exports[\"default\"] = /*#__PURE__*/function () {\n function Emitter() {}\n var _proto = Emitter.prototype;\n /**\n * Adds event listener.\n * @param {string} type Type of the event (e.g. `change`).\n * @param {function} listener Callback function to be called when the event is emitted.\n * @example\n * this.model.on('change', this.render.bind(this)); // Re render when model changes.\n */\n _proto.on = function on(type, listener) {\n if (typeof listener !== 'function') {\n throw TypeError('Listener must be a function');\n }\n if (!this.listeners) this.listeners = {};\n if (!this.listeners[type]) this.listeners[type] = [];\n this.listeners[type].push(listener);\n }\n\n /**\n * Adds event listener that executes once.\n * @param {string} type Type of the event (e.g. `change`).\n * @param {function} listener Callback function to be called when the event is emitted.\n * @example\n * this.model.once('change', () => console.log('This will happen once'));\n */;\n _proto.once = function once(type, _listener2) {\n if (typeof _listener2 === 'function') {\n var self = this;\n var _listener = _listener2;\n _listener2 = function listener() {\n _listener.apply(void 0, arguments);\n self.off(type, _listener2);\n };\n }\n this.on(type, _listener2);\n }\n\n /**\n * Removes event listeners.\n * @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.\n * @param {function} [listener] Callback function to be called when the event is emitted. If listener is not provided, it removes all listeners for specified type.\n * @example\n * this.model.off('change'); // Stop listening to changes.\n */;\n _proto.off = function off(type, listener) {\n if (!type) {\n this.listeners = {};\n } else {\n if (!listener) {\n delete this.listeners[type];\n } else {\n var listeners = this.listeners[type];\n if (listeners) {\n var copy = listeners.slice();\n copy.forEach(function (fn, idx) {\n if (fn === listener) listeners.splice(idx, 1);\n });\n if (!listeners.length) {\n delete this.listeners[type];\n }\n }\n }\n }\n }\n\n /**\n * Emits event of specified type. Listeners will receive specified arguments.\n * @param {string} type Type of the event (e.g. `change`).\n * @param {any} [...args] Arguments to be passed to listener.\n * @example\n * this.emit('invalid'); // Emit validation error event.\n */;\n _proto.emit = function emit(type) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n var listeners = this.listeners && this.listeners[type];\n if (!listeners || !listeners.length) return;\n var copy = listeners.slice();\n copy.forEach(function (fn) {\n fn.apply(void 0, args);\n });\n };\n return Emitter;\n}();\n\n//# sourceURL=webpack://Rasti/./src/Emitter.js?");
40
-
41
- /***/ }),
42
-
43
- /***/ "./src/Model.js":
44
- /*!**********************!*\
45
- !*** ./src/Model.js ***!
46
- \**********************/
47
- /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
48
-
49
- eval("\n\nexports.__esModule = true;\nexports[\"default\"] = void 0;\nvar _Emitter2 = _interopRequireDefault(__webpack_require__(/*! ./Emitter.js */ \"./src/Emitter.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nfunction _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n/**\n * - Orchestrates data and business logic.\n * - Emits events when data changes.\n * \n * A `Model` manages an internal table of data attributes and triggers change events when any of its data is modified. \n * Models may handle syncing data with a persistence layer. To design your models, create atomic, reusable objects \n * that contain all the necessary functions for manipulating their specific data. \n * Models should be easily passed throughout your app and used anywhere the corresponding data is needed. \n * Rasti models stores its attributes in `this.attributes`, which is extended from `this.defaults` and the \n * constructor `attrs` parameter. For every attribute, a getter is generated to retrieve the model property \n * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change` \n * and `change:attribute` events.\n * @module\n * @extends Rasti.Emitter\n * @param {object} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.\n * @example\n * import { Model } from 'rasti';\n * // Product model\n * class ProductModel extends Model {\n * preinitialize() {\n * // The Product model has `name` and `price` default attributes.\n * // `defaults` will extend `this.attributes`.\n * // Getters and setters are generated for `this.attributes`,\n * // in order to emit `change` events.\n * this.defaults = {\n * name: '',\n * price: 0\n * };\n * }\n *\n * setDiscount(discountPercentage) {\n * // Apply a discount to the price property.\n * // This will call a setter that will update `price` in `this.attributes`,\n * // and emit `change` and `change:price` events.\n * const discount = this.price * (discountPercentage / 100);\n * this.price -= discount;\n * }\n * }\n * // Create a product instance with a name and price.\n * const product = new ProductModel({ name: 'Smartphone', price: 1000 });\n * // Listen to the `change:price` event.\n * product.on('change:price', () => console.log('New Price:', product.price));\n * // Apply a 10% discount to the product.\n * product.setDiscount(10); // Output: \"New Price: 900\"\n */\nvar Model = exports[\"default\"] = /*#__PURE__*/function (_Emitter) {\n function Model() {\n var _this;\n var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n _this = _Emitter.call(this) || this;\n // Call preinitialize.\n _this.preinitialize.apply(_this, arguments);\n // attributes object.\n _this.attributes = Object.assign({}, _this.defaults || {}, attrs);\n // Previous attributes.\n _this.previous = {};\n // Generate getters/setters for every attr.\n Object.keys(_this.attributes).forEach(_this.defineAttribute.bind(_this));\n return _this;\n }\n\n /**\n * If you define a preinitialize method, it will be invoked when the Model is first created, before any instantiation logic is run for the Model.\n * @param {object} attrs Object containing model attributes to extend `this.attributes`.\n */\n _inheritsLoose(Model, _Emitter);\n var _proto = Model.prototype;\n _proto.preinitialize = function preinitialize() {}\n\n /**\n * Generate getter/setter for the given key. In order to emit `change` events.\n * This method is called internally by the constructor\n * for `this.attributes`.\n * @param {string} key Attribute key.\n */;\n _proto.defineAttribute = function defineAttribute(key) {\n var _this2 = this;\n Object.defineProperty(this, key, {\n get: function get() {\n return _this2.get(key);\n },\n set: function set(value) {\n _this2.set(key, value);\n }\n });\n }\n\n /**\n * Get an attribute from `this.attributes`.\n * This method is called internally by generated getters.\n * @param {string} key Attribute key.\n * @return {any} The attribute value.\n */;\n _proto.get = function get(key) {\n return this.attributes[key];\n }\n\n /**\n * Set an attribute into `this.attributes`. \n * Emit `change` and `change:attribute` if a value change. \n * Could be called in two forms, `this.set('key', value)` and\n * `this.set({ key : value })`. \n * This method is called internally by generated setters. \n * The `change` event listener will receive the model instance, an object containing the changed attributes, and the rest of the arguments passed to `set` method. \n * The `change:attribute` event listener will receive the model instance, the new attribute value, and the rest of the arguments passed to `set` method.\n * @param {string} key Attribute key or object containing keys/values.\n * @param [value] Attribute value.\n * @return {this} This model.\n * @emits change\n * @emits change:attribute\n */;\n _proto.set = function set(key, value) {\n var _this3 = this;\n var attrs, args;\n // Handle both `\"key\", value` and `{key: value}` style arguments.\n for (var _len = arguments.length, rest = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n rest[_key - 2] = arguments[_key];\n }\n if (typeof key === 'object') {\n attrs = key;\n args = [value].concat(rest);\n } else {\n var _attrs;\n attrs = (_attrs = {}, _attrs[key] = value, _attrs);\n args = rest;\n }\n // Are we in a nested `set` call?\n // Calling a `set` inside a `change:attribute` or `change` event listener\n var changing = this._changing;\n this._changing = true;\n // Store changed attributes.\n var changed = {};\n // Store previous attributes.\n if (!changing) {\n this.previous = Object.assign({}, this.attributes);\n }\n // Set attributes.\n Object.keys(attrs).forEach(function (key) {\n // Use equality to determine if value changed.\n if (attrs[key] !== _this3.attributes[key]) {\n changed[key] = attrs[key];\n _this3.attributes[key] = attrs[key];\n }\n });\n var changedKeys = Object.keys(changed);\n // Pending `change` event arguments.\n if (changedKeys.length) this._pending = ['change', this, changed].concat(args);\n // Emit `change:attribute` events.\n changedKeys.forEach(function (key) {\n _this3.emit.apply(_this3, [\"change:\" + key, _this3, attrs[key]].concat(args));\n });\n // Don't emit `change` event until the end of the nested \n // `set` calls inside `change:attribute` event listeners.\n if (changing) return this;\n // Emit `change` events, that might be nested.\n while (this._pending) {\n var pendingChange = this._pending;\n this._pending = null;\n this.emit.apply(this, pendingChange);\n }\n // Reset flags.\n this._pending = null;\n this._changing = false;\n return this;\n }\n\n /**\n * Return object representation of the model to be used for JSON serialization.\n * By default returns `this.attributes`.\n * @return {object} Object representation of the model to be used for JSON serialization.\n */;\n _proto.toJSON = function toJSON() {\n return this.attributes;\n };\n return Model;\n}(_Emitter2[\"default\"]);\n\n//# sourceURL=webpack://Rasti/./src/Model.js?");
50
-
51
- /***/ }),
52
-
53
- /***/ "./src/View.js":
54
- /*!*********************!*\
55
- !*** ./src/View.js ***!
56
- \*********************/
57
- /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
58
-
59
- eval("\n\nexports.__esModule = true;\nexports[\"default\"] = void 0;\nvar _Emitter2 = _interopRequireDefault(__webpack_require__(/*! ./Emitter.js */ \"./src/Emitter.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nfunction _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n// This options keys will be extended on view instance.\nvar viewOptions = {\n el: true,\n tag: true,\n attributes: true,\n events: true,\n model: true,\n template: true,\n onDestroy: true\n};\n\n/**\n * - Listens for changes and renders 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 the 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 Rasti.Emitter\n * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.\n * @property {node} el Every view has a root element, `this.el`. If not present it will be created.\n * @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`.\n * @property {object} attributes If `this.el` is not present, an element will be created using `this.attributes`.\n * @property {object} events Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.\n * @property {object} model A `Rasti.Model` or any object containing data and business logic.\n * @property {function} template A function that receives data and returns a markup string (html for example).\n * @example\n * import { View } from 'rasti';\n * \n * class Timer extends View {\n * constructor(options) {\n * super(options);\n * // Create model to store internal state. Set `seconds` attribute into 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 body.\n * document.body.appendChild(new Timer().render().el);\n */\nvar View = exports[\"default\"] = /*#__PURE__*/function (_Emitter) {\n function View() {\n var _this;\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n _this = _Emitter.call(this) || this;\n // Call preinitialize.\n _this.preinitialize.apply(_this, arguments);\n // Generate unique id.\n // Useful to generate elements ids.\n _this.uid = \"uid\" + ++View.uid;\n // Store delegated events 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 // Extend \"this\" with options, mapping viewOptions keys.\n Object.keys(options).forEach(function (key) {\n if (viewOptions[key]) _this[key] = options[key];\n });\n // Ensure that the view has a root element at `this.el`.\n _this.ensureElement();\n return _this;\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} attrs Object containing model attributes to extend `this.attributes`.\n */\n _inheritsLoose(View, _Emitter);\n var _proto = View.prototype;\n _proto.preinitialize = function preinitialize() {}\n\n /**\n * Returns the first element 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} Element matching selector within the view's root element (`this.el`).\n */;\n _proto.$ = function $(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 _proto.$$ = function $$(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 {Rasti.View} Return `this` for chaining.\n */;\n _proto.destroy = function destroy() {\n // Call destroy on children.\n this.destroyChildren();\n // Undelegate `this.el` event listeners\n this.undelegateEvents();\n // Unbind `this` events.\n this.off();\n // Call onDestroy lifecycle method\n this.onDestroy.apply(this, arguments);\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * `onDestroy` lifecycle method is called after 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 _proto.onDestroy = function 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 {Rasti.View} child\n * @return {Rasti.View}\n */;\n _proto.addChild = function addChild(child) {\n this.children.push(child);\n return child;\n }\n\n /**\n * Call destroy method on children views.\n */;\n _proto.destroyChildren = function destroyChildren() {\n while (this.children.length) this.children.shift().destroy();\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 constructor.\n * You may override it if you want to use a different logic or to \n * postpone element creation.\n */;\n _proto.ensureElement = function ensureElement() {\n // If \"this.el\" is not present,\n // create a new element according \"this.tag\"\n // and \"this.attributes\".\n if (!this.el) this.el = this.createElement(this.tag, this.attributes);\n // Delegate events on element.\n this.delegateEvents();\n }\n\n /**\n * Create an element.\n * Called from constructor if `this.el` is undefined, to ensure\n * the view to have a root element.\n * @param {string} tag Tag for the element. Default to `div`\n * @param {object} attrs Attributes for the element.\n * @return {node} The created element.\n */;\n _proto.createElement = function createElement() {\n var tag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div';\n var attrs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n // Create dom element.\n var el = document.createElement(tag);\n // Add element attributes.\n Object.keys(attrs).forEach(function (key) {\n return el.setAttribute(key, attrs[key]);\n });\n return el;\n }\n\n /**\n * Remove `this.el` from DOM.\n * @return {Rasti.View} Return `this` for chaining.\n */;\n _proto.removeElement = function 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 hash is not passed directly, uses `this.events` as the source. \n * Events are written in the format `{'event selector' : 'listener'}`. The listener may be either the name of a method on the view, or a direct function body.\n * Omitting the selector causes the event to be bound to the view's root element (`this.el`). \n * By default, `delegateEvents` is called within the View's constructor, \n * so if you have a simple events hash, all of your DOM events will always already be connected, and you will never have to call this function yourself. \n * All attached listeners are bound to the view automatically, so when the listeners are invoked, `this` continues to refer to the view object. \n * When `delegateEvents` is run again, perhaps with a different events hash, all listeners are removed and delegated afresh.\n * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.\n * @return {Rasti.View} Return `this` for chaining.\n * @example\n * MyView.prototype.events = {\n * 'click button.ok' : 'onClickOkButton',\n * 'click button.cancel' : function() {}\n * };\n */;\n _proto.delegateEvents = function delegateEvents() {\n var _this2 = this;\n var events = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.events;\n if (!events) return this;\n if (this.delegatedEventListeners.length) this.undelegateEvents();\n\n // Store events by type i.e.: \"click\", \"submit\", etc.\n var eventTypes = {};\n Object.keys(events).forEach(function (key) {\n var keyParts = key.split(' ');\n var type = keyParts.shift();\n var selector = keyParts.join(' ');\n var listener = events[key];\n // Listener may be a string representing a method name on the view,\n // or a function.\n listener = (typeof listener === 'string' ? _this2[listener] : listener).bind(_this2);\n if (!eventTypes[type]) eventTypes[type] = [];\n eventTypes[type].push({\n selector: selector,\n listener: listener\n });\n });\n Object.keys(eventTypes).forEach(function (type) {\n // Listener for the type of event.\n var typeListener = function typeListener(event) {\n // Iterate and run every individual listener if the selector matches.\n eventTypes[type].forEach(function (_ref) {\n var selector = _ref.selector,\n listener = _ref.listener;\n if (!selector || event.target.closest(selector)) listener(event, _this2);\n });\n };\n _this2.delegatedEventListeners.push({\n type: type,\n listener: typeListener\n });\n _this2.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. Useful if you want to disable or remove a view from the DOM temporarily. Called automatically when the view is destroyed.\n * @return {Rasti.View} Return `this` for chaining.\n */;\n _proto.undelegateEvents = function undelegateEvents() {\n var _this3 = this;\n this.delegatedEventListeners.forEach(function (_ref2) {\n var type = _ref2.type,\n listener = _ref2.listener;\n _this3.el.removeEventListener(type, listener);\n });\n this.delegatedEventListeners = [];\n // Return `this` for chaining.\n return this;\n }\n\n /**\n * Render the view.\n * This method should be overridden with custom logic.\n * The default implementation sets innerHTML of `this.el` with `this.template`.\n * Conventions are to only manipulate the dom in the scope of `this.el`, \n * and to return `this` for chaining.\n * If you added any child view, you must call `this.destroyChildren`.\n * @return {Rasti.View} Return `this` for chaining.\n */;\n _proto.render = function render() {\n if (this.template) this.el.innerHTML = this.template(this.model);\n // Return `this` for chaining.\n return this;\n };\n return View;\n}(_Emitter2[\"default\"]);\n/*\n * Unique Id\n */\nView.uid = 0;\n\n//# sourceURL=webpack://Rasti/./src/View.js?");
60
-
61
- /***/ }),
62
-
63
- /***/ "./src/index.js":
64
- /*!**********************!*\
65
- !*** ./src/index.js ***!
66
- \**********************/
67
- /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
68
-
69
- eval("\n\nexports.__esModule = true;\nvar _Emitter = _interopRequireDefault(__webpack_require__(/*! ./Emitter.js */ \"./src/Emitter.js\"));\nexports.Emitter = _Emitter[\"default\"];\nvar _Model = _interopRequireDefault(__webpack_require__(/*! ./Model.js */ \"./src/Model.js\"));\nexports.Model = _Model[\"default\"];\nvar _View = _interopRequireDefault(__webpack_require__(/*! ./View.js */ \"./src/View.js\"));\nexports.View = _View[\"default\"];\nvar _Component = _interopRequireDefault(__webpack_require__(/*! ./Component.js */ \"./src/Component.js\"));\nexports.Component = _Component[\"default\"];\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//# sourceURL=webpack://Rasti/./src/index.js?");
70
-
71
- /***/ })
72
-
73
- /******/ });
74
- /************************************************************************/
75
- /******/ // The module cache
76
- /******/ var __webpack_module_cache__ = {};
77
- /******/
78
- /******/ // The require function
79
- /******/ function __webpack_require__(moduleId) {
80
- /******/ // Check if module is in cache
81
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
82
- /******/ if (cachedModule !== undefined) {
83
- /******/ return cachedModule.exports;
84
- /******/ }
85
- /******/ // Create a new module (and put it into the cache)
86
- /******/ var module = __webpack_module_cache__[moduleId] = {
87
- /******/ // no module.id needed
88
- /******/ // no module.loaded needed
89
- /******/ exports: {}
90
- /******/ };
91
- /******/
92
- /******/ // Execute the module function
93
- /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
94
- /******/
95
- /******/ // Return the exports of the module
96
- /******/ return module.exports;
97
- /******/ }
98
- /******/
99
- /************************************************************************/
100
- /******/
101
- /******/ // startup
102
- /******/ // Load entry module and return exports
103
- /******/ // This entry module can't be inlined because the eval devtool is used.
104
- /******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
105
- /******/
106
- /******/ return __webpack_exports__;
107
- /******/ })()
108
- ;
109
- });
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Rasti = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * `Emitter` is a class that provides an easy way to implement the observer pattern
9
+ * in your applications.
10
+ * It can be extended to create new classes that have the ability to emit and bind custom named events.
11
+ * Emitter is used by `Model` and `View` classes, which inherit from it to implement
12
+ * event-driven functionality.
13
+ *
14
+ * @module
15
+ * @example
16
+ * import { Emitter } from 'rasti';
17
+ * // Custom cart
18
+ * class ShoppingCart extends Emitter {
19
+ * constructor() {
20
+ * super();
21
+ * this.items = [];
22
+ * }
23
+ *
24
+ * addItem(item) {
25
+ * this.items.push(item);
26
+ * // Emit a custom event called `itemAdded`.
27
+ * // Pass the added item as an argument to the event listener.
28
+ * this.emit('itemAdded', item);
29
+ * }
30
+ * }
31
+ * // Create an instance of ShoppingCart and Logger
32
+ * const cart = new ShoppingCart();
33
+ * // Listen to the `itemAdded` event and log the added item using the logger.
34
+ * cart.on('itemAdded', (item) => {
35
+ * console.log(`Item added to cart: ${item.name} - Price: $${item.price}`);
36
+ * });
37
+ * // Simulate adding items to the cart
38
+ * const item1 = { name : 'Smartphone', price : 1000 };
39
+ * const item2 = { name : 'Headphones', price : 150 };
40
+ *
41
+ * cart.addItem(item1); // Output: "Item added to cart: Smartphone - Price: $1000"
42
+ * cart.addItem(item2); // Output: "Item added to cart: Headphones - Price: $150"
43
+ */
44
+ class Emitter {
45
+ /**
46
+ * Adds event listener.
47
+ * @param {string} type Type of the event (e.g. `change`).
48
+ * @param {function} listener Callback function to be called when the event is emitted.
49
+ * @example
50
+ * this.model.on('change', this.render.bind(this)); // Re render when model changes.
51
+ */
52
+ on(type, listener) {
53
+ // Validate listener.
54
+ if (typeof listener !== 'function') {
55
+ throw new TypeError('Listener must be a function');
56
+ }
57
+ // Create listeners object if it doesn't exist.
58
+ if (!this.listeners) this.listeners = {};
59
+ if (!this.listeners[type]) this.listeners[type] = [];
60
+ // Add listener.
61
+ this.listeners[type].push(listener);
62
+ // Return a function to remove the listener.
63
+ return () => this.off(type, listener);
64
+ }
65
+
66
+ /**
67
+ * Adds event listener that executes once.
68
+ * @param {string} type Type of the event (e.g. `change`).
69
+ * @param {function} listener Callback function to be called when the event is emitted.
70
+ * @example
71
+ * this.model.once('change', () => console.log('This will happen once'));
72
+ */
73
+ once(type, listener) {
74
+ // If listener is a function, wrap it to remove it after it is called.
75
+ if (typeof listener === 'function') {
76
+ const self = this;
77
+ const originalListener = listener;
78
+
79
+ listener = function(...args) {
80
+ originalListener(...args);
81
+ self.off(type, listener);
82
+ };
83
+ }
84
+ // Add listener.
85
+ return this.on(type, listener);
86
+ }
87
+
88
+ /**
89
+ * Removes event listeners.
90
+ * @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.
91
+ * @param {function} [listener] Callback function to be called when the event is emitted. If listener is not provided, it removes all listeners for specified type.
92
+ * @example
93
+ * this.model.off('change'); // Stop listening to changes.
94
+ */
95
+ off(type, listener) {
96
+ // No listeners.
97
+ if (!this.listeners) return;
98
+ // No type provided, remove all listeners.
99
+ if (!type) {
100
+ delete this.listeners;
101
+ return;
102
+ }
103
+ // No listeners for specified type.
104
+ if (!this.listeners[type]) return;
105
+ // No listener provided, remove all listeners for specified type.
106
+ if (!listener) {
107
+ delete this.listeners[type];
108
+ } else {
109
+ // Remove specific listener.
110
+ this.listeners[type] = this.listeners[type].filter(fn => fn !== listener);
111
+ if (!this.listeners[type].length) delete this.listeners[type];
112
+ }
113
+ // Remove listeners object if it's empty.
114
+ if (!Object.keys(this.listeners).length) delete this.listeners;
115
+ }
116
+
117
+ /**
118
+ * Emits event of specified type. Listeners will receive specified arguments.
119
+ * @param {string} type Type of the event (e.g. `change`).
120
+ * @param {any} [...args] Arguments to be passed to listener.
121
+ * @example
122
+ * this.emit('invalid'); // Emit validation error event.
123
+ */
124
+ emit(type, ...args) {
125
+ // No listeners.
126
+ if (!this.listeners || !this.listeners[type]) return;
127
+ // Call listeners. Use `slice` to make a copy and prevent errors when
128
+ // removing listeners inside a listener.
129
+ this.listeners[type]
130
+ .slice()
131
+ .forEach(function(fn) {
132
+ fn(...args);
133
+ });
134
+ }
135
+ }
136
+
137
+ /*
138
+ * Evaluate the expression. If it's a function, call it with the provided context and return the result.
139
+ * Otherwise, return the expression as is.
140
+ * @param {any} expression Expression to be evaluated.
141
+ * @param {any} context Context to call the expression.
142
+ * @param {...any} args Arguments to pass to the expression.
143
+ * @return {any} The result of the expression.
144
+ */
145
+ var getResult = (expression, context, ...args) =>
146
+ typeof expression !== 'function' ? expression :
147
+ expression.apply(context, args);
148
+
149
+ /**
150
+ * - Orchestrates data and business logic.
151
+ * - Emits events when data changes.
152
+ *
153
+ * A `Model` manages an internal table of data attributes and triggers change events when any of its data is modified.
154
+ * Models may handle syncing data with a persistence layer. To design your models, create atomic, reusable objects
155
+ * that contain all the necessary functions for manipulating their specific data.
156
+ * Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
157
+ * Rasti models store their attributes in `this.attributes`, which is extended from `this.defaults` and the
158
+ * constructor `attrs` parameter. For every attribute, a getter is generated to retrieve the model property
159
+ * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
160
+ * and `change:attribute` events.
161
+ * @module
162
+ * @extends Rasti.Emitter
163
+ * @param {object} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.
164
+ * @property {object} defaults Object containing default attributes for the model. It will extend `this.attributes`. If a function is passed, it will be called to get the defaults. It will be bound to the model instance.
165
+ * @property {object} previous Object containing previous attributes when a change occurs.
166
+ * @example
167
+ * import { Model } from 'rasti';
168
+ * // Product model
169
+ * class ProductModel extends Model {
170
+ * preinitialize() {
171
+ * // The Product model has `name` and `price` default attributes.
172
+ * // `defaults` will extend `this.attributes`.
173
+ * // Getters and setters are generated for `this.attributes`,
174
+ * // in order to emit `change` events.
175
+ * this.defaults = {
176
+ * name: '',
177
+ * price: 0
178
+ * };
179
+ * }
180
+ *
181
+ * setDiscount(discountPercentage) {
182
+ * // Apply a discount to the price property.
183
+ * // This will call a setter that will update `price` in `this.attributes`,
184
+ * // and emit `change` and `change:price` events.
185
+ * const discount = this.price * (discountPercentage / 100);
186
+ * this.price -= discount;
187
+ * }
188
+ * }
189
+ * // Create a product instance with a name and price.
190
+ * const product = new ProductModel({ name: 'Smartphone', price: 1000 });
191
+ * // Listen to the `change:price` event.
192
+ * product.on('change:price', () => console.log('New Price:', product.price));
193
+ * // Apply a 10% discount to the product.
194
+ * product.setDiscount(10); // Output: "New Price: 900"
195
+ */
196
+ class Model extends Emitter {
197
+ constructor(attrs = {}) {
198
+ super();
199
+ // Call preinitialize.
200
+ this.preinitialize.apply(this, arguments);
201
+ // Get defaults. If `this.defaults` is a function, call it.
202
+ const defaults = getResult(this.defaults, this) || {};
203
+ // Set attributes object with defaults and passed attributes.
204
+ this.attributes = Object.assign({}, defaults, attrs);
205
+ // Object to store previous attributes when a change occurs.
206
+ this.previous = {};
207
+ // Generate getters/setters for every attribute.
208
+ Object.keys(this.attributes).forEach(this.defineAttribute.bind(this));
209
+ }
210
+
211
+ /**
212
+ * If you define a preinitialize method, it will be invoked when the Model is first created, before any instantiation logic is run for the Model.
213
+ * @param {object} attrs Object containing model attributes to extend `this.attributes`.
214
+ */
215
+ preinitialize() {}
216
+
217
+ /**
218
+ * Generate getter/setter for the given key. In order to emit `change` events.
219
+ * This method is called internally by the constructor
220
+ * for `this.attributes`.
221
+ * @param {string} key Attribute key.
222
+ */
223
+ defineAttribute(key) {
224
+ Object.defineProperty(
225
+ this,
226
+ key, {
227
+ get : () => this.get(key),
228
+ set : (value) => { this.set(key, value); }
229
+ }
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Get an attribute from `this.attributes`.
235
+ * This method is called internally by generated getters.
236
+ * @param {string} key Attribute key.
237
+ * @return {any} The attribute value.
238
+ */
239
+ get(key) {
240
+ return this.attributes[key];
241
+ }
242
+
243
+ /**
244
+ * Set an attribute into `this.attributes`.
245
+ * Emit `change` and `change:attribute` if a value changes.
246
+ * Could be called in two forms, `this.set('key', value)` and
247
+ * `this.set({ key : value })`.
248
+ * This method is called internally by generated setters.
249
+ * The `change` event listener will receive the model instance, an object containing the changed attributes, and the rest of the arguments passed to `set` method.
250
+ * The `change:attribute` event listener will receive the model instance, the new attribute value, and the rest of the arguments passed to `set` method.
251
+ * @param {string} key Attribute key or object containing keys/values.
252
+ * @param [value] Attribute value.
253
+ * @return {this} This model.
254
+ * @emits change
255
+ * @emits change:attribute
256
+ */
257
+ set(key, value, ...rest) {
258
+ let attrs, args;
259
+ // Handle both `"key", value` and `{key: value}` style arguments.
260
+ if (typeof key === 'object') {
261
+ attrs = key;
262
+ args = [value, ...rest];
263
+ } else {
264
+ attrs = { [key] : value };
265
+ args = rest;
266
+ }
267
+ // Are we in a nested `set` call?
268
+ // Calling a `set` inside a `change:attribute` or `change` event listener
269
+ const changing = this._changing;
270
+ this._changing = true;
271
+ // Store changed attributes.
272
+ const changed = {};
273
+ // Store previous attributes.
274
+ if (!changing) {
275
+ this.previous = Object.assign({}, this.attributes);
276
+ }
277
+ // Set attributes.
278
+ Object.keys(attrs).forEach(key => {
279
+ // Use equality to determine if value changed.
280
+ if (attrs[key] !== this.attributes[key]) {
281
+ changed[key] = attrs[key];
282
+ this.attributes[key] = attrs[key];
283
+ }
284
+ });
285
+
286
+ const changedKeys = Object.keys(changed);
287
+ // Pending `change` event arguments.
288
+ if (changedKeys.length) this._pending = ['change', this, changed, ...args];
289
+ // Emit `change:attribute` events.
290
+ changedKeys.forEach(key => {
291
+ this.emit(`change:${key}`, this, attrs[key], ...args);
292
+ });
293
+ // Don't emit `change` event until the end of the nested
294
+ // `set` calls inside `change:attribute` event listeners.
295
+ if (changing) return this;
296
+ // Emit `change` events, that might be nested.
297
+ while (this._pending) {
298
+ const pendingChange = this._pending;
299
+ this._pending = null;
300
+ this.emit.apply(this, pendingChange);
301
+ }
302
+ // Reset flags.
303
+ this._pending = null;
304
+ this._changing = false;
305
+
306
+ return this;
307
+ }
308
+
309
+ /**
310
+ * Return object representation of the model to be used for JSON serialization.
311
+ * By default returns a copy of `this.attributes`.
312
+ * @return {object} Object representation of the model to be used for JSON serialization.
313
+ */
314
+ toJSON() {
315
+ return Object.assign({}, this.attributes);
316
+ }
317
+ }
318
+
319
+ /*
320
+ * These option keys will be extended on the view instance.
321
+ */
322
+ const viewOptions = {
323
+ el : true,
324
+ tag : true,
325
+ attributes : true,
326
+ events : true,
327
+ model : true,
328
+ template : true,
329
+ onDestroy : true
330
+ };
331
+
332
+ /**
333
+ * - Listens for changes and renders UI.
334
+ * - Handles user input and interactivity.
335
+ * - Sends captured input to the model.
336
+ *
337
+ * A `View` is an atomic unit of the user interface that can render data from a specific model or multiple models.
338
+ * However, views can also be independent and have no associated data.
339
+ * Models must be unaware of views. Views, on the other hand, may render model data and listen to the change events
340
+ * emitted by the models to re-render themselves based on changes.
341
+ * Each `View` has a root element, `this.el`, which is used for event delegation.
342
+ * All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it.
343
+ * If `this.el` is not present, an element will be created using `this.tag` (defaulting to div) and `this.attributes`.
344
+ * @module
345
+ * @extends Rasti.Emitter
346
+ * @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
347
+ * @property {node} el Every view has a root element, `this.el`. If not present it will be created. If a function is passed, it will be called to get the element. It will be bound to the view instance.
348
+ * @property {string} tag If `this.el` is not present, an element will be created using `this.tag`. Default is `div`. If a function is passed, it will be called to get the tag name. It will be bound to the view instance.
349
+ * @property {object} attributes If `this.el` is not present, an element will be created using `this.attributes`. If a function is passed, it will be called to get the attributes. It will be bound to the view instance.
350
+ * @property {object} events Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element. If a function is passed, it will be called to get the events. It will be bound to the view instance.
351
+ * @property {object} model A `Rasti.Model` or any object containing data and business logic.
352
+ * @property {function} template A function that receives data and returns a markup string (e.g., HTML).
353
+ * @example
354
+ * import { View } from 'rasti';
355
+ *
356
+ * class Timer extends View {
357
+ * constructor(options) {
358
+ * super(options);
359
+ * // Create model to store internal state. Set `seconds` attribute to 0.
360
+ * this.model = new Model({ seconds : 0 });
361
+ * // Listen to changes in model `seconds` attribute and re-render.
362
+ * this.model.on('change:seconds', this.render.bind(this));
363
+ * // Increment model `seconds` attribute every 1000 milliseconds.
364
+ * this.interval = setInterval(() => this.model.seconds++, 1000);
365
+ * }
366
+ *
367
+ * template(model) {
368
+ * return `Seconds: <span>${model.seconds}</span>`;
369
+ * }
370
+ * }
371
+ * // Render view and append view's element into the body.
372
+ * document.body.appendChild(new Timer().render().el);
373
+ */
374
+ class View extends Emitter {
375
+ constructor(options = {}) {
376
+ super();
377
+ // Call preinitialize.
378
+ this.preinitialize.apply(this, arguments);
379
+ // Generate unique id.
380
+ // Useful to generate element ids.
381
+ this.uid = `uid${++View.uid}`;
382
+ // Store delegated event listeners,
383
+ // so they can be unbound later.
384
+ this.delegatedEventListeners = [];
385
+ // Store child views,
386
+ // so they can be destroyed.
387
+ this.children = [];
388
+ // Mutable array to store handlers to be called on destroy.
389
+ this.destroyQueue = [];
390
+ // Extend "this" with options, mapping viewOptions keys.
391
+ Object.keys(options).forEach(key => {
392
+ if (viewOptions[key]) this[key] = options[key];
393
+ });
394
+ // Ensure that the view has a root element at `this.el`.
395
+ this.ensureElement();
396
+ }
397
+
398
+ /**
399
+ * If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.
400
+ * @param {object} attrs Object containing model attributes to extend `this.attributes`.
401
+ */
402
+ preinitialize() {}
403
+
404
+ /**
405
+ * Returns the first element that matches the selector,
406
+ * scoped to DOM elements within the current view's root element (`this.el`).
407
+ * @param {string} selector CSS selector.
408
+ * @return {node} Element matching selector within the view's root element (`this.el`).
409
+ */
410
+ $(selector) {
411
+ return this.el.querySelector(selector);
412
+ }
413
+
414
+ /**
415
+ * Returns a list of elements that match the selector,
416
+ * scoped to DOM elements within the current view's root element (`this.el`).
417
+ * @param {string} selector CSS selector.
418
+ * @return {node[]} List of elements matching selector within the view's root element (`this.el`).
419
+ */
420
+ $$(selector) {
421
+ return this.el.querySelectorAll(selector);
422
+ }
423
+
424
+ /**
425
+ * Destroy the view.
426
+ * Destroy children views if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.
427
+ * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
428
+ * @return {Rasti.View} Return `this` for chaining.
429
+ */
430
+ destroy() {
431
+ // Call destroy on children.
432
+ this.destroyChildren();
433
+ // Undelegate `this.el` event listeners
434
+ this.undelegateEvents();
435
+ // Unbind `this` events.
436
+ this.off();
437
+ // Call destroy queue.
438
+ this.destroyQueue.forEach(fn => fn());
439
+ this.destroyQueue = [];
440
+ // Call onDestroy lifecycle method
441
+ this.onDestroy.apply(this, arguments);
442
+ // Return `this` for chaining.
443
+ return this;
444
+ }
445
+
446
+ /**
447
+ * `onDestroy` lifecycle method is called after the view is destroyed.
448
+ * Override with your code. Useful to stop listening to model's events.
449
+ * @param {object} options Options object or any arguments passed to `destroy` method.
450
+ */
451
+ onDestroy() {}
452
+
453
+ /**
454
+ * Add a view as a child.
455
+ * Children views are stored at `this.children`, and destroyed when the parent is destroyed.
456
+ * Returns the child for chaining.
457
+ * @param {Rasti.View} child
458
+ * @return {Rasti.View}
459
+ */
460
+ addChild(child) {
461
+ this.children.push(child);
462
+ return child;
463
+ }
464
+
465
+ /**
466
+ * Call destroy method on children views.
467
+ */
468
+ destroyChildren() {
469
+ this.children.forEach(child => child.destroy());
470
+ this.children = [];
471
+ }
472
+
473
+ /**
474
+ * Ensure that the view has a root element at `this.el`.
475
+ * You shouldn't call this method directly. It's called from the constructor.
476
+ * You may override it if you want to use a different logic or to
477
+ * postpone element creation.
478
+ */
479
+ ensureElement() {
480
+ // Element is already present.
481
+ if (this.el) {
482
+ // If "this.el" is a function, call it to get the element.
483
+ this.el = getResult(this.el, this);
484
+ } else {
485
+ // If "this.el" is not present,
486
+ // create a new element according "this.tag"
487
+ // and "this.attributes".
488
+ const tag = getResult(this.tag, this);
489
+ const attrs = getResult(this.attributes, this);
490
+ this.el = this.createElement(tag, attrs);
491
+ }
492
+ // Delegate events on element.
493
+ this.delegateEvents();
494
+ }
495
+
496
+ /**
497
+ * Create an element.
498
+ * Called from the constructor if `this.el` is undefined, to ensure
499
+ * the view has a root element.
500
+ * @param {string} tag Tag for the element. Default to `div`
501
+ * @param {object} attrs Attributes for the element.
502
+ * @return {node} The created element.
503
+ */
504
+ createElement(tag = 'div', attrs = {}) {
505
+ // Create DOM element.
506
+ let el = document.createElement(tag);
507
+ // Add element attributes.
508
+ Object.keys(attrs)
509
+ .forEach(key => el.setAttribute(key, attrs[key]));
510
+
511
+ return el;
512
+ }
513
+
514
+ /**
515
+ * Remove `this.el` from the DOM.
516
+ * @return {Rasti.View} Return `this` for chaining.
517
+ */
518
+ removeElement() {
519
+ this.el.parentNode.removeChild(this.el);
520
+ // Return `this` for chaining.
521
+ return this;
522
+ }
523
+
524
+ /**
525
+ * Provide declarative listeners for DOM events within a view. If an events hash is not passed directly,
526
+ * uses `this.events` as the source.
527
+ * Events are written in the format `{'event selector' : 'listener'}`.
528
+ * The listener may be either the name of a method on the view, or a direct function body.
529
+ * Omitting the selector causes the event to be bound to the view's root element (`this.el`).
530
+ * By default, `delegateEvents` is called within the View's constructor,
531
+ * so if you have a simple events hash, all of your DOM events will always already be connected,
532
+ * and you will never have to call this function yourself.
533
+ * All attached listeners are bound to the view automatically, so when the listeners are invoked,
534
+ * `this` continues to refer to the view object.
535
+ * When `delegateEvents` is run again, perhaps with a different events hash, all listeners
536
+ * are removed and delegated afresh.
537
+ * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to root element.
538
+ * @return {Rasti.View} Return `this` for chaining.
539
+ * @example
540
+ * MyView.prototype.events = {
541
+ * 'click button.ok' : 'onClickOkButton',
542
+ * 'click button.cancel' : function() {}
543
+ * };
544
+ */
545
+ delegateEvents(events) {
546
+ if (!events) events = getResult(this.events, this);
547
+ if (!events) return this;
548
+
549
+ if (this.delegatedEventListeners.length) this.undelegateEvents();
550
+
551
+ // Store events by type i.e.: "click", "submit", etc.
552
+ let eventTypes = {};
553
+
554
+ Object.keys(events).forEach(key => {
555
+ const keyParts = key.split(' ');
556
+ const type = keyParts.shift();
557
+ const selector = keyParts.join(' ');
558
+
559
+ let listener = events[key];
560
+ // Listener may be a string representing a method name on the view,
561
+ // or a function.
562
+ listener = (
563
+ typeof listener === 'string' ?
564
+ this[listener] :
565
+ listener
566
+ ).bind(this);
567
+
568
+ if (!eventTypes[type]) eventTypes[type] = [];
569
+
570
+ eventTypes[type].push({ selector, listener });
571
+ });
572
+
573
+ Object.keys(eventTypes).forEach(type => {
574
+ // Listener for the type of event.
575
+ const typeListener = (event) => {
576
+ // Iterate and run every individual listener if the selector matches.
577
+ eventTypes[type].forEach(({ selector, listener }) => {
578
+ if (!selector || event.target.closest(selector)) listener(event, this);
579
+ });
580
+ };
581
+
582
+ this.delegatedEventListeners.push({ type, listener : typeListener });
583
+ this.el.addEventListener(type, typeListener);
584
+ });
585
+ // Return `this` for chaining.
586
+ return this;
587
+ }
588
+
589
+ /**
590
+ * Removes all of the view's delegated events.
591
+ * Useful if you want to disable or remove a view from the DOM temporarily.
592
+ * Called automatically when the view is destroyed.
593
+ * @return {Rasti.View} Return `this` for chaining.
594
+ */
595
+ undelegateEvents() {
596
+ this.delegatedEventListeners.forEach(({ type, listener }) => {
597
+ this.el.removeEventListener(type, listener);
598
+ });
599
+
600
+ this.delegatedEventListeners = [];
601
+ // Return `this` for chaining.
602
+ return this;
603
+ }
604
+
605
+ /**
606
+ * Renders the view.
607
+ * This method should be overridden with custom logic.
608
+ * The convention is to only manipulate the DOM within the scope of `this.el`,
609
+ * and to return `this` for chaining.
610
+ * If you add any child views, you must call `this.destroyChildren`.
611
+ * The default implementation sets the innerHTML of `this.el` with the result
612
+ * of calling `this.template`, passing `this.model` as an argument.
613
+ * <br><br> &#9888; **Security Notice:** The default implementation utilizes `innerHTML` on the root element
614
+ * for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated
615
+ * content is properly sanitized before inserting it into the DOM. For best practices on secure data handling,
616
+ * refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
617
+ * @return {Rasti.View} Return `this` for chaining.
618
+ */
619
+ render() {
620
+ if (this.template) this.el.innerHTML = this.template(this.model);
621
+ // Return `this` for chaining.
622
+ return this;
623
+ }
624
+ }
625
+
626
+ /*
627
+ * Unique Id
628
+ */
629
+ View.uid = 0;
630
+
631
+ /*
632
+ * Same as getResult, but pass context as argument to the expression.
633
+ * Used to evaluate expressions in the context of a component.
634
+ * @param {any} expression The expression to be evaluated.
635
+ * @param {any} context The context to call the expression with.
636
+ * @return {any} The result of the evaluated expression.
637
+ */
638
+ const renderExpression = (expression, context) => getResult(expression, context, context);
639
+
640
+ /*
641
+ * Generate string with placeholders for interpolated expressions.
642
+ * @param strings {array} Array of strings.
643
+ * @param expressions {array} Array of expressions.
644
+ * @return {string} String with placeholders.
645
+ */
646
+ const addPlaceholders = (strings, expressions) =>
647
+ strings.reduce((out, string, i) => {
648
+ // Add string part.
649
+ out.push(string);
650
+ // Add expression placeholders.
651
+ if (typeof expressions[i] !== 'undefined') {
652
+ out.push(Component.PLACEHOLDER_EXPRESSION(i));
653
+ }
654
+ return out;
655
+ }, []).join('');
656
+
657
+ /*
658
+ * Generate one dimensional array with strings and expressions.
659
+ * @param main {string} The main template containing placeholders.
660
+ * @param expressions {array} Array of expressions to replace placeholders.
661
+ * @return {array} Array containing strings and expressions.
662
+ */
663
+ const splitPlaceholders = (main, expressions) => {
664
+ const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
665
+ const regExp = new RegExp(`${PH}`, 'g');
666
+ const out = [];
667
+ let lastIndex = 0;
668
+ let match;
669
+ // Generate one dimensional array with strings and expressions,
670
+ // so all the components are added as children by the parent component.
671
+ while ((match = regExp.exec(main)) !== null) {
672
+ const before = main.slice(lastIndex, match.index);
673
+ out.push(before, expressions[match[1]]);
674
+ lastIndex = match.index + match[0].length;
675
+ }
676
+ out.push(main.slice(lastIndex));
677
+
678
+ return out;
679
+ };
680
+
681
+ /*
682
+ * Expand attributes.
683
+ * @param attributes {array} Array of attributes as key, value pairs.
684
+ * @param renderExpression {function} Function to render expressions.
685
+ * @return {object}
686
+ * @property {object} all All attributes.
687
+ * @property {object} events Event listeners.
688
+ * @property {object} attributes Attributes.
689
+ */
690
+ const expandAttributes = (attributes, renderExpression) => {
691
+ const out = attributes.reduce((out, pair) => {
692
+ const attribute = renderExpression(pair[0]);
693
+ // Attribute without value.
694
+ if (pair.length === 1) {
695
+ if (typeof attribute === 'object') {
696
+ // Expand objects as attributes.
697
+ out.all = Object.assign(out.all, attribute);
698
+ } else if (typeof attribute === 'string') {
699
+ // Treat as boolean.
700
+ out.all[attribute] = true;
701
+ }
702
+ } else {
703
+ // Attribute with value.
704
+ const value = renderExpression(pair[1]);
705
+ out.all[attribute] = value;
706
+ }
707
+
708
+ return out;
709
+ }, { all : {}, events : {}, attributes : {} });
710
+
711
+ Object.keys(out.all).forEach(key => {
712
+ // Check if key is an event listener.
713
+ const match = key.match(/on(([A-Z]{1}[a-z]+)+)/);
714
+ if (match && match[1]) {
715
+ // Add event listener.
716
+ out.events[match[1].toLowerCase()] = out.all[key];
717
+ } else {
718
+ // Add attribute.
719
+ out.attributes[key] = out.all[key];
720
+ }
721
+ });
722
+
723
+ return out;
724
+ };
725
+
726
+ /*
727
+ * Replace component tags with expressions.
728
+ * `<${Component} />` or `<${Component}></${Component}>` will be replaced
729
+ * by a function that mounts the component.
730
+ * Returns the template with component tags replaced by expressions placeholders
731
+ * modifies the expressions array adding the mount functions.
732
+ * @param main {string} The main template.
733
+ * @return {string} The template with components tags replaced by expressions
734
+ * placeholders.
735
+ */
736
+ const expandComponents = (main, expressions) => {
737
+ const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
738
+ // Match component tags.
739
+ return main.replace(
740
+ new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</(${PH})>|<(${PH})([^>]*)/>`,'g'),
741
+ function() {
742
+ const { tag, attributes, inner, close, raw } = parseMatch(arguments, expressions);
743
+ // No component found.
744
+ if (!(tag.prototype instanceof Component)) return raw;
745
+
746
+ let renderChildren;
747
+ // Non void component.
748
+ if (close) {
749
+ // Close component tag must match open component tag.
750
+ if (tag !== close) return raw;
751
+ // Recursively expand inner components.
752
+ const list = splitPlaceholders(expandComponents(inner, expressions), expressions);
753
+ // Create renderChildren function.
754
+ renderChildren = function() {
755
+ return list.map(item => renderExpression(item, this)).flat();
756
+ };
757
+ }
758
+ // Create mount function.
759
+ const mount = function() {
760
+ const options = expandAttributes(attributes, value => renderExpression(value, this)).all;
761
+ // Add renderChildren function to options.
762
+ if (renderChildren) options.renderChildren = renderChildren.bind(this);
763
+ // Mount component.
764
+ return tag.mount(options);
765
+ };
766
+ // Add mount function to expression.
767
+ expressions.push(mount);
768
+ // Replace whole string with expression placeholder.
769
+ return Component.PLACEHOLDER_EXPRESSION(expressions.length - 1);
770
+ }
771
+ );
772
+ };
773
+
774
+ /*
775
+ * Parse match data to get tag, attributes, inner html and close tag.
776
+ * @param match {array}
777
+ * @return {object}
778
+ * @property {string} tag The tag.
779
+ * @property {string} inner The inner html.
780
+ * @property {string} close The closing tag.
781
+ * @property {array} attributes Array of attributes as key, value pairs.
782
+ * @property {string} raw The whole match.
783
+ */
784
+ const parseMatch = (match, expressions) => {
785
+ const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
786
+
787
+ const [all, openTag, openIdx, nonVoidAttrs, inner, closeTag, closeIdx,
788
+ selfClosingTag, selfClosingIdx, selfClosingAttrs] = match;
789
+
790
+ const data = { raw : all, attributes : [] };
791
+
792
+ let attributesStr;
793
+ if (openTag) {
794
+ // Non void element.
795
+ data.tag = typeof openIdx !== 'undefined' ? expressions[openIdx] : openTag;
796
+ data.inner = inner;
797
+ data.close = typeof closeIdx !== 'undefined' ? expressions[closeIdx] : closeTag;
798
+ attributesStr = nonVoidAttrs;
799
+ } else {
800
+ // Self closing element.
801
+ data.tag = typeof selfClosingIdx !== 'undefined' ? expressions[selfClosingIdx] : selfClosingTag;
802
+ attributesStr = selfClosingAttrs;
803
+ }
804
+ // Parse attributes.
805
+ const regExp = new RegExp(`(${PH}|[\\w-]+)(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`, 'g');
806
+
807
+ let attributeMatch;
808
+ while ((attributeMatch = regExp.exec(attributesStr)) !== null) {
809
+ const [, attribute, attributeIdx,, valueIdx, value] = attributeMatch;
810
+
811
+ const attr = typeof attributeIdx !== 'undefined' ? expressions[attributeIdx] : attribute;
812
+ const val = typeof valueIdx !== 'undefined' ? expressions[valueIdx] : value;
813
+
814
+ if (typeof val !== 'undefined') {
815
+ data.attributes.push([attr, val]);
816
+ } else {
817
+ data.attributes.push([attr]);
818
+ }
819
+ }
820
+
821
+ return data;
822
+ };
823
+
824
+ /*
825
+ * These option keys will be extended on the component instance.
826
+ */
827
+ const componentOptions = {
828
+ key : true,
829
+ state : true,
830
+ onCreate : true,
831
+ onChange : true,
832
+ onRender : true
833
+ };
834
+
835
+ /*
836
+ * HTML tags that are self closing.
837
+ */
838
+ const selfClosingTags = {
839
+ area : true, base : true, br : true, col : true, embed : true, hr : true,
840
+ img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
841
+ };
842
+
843
+ /**
844
+ * Components are a special kind of `View` that is designed to be easily composable,
845
+ * making it simple to add child views and build complex user interfaces.
846
+ * Unlike views, which are render-agnostic, components have a specific set of rendering
847
+ * guidelines that allow for a more declarative development style.
848
+ * Components are defined with the `create` static method, which takes a tagged template.
849
+ * @module
850
+ * @extends Rasti.View
851
+ * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
852
+ * @property {string} key A unique key to identify the component. Used to recycle child components.
853
+ * @property {object} model A `Rasti.Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method.
854
+ * @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state. The component will listen to `change` events and call `onChange` lifecycle method.
855
+ * @example
856
+ * import { Component, Model } from 'rasti';
857
+ * // Create Timer component.
858
+ * const Timer = Component.create`
859
+ * <div>
860
+ * Seconds: <span>${({ model }) => model.seconds}</span>
861
+ * </div>
862
+ * `;
863
+ * // Create model to store seconds.
864
+ * const model = new Model({ seconds: 0 });
865
+ * // Mount timer on body.
866
+ * Timer.mount({ model }, document.body);
867
+ * // Increment `model.seconds` every second.
868
+ * setInterval(() => model.seconds++, 1000);
869
+ */
870
+ class Component extends View {
871
+ constructor(options = {}) {
872
+ super(...arguments);
873
+ // Extend "this" with options, mapping componentOptions keys.
874
+ Object.keys(options).forEach(key => {
875
+ if (componentOptions[key]) this[key] = options[key];
876
+ });
877
+ // Store options by default.
878
+ this.options = options;
879
+ // Call lifecycle method.
880
+ this.onCreate.apply(this, arguments);
881
+ }
882
+
883
+ /**
884
+ * Listen to `change` event on a model or emitter object and call `onChange` lifecycle method.
885
+ * The listener will be removed when the component is destroyed.
886
+ * By default the component will be subscribed to `this.model` and `this.state`.
887
+ * @param {Rasti.Model} model A model or emitter object to listen to changes.
888
+ * @return {Rasti.Component} The component instance.
889
+ */
890
+ subscribe(model) {
891
+ // Check if model has `on` method.
892
+ if (!model.on) return;
893
+ // Store bound onChange method.
894
+ const onChange = this.onChange.bind(this);
895
+ // Listen to model changes and store unbind function.
896
+ const off = model.on('change', onChange);
897
+ // Add unbind function to destroy queue.
898
+ // So the component stops listening to model changes when destroyed.
899
+ this.destroyQueue.push(
900
+ // Rasti `on` method returns an unbind function.
901
+ // But other libraries may return the object itself.
902
+ typeof off === 'function' ? off : () => model.off('change', onChange)
903
+ );
904
+
905
+ return this;
906
+ }
907
+
908
+ /*
909
+ * Tell if component is a container.
910
+ * In which case, it will not have an element by itself.
911
+ * It will render a single expression which is expected to return a single component as child.
912
+ * `this.el` will be a reference to that child component's element.
913
+ * @return {boolean}
914
+ */
915
+ isContainer() {
916
+ return !!(!this.tag && this.template);
917
+ }
918
+
919
+ /*
920
+ * Override. We don't want to ensure an element on instantiation.
921
+ * We will provide it later.
922
+ */
923
+ ensureElement() {
924
+ // If el is provided, delegate events.
925
+ if (this.el) {
926
+ // If "this.el" is a function, call it to get the element.
927
+ this.el = getResult(this.el, this);
928
+ this.delegateEvents();
929
+ }
930
+ }
931
+
932
+ /*
933
+ * Find view's element on parent node, using its data attribute.
934
+ * @param parent {node} The parent node.
935
+ * @return {node} The component's element.
936
+ */
937
+ findElement(parent) {
938
+ return (parent || document).querySelector(`[${Component.DATA_ATTRIBUTE_UID}="${this.uid}"]`);
939
+ }
940
+
941
+ /*
942
+ * Eval attributes expressions.
943
+ * @return {object} Object containing add, remove and html properties.
944
+ */
945
+ getAttributes() {
946
+ const add = {};
947
+ const remove = {};
948
+ const html = [];
949
+
950
+ const attributes = { [Component.DATA_ATTRIBUTE_UID] : this.uid };
951
+
952
+ if (this.attributes) Object.assign(attributes, getResult(this.attributes, this));
953
+
954
+ Object.keys(attributes).forEach(key => {
955
+ // Evaluate attribute value.
956
+ let value = attributes[key];
957
+ // Transform bool attribute values
958
+ if (value === false) {
959
+ remove[key] = true;
960
+ } else if (value === true) {
961
+ add[key] = '';
962
+ html.push(key);
963
+ } else {
964
+ if (value === null || typeof value === 'undefined') value = '';
965
+
966
+ add[key] = value;
967
+ html.push(`${key}="${value}"`);
968
+ }
969
+ });
970
+
971
+ return { add, remove, html : html.join(' ') };
972
+ }
973
+
974
+ /*
975
+ * Used internally on the render process.
976
+ * Attach the view to the dom element.
977
+ * @param parent {node} The parent node.
978
+ * @return {Rasti.Component} The component instance.
979
+ */
980
+ hydrate(parent) {
981
+ // Listen to model changes and call onChange.
982
+ if (this.model) this.subscribe(this.model);
983
+ // Listen to state changes and call onChange.
984
+ if (this.state) this.subscribe(this.state);
985
+
986
+ if (!this.isContainer()) {
987
+ this.el = this.findElement(parent);
988
+ this.delegateEvents();
989
+ this.children.forEach(child => child.hydrate(this.el));
990
+ } else {
991
+ this.children[0].hydrate(parent);
992
+ this.el = this.children[0].el;
993
+ }
994
+ // Call `onRender` lifecycle method.
995
+ this.onRender.call(this, Component.RENDER_TYPE_HYDRATE);
996
+ // Return `this` for chaining.
997
+ return this;
998
+ }
999
+
1000
+ /*
1001
+ * Used internally in the render process.
1002
+ * Reuse a view that has `key` when its parent is rendered.
1003
+ * @param parent {node} The parent node.
1004
+ * @return {Rasti.Component} The component instance.
1005
+ */
1006
+ recycle(parent) {
1007
+ // If component is a container, call recycle on its child.
1008
+ if (this.isContainer()) return this.children[0].recycle(parent);
1009
+ // Find placeholder element to be replaced. It has same data attribute as this component.
1010
+ const toBeReplaced = this.findElement(parent);
1011
+ // Replace it with this.el.
1012
+ toBeReplaced.replaceWith(this.el);
1013
+ // Call `onRender` lifecycle method.
1014
+ this.onRender.call(this, Component.RENDER_TYPE_RECYCLE);
1015
+ // Return `this` for chaining.
1016
+ return this;
1017
+ }
1018
+
1019
+ /*
1020
+ * Override. Add some custom logic to super `destroy` method.
1021
+ * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
1022
+ */
1023
+ destroy() {
1024
+ super.destroy.apply(this, arguments);
1025
+ // Set destroyed flag to prevent a last render after destroyed.
1026
+ this.destroyed = true;
1027
+ // Return `this` for chaining.
1028
+ return this;
1029
+ }
1030
+
1031
+ /**
1032
+ * Lifecycle method. Called when the view is created, at the end of the `constructor`.
1033
+ * @param options {object} The view options.
1034
+ */
1035
+ onCreate() {}
1036
+
1037
+ /**
1038
+ * Lifecycle method. Called when model emits `change` event.
1039
+ * By default calls `render` method.
1040
+ * This method can be extended with custom logic.
1041
+ * Maybe comparing new attributes with previous ones and calling
1042
+ * render when needed.
1043
+ * @param model {Rasti.Model} The model that emitted the event.
1044
+ * @param changed {object} Object containing keys and values that has changed.
1045
+ * @param [...args] {any} Any extra arguments passed to set method.
1046
+ */
1047
+ onChange() {
1048
+ this.render();
1049
+ }
1050
+
1051
+ /**
1052
+ * Lifecycle method. Called when the view is rendered.
1053
+ * @param type {string} The render type. Can be `render`, `hydrate` or `recycle`.
1054
+ */
1055
+ onRender() {}
1056
+
1057
+ /**
1058
+ * Lifecycle method. Called when the view is destroyed.
1059
+ * @param {object} options Options object or any arguments passed to `destroy` method.
1060
+ */
1061
+ onDestroy() {}
1062
+
1063
+ /**
1064
+ * Tagged template helper method.
1065
+ * Used to create a partial template.
1066
+ * It will return a one-dimensional array with strings and expressions.
1067
+ * @param {TemplateStringsArray} strings - Template strings.
1068
+ * @param {...any} expressions - Template expressions.
1069
+ * @return {Array} Array containing strings and expressions.
1070
+ * @example
1071
+ * import { Component } from 'rasti';
1072
+ * // Create a Title component.
1073
+ * const Title = Component.create`
1074
+ * <h1>${self => self.renderChildren()}</h1>
1075
+ * `;
1076
+ * // Create Main component.
1077
+ * const Main = Component.create`
1078
+ * <main>
1079
+ * ${self => self.renderHeader()}
1080
+ * </main>
1081
+ * `.extend({
1082
+ * // Render header method.
1083
+ * // Use `partial` to render an HTML template adding children components.
1084
+ * renderHeader() {
1085
+ * return this.partial`
1086
+ * <header>
1087
+ * <${Title}>${self => self.model.title}</${Title}>
1088
+ * </header>
1089
+ * `;
1090
+ * }
1091
+ * });
1092
+ */
1093
+ partial(strings, ...expressions) {
1094
+ return splitPlaceholders(
1095
+ expandComponents(
1096
+ addPlaceholders(strings, expressions), expressions
1097
+ ), expressions
1098
+ ).map(item => {
1099
+ return renderExpression(item, this);
1100
+ }).flat();
1101
+ }
1102
+
1103
+ getRecyclePlaceholder() {
1104
+ if (this.isContainer()) return this.children[0].getRecyclePlaceholder();
1105
+
1106
+ const tag = getResult(this.tag, this) || 'div';
1107
+ const attributes = `${Component.DATA_ATTRIBUTE_UID}="${this.uid}"`;
1108
+
1109
+ return this.template || !selfClosingTags[tag] ?
1110
+ `<${tag} ${attributes}></${tag}>` :
1111
+ `<${tag} ${attributes} />`;
1112
+ }
1113
+
1114
+ /*
1115
+ * Treat the whole view as a HTML string.
1116
+ */
1117
+ toString() {
1118
+ // Normally there won't be any children, but if there are, destroy them.
1119
+ this.destroyChildren();
1120
+ // Container.
1121
+ if (this.isContainer()) return this.template.call(this, this.addChild.bind(this));
1122
+ // Get tag name.
1123
+ const tag = getResult(this.tag, this) || 'div';
1124
+ // Get attributes.
1125
+ const attributes = this.getAttributes().html;
1126
+ // Replace expressions of inner template.
1127
+ const inner = this.template ? this.template.call(this, this.addChild.bind(this)) : '';
1128
+ // Generate outer template.
1129
+ return this.template || !selfClosingTags[tag] ?
1130
+ `<${tag} ${attributes}>${inner}</${tag}>` :
1131
+ `<${tag} ${attributes} />`;
1132
+ }
1133
+
1134
+ /*
1135
+ * View render method.
1136
+ */
1137
+ render() {
1138
+ // Prevent a last re render if view is already destroyed.
1139
+ if (this.destroyed) return this;
1140
+
1141
+ if (!this.isContainer()) {
1142
+ // If `this.el` is not present, render the view as a string and hydrate it.
1143
+ if (!this.el) {
1144
+ const fragment = this.createElement('template');
1145
+ fragment.innerHTML = this;
1146
+ this.hydrate(fragment.content);
1147
+ return this;
1148
+ }
1149
+ // Set `this.el` attributes.
1150
+ const attributes = this.getAttributes();
1151
+ // Remove attributes.
1152
+ Object.keys(attributes.remove).forEach(key => {
1153
+ this.el.removeAttribute(key);
1154
+ });
1155
+ // Add attributes.
1156
+ Object.keys(attributes.add).forEach(key => {
1157
+ this.el.setAttribute(key, attributes.add[key]);
1158
+ });
1159
+ }
1160
+ // Check for `template` to see if view has innerHTML.
1161
+ if (this.template) {
1162
+ // Store active element.
1163
+ const activeElement = document.activeElement;
1164
+
1165
+ const nextChildren = [];
1166
+ const recycledChildren = [];
1167
+
1168
+ const previousChildren = this.children;
1169
+ this.children = [];
1170
+ // Replace expressions. Set html inside of `this.el`.
1171
+ const inner = this.template.call(this, component => {
1172
+ let out = component;
1173
+ // Check if child already exists.
1174
+ const found = component.key && previousChildren.find(
1175
+ previousChild => previousChild.key === component.key
1176
+ );
1177
+
1178
+ if (found) {
1179
+ // If child already exists, replace it html by its root element.
1180
+ out = found.getRecyclePlaceholder();
1181
+ // Add child to recycled children.
1182
+ recycledChildren.push(found);
1183
+ // Destroy new child component. Use recycled one instead.
1184
+ component.destroy();
1185
+ } else {
1186
+ // Not found. Add new child component.
1187
+ nextChildren.push(component);
1188
+ }
1189
+ // Component html.
1190
+ return out;
1191
+ });
1192
+
1193
+ if (this.isContainer()) {
1194
+ if (nextChildren[0]) {
1195
+ const fragment = this.createElement('template');
1196
+ fragment.innerHTML = inner;
1197
+ // Add new child to dom fragment and hydrate it.
1198
+ this.addChild(nextChildren[0]).hydrate(fragment.content);
1199
+ // Get next child element.
1200
+ const nextEl = fragment.content.children[0];
1201
+ // If `this.el` is present, replace it with nextEl.
1202
+ if (this.el) this.el.replaceWith(nextEl);
1203
+ // Set `this.el` to nextEl.
1204
+ this.el = nextEl;
1205
+ } else if (recycledChildren[0]) {
1206
+ this.addChild(recycledChildren[0]);
1207
+ } else {
1208
+ throw new Error('Container component must have a child component');
1209
+ }
1210
+ } else {
1211
+ this.el.innerHTML = inner;
1212
+ // Add new children. Hydrate them.
1213
+ nextChildren.forEach(nextChild => {
1214
+ this.addChild(nextChild).hydrate(this.el);
1215
+ });
1216
+ // Replace children root elements with recycled components.
1217
+ recycledChildren.forEach(recycledChild => {
1218
+ this.addChild(recycledChild).recycle(this.el);
1219
+ });
1220
+ }
1221
+ // Destroy unused children.
1222
+ previousChildren.forEach(previousChild => {
1223
+ const found = recycledChildren.indexOf(previousChild) > -1;
1224
+ if (!found) previousChild.destroy();
1225
+ });
1226
+ // Restore focus.
1227
+ if (this.el.contains(activeElement)) {
1228
+ activeElement.focus();
1229
+ }
1230
+ }
1231
+ // Call onRender lifecycle method.
1232
+ this.onRender.call(this, Component.RENDER_TYPE_RENDER);
1233
+ // Return this for chaining.
1234
+ return this;
1235
+ }
1236
+
1237
+ /**
1238
+ * Helper method used to extend a `Component`, creating a subclass.
1239
+ * @static
1240
+ * @param {object} object Object containing methods to be added to the new `Component` subclass. Also can be a function that receives the parent prototype and returns an object.
1241
+ */
1242
+ static extend(object) {
1243
+ const Current = this;
1244
+
1245
+ class Component extends Current {}
1246
+
1247
+ Object.assign(
1248
+ Component.prototype,
1249
+ typeof object === 'function' ? object(Current.prototype) : object
1250
+ );
1251
+
1252
+ return Component;
1253
+ }
1254
+
1255
+ /**
1256
+ * Mount the component into the dom.
1257
+ * It instantiate the Component view using options,
1258
+ * appends its element into the DOM (if `el` is provided).
1259
+ * And returns the view instance.
1260
+ * <br><br> &#9888; **Security Notice:** `Component` utilizes `innerHTML` on a document fragment for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated content is properly sanitized before inserting it into the DOM. For best practices on secure data handling, refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
1261
+ * @static
1262
+ * @param {object} options The view options.
1263
+ * @param {node} el Dom element to append the view element.
1264
+ * @param {boolean} hydrate If true, the view will hydrate existing DOM.
1265
+ * @return {Rasti.Component}
1266
+ */
1267
+ static mount(options, el, hydrate) {
1268
+ // Instantiate view.
1269
+ const view = new this(options);
1270
+ // If `el` is passed, mount component.
1271
+ if (el) {
1272
+ if (hydrate) {
1273
+ // Hydrate existing DOM.
1274
+ view.toString();
1275
+ view.hydrate(el);
1276
+ } else {
1277
+ // Append element to the DOM.
1278
+ el.appendChild(view.render().el);
1279
+ }
1280
+ }
1281
+ // Return view instance.
1282
+ return view;
1283
+ }
1284
+
1285
+ /**
1286
+ * Takes a tagged template containing an HTML string,
1287
+ * and returns a new `Component` class.
1288
+ * - The template outer tag and attributes will be used to create the view's root element.
1289
+ * - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
1290
+ * - Event handlers should be passed, at the root element, in the form of `onEventName=${{'selector' : listener }}`. Where `selector` is a CSS selector. The event will be delegated to the view's root element.
1291
+ * - The template inner HTML will be used as the view's template.
1292
+ * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it.
1293
+ * - If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
1294
+ * - If the function returns a component instance, it will be added as a child component.
1295
+ * - If the function returns an array, each item will be evaluated as above.
1296
+ * - If the tagged template contains only one expression that mounts a component, or the tags are references to a component, the component will be considered a <b>container</b>. It will render a single component as a child. `this.el` will be a reference to that child component's element.
1297
+ * @static
1298
+ * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
1299
+ * @param {...*} expressions - The expressions to be interpolated within the template.
1300
+ * @return {Rasti.Component} The newly created component class.
1301
+ * @example
1302
+ * import { Component } from 'rasti';
1303
+ * // Create a button component.
1304
+ * const Button = Component.create`
1305
+ * <button class="${({ options }) => options.className}">
1306
+ * ${self => self.renderChildren()}
1307
+ * </button>
1308
+ * `;
1309
+ * // Create a container using the button component
1310
+ * const ButtonOk = Component.create`
1311
+ * <${Button} className="ok">Ok</${Button}>
1312
+ * `;
1313
+ * // Create a button component using a function
1314
+ * const ButtonCancel = Component.create(() => Button.mount({
1315
+ * className: 'cancel',
1316
+ * renderChildren: () => 'Cancel'
1317
+ * }));
1318
+ */
1319
+ static create(strings, ...expressions) {
1320
+ const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
1321
+ // Containers can be created using create as a functions instead of a tagged template.
1322
+ if (typeof strings === 'function') {
1323
+ expressions = [strings];
1324
+ strings = ['', ''];
1325
+ }
1326
+
1327
+ let tag, attributes, events, template;
1328
+ // Create output string for main template. Add placeholders for new lines.
1329
+ const main = expandComponents(addPlaceholders(strings, expressions), expressions);
1330
+
1331
+ let match = main.match(
1332
+ new RegExp(`^\\s*<([a-z]+[1-6]?|${PH})([^>]*)>([\\s\\S]*?)</(\\1|${PH})>\\s*$|^\\s*<([a-z]+[1-6]?|${PH})([^>]*)/>\\s*$`)
1333
+ );
1334
+
1335
+ if (match) {
1336
+ // It's a component with tag.
1337
+ const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
1338
+ // Get tag, attributes.
1339
+ tag = function() {
1340
+ return renderExpression(tagExpression, this);
1341
+ };
1342
+ // Get attributes.
1343
+ attributes = function() {
1344
+ return expandAttributes(attributesAndEvents, value => renderExpression(value, this)).attributes;
1345
+ };
1346
+ // Get events.
1347
+ events = function() {
1348
+ const onlyEvents = expandAttributes(attributesAndEvents, value => renderExpression(value, this)).events;
1349
+
1350
+ return Object.keys(onlyEvents).reduce((out, key) => {
1351
+ const typeListeners = renderExpression(onlyEvents[key], this);
1352
+
1353
+ Object.keys(typeListeners).forEach(selector => {
1354
+ out[`${key}${selector === '&' ? '' : ` ${selector}`}`] = typeListeners[selector];
1355
+ });
1356
+
1357
+ return out;
1358
+ }, {});
1359
+ };
1360
+ // If there is a closing tag, get template.
1361
+ if (close) {
1362
+ const list = inner ? splitPlaceholders(inner, expressions) : [];
1363
+ template = function(addChild) {
1364
+ return list.map(item => renderExpression(item, this)).flat().map(item => {
1365
+ if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
1366
+ return item instanceof Component ? addChild(item) : item;
1367
+ }
1368
+ return '';
1369
+ }).join('');
1370
+ };
1371
+ }
1372
+ } else {
1373
+ // It's a container.
1374
+ match = main.match(new RegExp(`^\\s*${PH}\\s*$`));
1375
+
1376
+ if (match) {
1377
+ // If there is only one expression and no tag, is a container.
1378
+ template = function(addChild) {
1379
+ // Replace expressions.
1380
+ return addChild(renderExpression(expressions[match[1]], this)).toString();
1381
+ };
1382
+ } else {
1383
+ throw new SyntaxError('Invalid component');
1384
+ }
1385
+ }
1386
+
1387
+ const Current = this;
1388
+ // Create subclass for this component.
1389
+ return Current.extend({
1390
+ // Set root element tag.
1391
+ tag,
1392
+ // Set attributes.
1393
+ attributes,
1394
+ // Set events.
1395
+ events,
1396
+ // Set template.
1397
+ template
1398
+ });
1399
+ }
1400
+ }
1401
+
1402
+ Component.PLACEHOLDER_EXPRESSION = (idx) => `__RASTI_{${idx}}__`;
1403
+ Component.DATA_ATTRIBUTE_UID = 'data-rasti-uid';
1404
+ Component.RENDER_TYPE_HYDRATE = 'hydrate';
1405
+ Component.RENDER_TYPE_RECYCLE = 'recycle';
1406
+ Component.RENDER_TYPE_RENDER = 'render';
1407
+
1408
+ exports.Component = Component;
1409
+ exports.Emitter = Emitter;
1410
+ exports.Model = Model;
1411
+ exports.View = View;
1412
+
1413
+ }));