rasti 3.0.0-alpha.0 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,13 +19,20 @@
19
19
 
20
20
  ## Key Features
21
21
 
22
- - 🌟 **Declarative Components**: Build dynamic UI components using intuitive template literals.
23
- - 🎯 **Event Delegation**: Simplify event handling with built-in delegation.
24
- - 🔗 **Model-View Binding**: Keep your UI and data in sync with ease.
25
- - 🌐 **Server-Side Rendering**: Render as plain text for server-side use or static builds.
26
- - ⚡ **Lightweight and Fast**: Minimal overhead with efficient rendering.
27
- - 🕰️ **Legacy Compatibility**: Seamlessly integrates into existing **Backbone.js** projects.
28
- - 📐 **Standards-Based**: Built on modern web standards, no tooling required.
22
+ - **Declarative Components** 🌟
23
+ Build dynamic UI components using intuitive template literals.
24
+ - **Event Delegation** 🎯
25
+ Simplify event handling with built-in delegation.
26
+ - **Model-View Binding** 🔗
27
+ Keep your UI and data in sync with ease.
28
+ - **Server-Side Rendering** 🌐
29
+ Render as plain text for server-side use or static builds.
30
+ - **Lightweight and Fast** ⚡
31
+ Minimal overhead with efficient rendering.
32
+ - **Legacy Compatibility** 🕰️
33
+ Seamlessly integrates into existing **Backbone.js** projects.
34
+ - **Standards-Based** 📐
35
+ Built on modern web standards, no tooling required.
29
36
 
30
37
  ## Getting Started
31
38
 
@@ -58,19 +65,20 @@ const { Model, Component } = Rasti;
58
65
  ### Create a `Component`
59
66
 
60
67
  ```javascript
61
- // Create Timer component.
62
- // Renders seconds from model.
68
+ // Define a Timer component that displays the number of seconds from the model.
63
69
  const Timer = Component.create`
64
70
  <div>
65
71
  Seconds: <span>${({ model }) => model.seconds}</span>
66
72
  </div>
67
73
  `;
68
- // Create model to store seconds.
74
+
75
+ // Create a model to store the seconds.
69
76
  const model = new Model({ seconds: 0 });
70
- // Mount timer on body.
71
- // Pass model as options.
77
+
78
+ // Mount the Timer component to the body and pass the model as an option.
72
79
  Timer.mount({ model }, document.body);
73
- // Increment `model.seconds` every second.
80
+
81
+ // Increment the `seconds` property of the model every second.
74
82
  setInterval(() => model.seconds++, 1000);
75
83
  ```
76
84
 
@@ -79,56 +87,21 @@ setInterval(() => model.seconds++, 1000);
79
87
  ### Adding sub components
80
88
 
81
89
  ```javascript
82
- // Create Button component.
83
- // Calls `options.handleClick` when clicked.
84
- // Renders children passed to it.
85
- const Button = Component.create`
86
- <button
87
- onClick=${{ '&' : function() { this.options.handleClick() } }}
88
- >
89
- ${({ options }) => options.renderChildren()}
90
- </button>
91
- `;
92
- // Create Counter component.
93
- // Adds two Buttons to increment and decrement count.
94
- // Displays current count.
95
- const Counter = Component.create`
96
- <div>
97
- <div>Counter: ${() => model.count}</div>
98
- <${Button} handleClick=${({ model }) => () => model.count++}>
99
- Increment
100
- </${Button}>
101
- <${Button} handleClick=${({ model }) => () => model.count--}>
102
- Decrement
103
- </${Button}>
104
- </div>
105
- `;
106
- // Create model to store count.
107
- const model = new Model({ count: 0 });
108
- // Mount counter on body.
109
- // Pass model as options.
110
- Counter.mount({ model }, document.body);
111
- ```
112
-
113
- [Try it on CodePen](https://codepen.io/8tentaculos/pen/ZEZarEQ?editors=0010)
114
-
115
- ### Rendering iterables
116
-
117
- ```javascript
118
- // Routes data to be used in Navigation.
90
+ // Define the routes for the navigation menu.
119
91
  const routes = [
120
- { label : 'Home', href : '#' },
121
- { label : 'Faq', href : '#faq' },
122
- { label : 'Contact', href : '#contact' },
92
+ { label: 'Home', href: '#' },
93
+ { label: 'Faq', href: '#faq' },
94
+ { label: 'Contact', href: '#contact' },
123
95
  ];
124
- // Create Link component to be used in Navigation.
96
+
97
+ // Create a Link component for navigation items.
125
98
  const Link = Component.create`
126
99
  <a href="${({ options }) => options.href}">
127
100
  ${({ options }) => options.label}
128
101
  </a>
129
102
  `;
130
- // Create Navigation component.
131
- // Iterates over routes and mounts Link components.
103
+
104
+ // Create a Navigation component that renders Link components for each route.
132
105
  const Navigation = Component.create`
133
106
  <nav>
134
107
  ${({ options }) => options.routes.map(
@@ -136,8 +109,8 @@ const Navigation = Component.create`
136
109
  )}
137
110
  </nav>
138
111
  `;
139
- // Create Page component.
140
- // Adds Navigation and displays current route label as title.
112
+
113
+ // Create a Page component that includes the Navigation and displays the current route's label as the title.
141
114
  const Page = Component.create`
142
115
  <main>
143
116
  <${Navigation} routes=${({ options }) => options.routes} />
@@ -150,12 +123,14 @@ const Page = Component.create`
150
123
  </section>
151
124
  </main>
152
125
  `;
153
- // Local state to store location.
154
- const model = new Model({ location : document.location.hash });
155
- // Listen to location changes and update state.
126
+
127
+ // Initialize a model to store the current location.
128
+ const model = new Model({ location: document.location.hash });
129
+
130
+ // Update the model's location state when the browser's history changes.
156
131
  window.addEventListener('popstate', () => model.location = document.location.hash);
157
- // Mount counter on body.
158
- // Pass routes and model as options.
132
+
133
+ // Mount the Page component to the body, passing the routes and model as options.
159
134
  Page.mount({ routes, model }, document.body);
160
135
  ```
161
136
 
package/dist/rasti.js CHANGED
@@ -47,7 +47,8 @@
47
47
  * @param {string} type Type of the event (e.g. `change`).
48
48
  * @param {function} listener Callback function to be called when the event is emitted.
49
49
  * @example
50
- * this.model.on('change', this.render.bind(this)); // Re render when model changes.
50
+ * // Re render when model changes.
51
+ * this.model.on('change', this.render.bind(this));
51
52
  */
52
53
  on(type, listener) {
53
54
  // Validate listener.
@@ -68,6 +69,7 @@
68
69
  * @param {string} type Type of the event (e.g. `change`).
69
70
  * @param {function} listener Callback function to be called when the event is emitted.
70
71
  * @example
72
+ * // Log a message once when model changes.
71
73
  * this.model.once('change', () => console.log('This will happen once'));
72
74
  */
73
75
  once(type, listener) {
@@ -90,7 +92,8 @@
90
92
  * @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.
91
93
  * @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
94
  * @example
93
- * this.model.off('change'); // Stop listening to changes.
95
+ * // Stop listening to changes.
96
+ * this.model.off('change');
94
97
  */
95
98
  off(type, listener) {
96
99
  // No listeners.
@@ -119,7 +122,8 @@
119
122
  * @param {string} type Type of the event (e.g. `change`).
120
123
  * @param {any} [...args] Arguments to be passed to listener.
121
124
  * @example
122
- * this.emit('invalid'); // Emit validation error event.
125
+ * // Emit validation error event.
126
+ * this.emit('invalid');
123
127
  */
124
128
  emit(type, ...args) {
125
129
  // No listeners.
@@ -155,13 +159,13 @@
155
159
  * that contain all the necessary functions for manipulating their specific data.
156
160
  * Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
157
161
  * 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
162
+ * constructor `attributes` parameter. For every attribute, a getter is generated to retrieve the model property
159
163
  * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
160
164
  * and `change:attribute` events.
161
165
  * @module
162
166
  * @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.
167
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributes`, in order to emit `change` events.
168
+ * @property {object|function} 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
169
  * @property {object} previous Object containing previous attributes when a change occurs.
166
170
  * @example
167
171
  * import { Model } from 'rasti';
@@ -194,14 +198,14 @@
194
198
  * product.setDiscount(10); // Output: "New Price: 900"
195
199
  */
196
200
  class Model extends Emitter {
197
- constructor(attrs = {}) {
201
+ constructor(attributes = {}) {
198
202
  super();
199
203
  // Call preinitialize.
200
204
  this.preinitialize.apply(this, arguments);
201
205
  // Get defaults. If `this.defaults` is a function, call it.
202
206
  const defaults = getResult(this.defaults, this) || {};
203
207
  // Set attributes object with defaults and passed attributes.
204
- this.attributes = Object.assign({}, defaults, attrs);
208
+ this.attributes = Object.assign({}, defaults, attributes);
205
209
  // Object to store previous attributes when a change occurs.
206
210
  this.previous = {};
207
211
  // Generate getters/setters for every attribute.
@@ -210,7 +214,7 @@
210
214
 
211
215
  /**
212
216
  * 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`.
217
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`.
214
218
  */
215
219
  preinitialize() {}
216
220
 
@@ -330,7 +334,7 @@
330
334
  };
331
335
 
332
336
  /**
333
- * - Listens for changes and renders UI.
337
+ * - Listens for changes and renders the UI.
334
338
  * - Handles user input and interactivity.
335
339
  * - Sends captured input to the model.
336
340
  *
@@ -340,16 +344,16 @@
340
344
  * emitted by the models to re-render themselves based on changes.
341
345
  * Each `View` has a root element, `this.el`, which is used for event delegation.
342
346
  * 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`.
347
+ * If `this.el` is not present, an element will be created using `this.tag` (defaulting to `div`) and `this.attributes`.
344
348
  * @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).
349
+ * @extends Emitter
350
+ * @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.
351
+ * @property {node|function} el Every view has a root DOM element stored at `this.el`. If not present, it will be created. If `this.el` is a function, it will be called to get the element at `this.ensureElement`, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
352
+ * @property {string|function} tag If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. Default is `div`. If it is a function, it will be called to get the tag, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
353
+ * @property {object|function} attributes If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. If it is a function, it will be called to get the attributes object, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
354
+ * @property {object|function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.
355
+ * @property {object} model A model or any object containing data and business logic.
356
+ * @property {function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
353
357
  * @example
354
358
  * import { View } from 'rasti';
355
359
  *
@@ -522,24 +526,39 @@
522
526
  }
523
527
 
524
528
  /**
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.
529
+ * Provide declarative listeners for DOM events within a view. If an events object is not provided,
530
+ * it defaults to using `this.events`. If `this.events` is a function, it will be called to get the events object.
531
+ *
532
+ * The events object should follow the format `{'event selector': 'listener'}`:
533
+ * - `event`: The type of event (e.g., 'click').
534
+ * - `selector`: A CSS selector to match the event target. If omitted, the event is bound to the root element.
535
+ * - `listener`: A function or a string representing a method name on the view. The method will be called with `this` bound to the view instance.
536
+ *
537
+ * By default, `delegateEvents` is called within the View's constructor. If you have a simple events object,
538
+ * all of your DOM events will be connected automatically, and you will not need to call this function manually.
539
+ *
540
+ * All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.
541
+ * When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.
542
+ *
543
+ * The listeners will be invoked with the event and the view as arguments.
544
+ *
545
+ * @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
546
+ * @return {Rasti.View} Returns `this` for chaining.
539
547
  * @example
540
- * MyView.prototype.events = {
541
- * 'click button.ok' : 'onClickOkButton',
542
- * 'click button.cancel' : function() {}
548
+ * // Using a function.
549
+ * class Modal extends View {
550
+ * events() {
551
+ * return {
552
+ * 'click button.ok': 'onClickOkButton',
553
+ * 'click button.cancel': function() {}
554
+ * };
555
+ * }
556
+ * }
557
+ *
558
+ * // Using an object.
559
+ * Modal.prototype.events = {
560
+ * 'click button.ok' : 'onClickOkButton',
561
+ * 'click button.cancel' : function() {}
543
562
  * };
544
563
  */
545
564
  delegateEvents(events) {
@@ -589,7 +608,7 @@
589
608
  /**
590
609
  * Removes all of the view's delegated events.
591
610
  * Useful if you want to disable or remove a view from the DOM temporarily.
592
- * Called automatically when the view is destroyed.
611
+ * Called automatically when the view is destroyed and when `delegateEvents` is called again.
593
612
  * @return {Rasti.View} Return `this` for chaining.
594
613
  */
595
614
  undelegateEvents() {
@@ -605,16 +624,16 @@
605
624
  /**
606
625
  * Renders the view.
607
626
  * This method should be overridden with custom logic.
608
- * The convention is to only manipulate the DOM within the scope of `this.el`,
627
+ * The only convention is to manipulate the DOM within the scope of `this.el`,
609
628
  * and to return `this` for chaining.
610
- * If you add any child views, you must call `this.destroyChildren`.
629
+ * If you add any child views, you should call `this.destroyChildren` before re-rendering.
611
630
  * The default implementation sets the innerHTML of `this.el` with the result
612
631
  * of calling `this.template`, passing `this.model` as an argument.
613
632
  * <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
633
+ * for rendering, which may introduce Cross-Site Scripting (XSS) risks. Ensure that any user-generated
615
634
  * content is properly sanitized before inserting it into the DOM. For best practices on secure data handling,
616
635
  * 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.
636
+ * @return {Rasti.View} Returns `this` for chaining.
618
637
  */
619
638
  render() {
620
639
  if (this.template) this.el.innerHTML = this.template(this.model);
@@ -628,6 +647,17 @@
628
647
  */
629
648
  View.uid = 0;
630
649
 
650
+ /*
651
+ * Flatten an array recursively
652
+ * @param {Array} arr Array to flat recursively
653
+ * @return {Array} Flat array
654
+ */
655
+ const deepFlat = (arr) => arr.reduce((acc, val) => {
656
+ if (Array.isArray(val)) acc.push(...deepFlat(val));
657
+ else acc.push(val);
658
+ return acc;
659
+ }, []);
660
+
631
661
  /*
632
662
  * Same as getResult, but pass context as argument to the expression.
633
663
  * Used to evaluate expressions in the context of a component.
@@ -752,7 +782,7 @@
752
782
  const list = splitPlaceholders(expandComponents(inner, expressions), expressions);
753
783
  // Create renderChildren function.
754
784
  renderChildren = function() {
755
- return list.map(item => renderExpression(item, this)).flat();
785
+ return deepFlat(list.map(item => renderExpression(item, this)));
756
786
  };
757
787
  }
758
788
  // Create mount function.
@@ -821,6 +851,14 @@
821
851
  return data;
822
852
  };
823
853
 
854
+ /*
855
+ * HTML tags that are self closing.
856
+ */
857
+ const selfClosingTags = {
858
+ area : true, base : true, br : true, col : true, embed : true, hr : true,
859
+ img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
860
+ };
861
+
824
862
  /*
825
863
  * These option keys will be extended on the component instance.
826
864
  */
@@ -832,26 +870,19 @@
832
870
  onRender : true
833
871
  };
834
872
 
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
873
  /**
844
874
  * Components are a special kind of `View` that is designed to be easily composable,
845
875
  * making it simple to add child views and build complex user interfaces.
846
876
  * Unlike views, which are render-agnostic, components have a specific set of rendering
847
877
  * guidelines that allow for a more declarative development style.
848
- * Components are defined with the `create` static method, which takes a tagged template.
878
+ * Components are defined with the {@link #module_component_create Component.create} static method, which takes a tagged template string or a function that returns another component.
849
879
  * @module
850
880
  * @extends Rasti.View
851
881
  * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
852
882
  * @property {string} key A unique key to identify the component. Used to recycle child components.
853
883
  * @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
884
  * @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.
885
+ * @see {@link #module_component_create Component.create}
855
886
  * @example
856
887
  * import { Component, Model } from 'rasti';
857
888
  * // Create Timer component.
@@ -1091,13 +1122,13 @@
1091
1122
  * });
1092
1123
  */
1093
1124
  partial(strings, ...expressions) {
1094
- return splitPlaceholders(
1125
+ return deepFlat(splitPlaceholders(
1095
1126
  expandComponents(
1096
1127
  addPlaceholders(strings, expressions), expressions
1097
1128
  ), expressions
1098
1129
  ).map(item => {
1099
1130
  return renderExpression(item, this);
1100
- }).flat();
1131
+ }));
1101
1132
  }
1102
1133
 
1103
1134
  getRecyclePlaceholder() {
@@ -1283,38 +1314,96 @@
1283
1314
  }
1284
1315
 
1285
1316
  /**
1286
- * Takes a tagged template containing an HTML string,
1287
- * and returns a new `Component` class.
1317
+ * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
1288
1318
  * - 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
1319
  * - 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.
1320
+ * ```javascript
1321
+ * const Button = Component.create`<button class="button">Click me</button>`;
1322
+ * ```
1323
+ * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it. If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
1324
+ * ```javascript
1325
+ * const Button = Component.create`
1326
+ * <button class="${({ options }) => options.className}">
1327
+ * ${({ options }) => options.renderChildren()}
1328
+ * </button>
1329
+ * `;
1330
+ * ```
1331
+ * - Event handlers should be passed, at the root element as camelized attributes, in the format `onEventName=${{'selector' : listener }}`. They will be transformed to an event object and delegated to the root element. See {@link #module_view__delegateevents View.delegateEvents}.
1332
+ * - Boolean attributes should be passed in the form of `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
1333
+ * ```javascript
1334
+ * const Input = Component.create`
1335
+ * <input type="text" disabled=${({ options }) => options.disabled} />
1336
+ * `;
1337
+ * ```
1338
+ * - If the interpolated function returns a component instance, it will be added as a child component.
1339
+ * - If the interpolated function returns an array, each item will be evaluated as above.
1340
+ * ```javascript
1341
+ * // Create a button component.
1342
+ * const Button = Component.create`
1343
+ * <button class="button">
1344
+ * ${({ options }) => options.renderChildren()}
1345
+ * </button>
1346
+ * `;
1347
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1348
+ * const Navigation = Component.create`
1349
+ * <nav>
1350
+ * ${({ options }) => options.items.map(
1351
+ * item => Button.mount({ renderChildren: () => item.label })
1352
+ * )}
1353
+ * </nav>
1354
+ * `;
1355
+ * // Create a header component. Add navigation as a child.
1356
+ * const Header = Component.create`
1357
+ * <header>
1358
+ * ${({ options }) => Navigation.mount({ items : options.items})}
1359
+ * </header>
1360
+ * `;
1361
+ * ```
1362
+ * - Child components can be added using a component tag.
1363
+ * ```javascript
1364
+ * // Create a button component.
1365
+ * const Button = Component.create`
1366
+ * <button class="button">
1367
+ * ${({ options }) => options.renderChildren()}
1368
+ * </button>
1369
+ * `;
1370
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1371
+ * const Navigation = Component.create`
1372
+ * <nav>
1373
+ * ${(self) => self.options.items.map(
1374
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
1375
+ * )}
1376
+ * </nav>
1377
+ * `;
1378
+ * // Create a header component. Add navigation as a child.
1379
+ * const Header = Component.create`
1380
+ * <header>
1381
+ * <${Navigation} items="${({ options }) => options.items}" />
1382
+ * </header>
1383
+ * `;
1384
+ * ```
1296
1385
  * - 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.
1386
+ * ```javascript
1387
+ * // Create a button component.
1388
+ * const Button = Component.create`
1389
+ * <button class="${({ options }) => options.className}">
1390
+ * ${self => self.renderChildren()}
1391
+ * </button>
1392
+ * `;
1393
+ * // Create a container using the button component
1394
+ * const ButtonOk = Component.create`
1395
+ * <${Button} className="ok">Ok</${Button}>
1396
+ * `;
1397
+ * // Create a button component using a function
1398
+ * const ButtonCancel = Component.create(() => Button.mount({
1399
+ * className: 'cancel',
1400
+ * renderChildren: () => 'Cancel'
1401
+ * }));
1402
+ * ```
1297
1403
  * @static
1298
1404
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
1299
1405
  * @param {...*} expressions - The expressions to be interpolated within the template.
1300
1406
  * @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
1407
  */
1319
1408
  static create(strings, ...expressions) {
1320
1409
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -1361,7 +1450,7 @@
1361
1450
  if (close) {
1362
1451
  const list = inner ? splitPlaceholders(inner, expressions) : [];
1363
1452
  template = function(addChild) {
1364
- return list.map(item => renderExpression(item, this)).flat().map(item => {
1453
+ return deepFlat(list.map(item => renderExpression(item, this))).map(item => {
1365
1454
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
1366
1455
  return item instanceof Component ? addChild(item) : item;
1367
1456
  }
package/dist/rasti.min.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";class e{on(t,e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),()=>this.off(t,e)}once(t,e){if("function"==typeof e){const s=this,i=e;e=function(...n){i(...n),s.off(t,e)}}return this.on(t,e)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((function(t){t(...e)}))}}var s=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);const i={el:!0,tag:!0,attributes:!0,events:!0,model:!0,template:!0,onDestroy:!0};class n extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.uid="uid"+ ++n.uid,this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],Object.keys(t).forEach((e=>{i[e]&&(this[e]=t[e])})),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureElement(){if(this.el)this.el=s(this.el,this);else{const t=s(this.tag,this),e=s(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.parentNode.removeChild(this.el),this}delegateEvents(t){if(t||(t=s(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let e={};return Object.keys(t).forEach((s=>{const i=s.split(" "),n=i.shift(),r=i.join(" ");let h=t[s];h=("string"==typeof h?this[h]:h).bind(this),e[n]||(e[n]=[]),e[n].push({selector:r,listener:h})})),Object.keys(e).forEach((t=>{const s=s=>{e[t].forEach((({selector:t,listener:e})=>{t&&!s.target.closest(t)||e(s,this)}))};this.delegatedEventListeners.push({type:t,listener:s}),this.el.addEventListener(t,s)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}}n.uid=0;const r=(t,e)=>s(t,e,e),h=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(E.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),l=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)"),i=new RegExp(`${s}`,"g"),n=[];let r,h=0;for(;null!==(r=i.exec(t));){const s=t.slice(h,r.index);n.push(s,e[r[1]]),h=r.index+r[0].length}return n.push(t.slice(h)),n},o=(t,e)=>{const s=t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t.all=Object.assign(t.all,i):"string"==typeof i&&(t.all[i]=!0);else{const n=e(s[1]);t.all[i]=n}return t}),{all:{},events:{},attributes:{}});return Object.keys(s.all).forEach((t=>{const e=t.match(/on(([A-Z]{1}[a-z]+)+)/);e&&e[1]?s.events[e[1].toLowerCase()]=s.all[t]:s.attributes[t]=s.all[t]})),s},c=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)");return t.replace(new RegExp(`<(${s})([^>]*)>([\\s\\S]*?)</(${s})>|<(${s})([^>]*)/>`,"g"),(function(){const{tag:t,attributes:s,inner:i,close:n,raw:h}=a(arguments,e);if(!(t.prototype instanceof E))return h;let d;if(n){if(t!==n)return h;const s=l(c(i,e),e);d=function(){return s.map((t=>r(t,this))).flat()}}return e.push((function(){const e=o(s,(t=>r(t,this))).all;return d&&(e.renderChildren=d.bind(this)),t.mount(e)})),E.PLACEHOLDER_EXPRESSION(e.length-1)}))},a=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,l,o,c,a,d,u]=t,f={raw:i,attributes:[]};let p;n?(f.tag=void 0!==r?e[r]:n,f.inner=l,f.close=void 0!==c?e[c]:o,p=h):(f.tag=void 0!==d?e[d]:a,p=u);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(p));){const[,t,s,,i,n]=g,r=void 0!==s?e[s]:t,h=void 0!==i?e[i]:n;void 0!==h?f.attributes.push([r,h]):f.attributes.push([r])}return f},d={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0},u={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,source:!0,track:!0,wbr:!0};class E extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{d[e]&&(this[e]=t[e])})),this.options=t,this.onCreate.apply(this,arguments)}subscribe(t){if(!t.on)return;const e=this.onChange.bind(this),s=t.on("change",e);return this.destroyQueue.push("function"==typeof s?s:()=>t.off("change",e)),this}isContainer(){return!(this.tag||!this.template)}ensureElement(){this.el&&(this.el=s(this.el,this),this.delegateEvents())}findElement(t){return(t||document).querySelector(`[${E.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],n={[E.DATA_ATTRIBUTE_UID]:this.uid};return this.attributes&&Object.assign(n,s(this.attributes,this)),Object.keys(n).forEach((s=>{let r=n[s];!1===r?e[s]=!0:!0===r?(t[s]="",i.push(s)):(null==r&&(r=""),t[s]=r,i.push(`${s}="${r}"`))})),{add:t,remove:e,html:i.join(" ")}}hydrate(t){return this.model&&this.subscribe(this.model),this.state&&this.subscribe(this.state),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.el=this.findElement(t),this.delegateEvents(),this.children.forEach((t=>t.hydrate(this.el)))),this.onRender.call(this,E.RENDER_TYPE_HYDRATE),this}recycle(t){if(this.isContainer())return this.children[0].recycle(t);return this.findElement(t).replaceWith(this.el),this.onRender.call(this,E.RENDER_TYPE_RECYCLE),this}destroy(){return super.destroy.apply(this,arguments),this.destroyed=!0,this}onCreate(){}onChange(){this.render()}onRender(){}onDestroy(){}partial(t,...e){return l(c(h(t,e),e),e).map((t=>r(t,this))).flat()}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${E.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!u[t]?`<${t} ${e}></${t}>`:`<${t} ${e} />`}toString(){if(this.destroyChildren(),this.isContainer())return this.template.call(this,this.addChild.bind(this));const t=s(this.tag,this)||"div",e=this.getAttributes().html,i=this.template?this.template.call(this,this.addChild.bind(this)):"";return this.template||!u[t]?`<${t} ${e}>${i}</${t}>`:`<${t} ${e} />`}render(){if(this.destroyed)return this;if(!this.isContainer()){if(!this.el){const t=this.createElement("template");return t.innerHTML=this,this.hydrate(t.content),this}const t=this.getAttributes();Object.keys(t.remove).forEach((t=>{this.el.removeAttribute(t)})),Object.keys(t.add).forEach((e=>{this.el.setAttribute(e,t.add[e])}))}if(this.template){const t=document.activeElement,e=[],s=[],i=this.children;this.children=[];const n=this.template.call(this,(t=>{let n=t;const r=t.key&&i.find((e=>e.key===t.key));return r?(n=r.getRecyclePlaceholder(),s.push(r),t.destroy()):e.push(t),n}));if(this.isContainer())if(e[0]){const t=this.createElement("template");t.innerHTML=n,this.addChild(e[0]).hydrate(t.content);const s=t.content.children[0];this.el&&this.el.replaceWith(s),this.el=s}else{if(!s[0])throw new Error("Container component must have a child component");this.addChild(s[0])}else this.el.innerHTML=n,e.forEach((t=>{this.addChild(t).hydrate(this.el)})),s.forEach((t=>{this.addChild(t).recycle(this.el)}));i.forEach((t=>{s.indexOf(t)>-1||t.destroy()})),this.el.contains(t)&&t.focus()}return this.onRender.call(this,E.RENDER_TYPE_RENDER),this}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.appendChild(i.render().el)),i}static create(t,...e){const s=E.PLACEHOLDER_EXPRESSION("(\\d+)");let i,n,d,u;"function"==typeof t&&(e=[t],t=["",""]);const f=c(h(t,e),e);let p=f.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(p){const{tag:t,attributes:s,inner:h,close:c}=a(p,e);if(i=function(){return r(t,this)},n=function(){return o(s,(t=>r(t,this))).attributes},d=function(){const t=o(s,(t=>r(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=r(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},c){const t=h?l(h,e):[];u=function(e){return t.map((t=>r(t,this))).flat().map((t=>null!=t&&!1!==t&&!0!==t?t instanceof E?e(t):t:"")).join("")}}}else{if(p=f.match(new RegExp(`^\\s*${s}\\s*$`)),!p)throw new SyntaxError("Invalid component");u=function(t){return t(r(e[p[1]],this)).toString()}}return this.extend({tag:i,attributes:n,events:d,template:u})}}E.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,E.DATA_ATTRIBUTE_UID="data-rasti-uid",E.RENDER_TYPE_HYDRATE="hydrate",E.RENDER_TYPE_RECYCLE="recycle",E.RENDER_TYPE_RENDER="render",t.Component=E,t.Emitter=e,t.Model=class extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments);const e=s(this.defaults,this)||{};this.attributes=Object.assign({},e,t),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,t,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const l=Object.keys(h);if(l.length&&(this._pending=["change",this,h,...n]),l.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}toJSON(){return Object.assign({},this.attributes)}},t.View=n}));
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";class e{on(t,e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),()=>this.off(t,e)}once(t,e){if("function"==typeof e){const s=this,i=e;e=function(...n){i(...n),s.off(t,e)}}return this.on(t,e)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((function(t){t(...e)}))}}var s=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);const i={el:!0,tag:!0,attributes:!0,events:!0,model:!0,template:!0,onDestroy:!0};class n extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.uid="uid"+ ++n.uid,this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],Object.keys(t).forEach((e=>{i[e]&&(this[e]=t[e])})),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureElement(){if(this.el)this.el=s(this.el,this);else{const t=s(this.tag,this),e=s(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.parentNode.removeChild(this.el),this}delegateEvents(t){if(t||(t=s(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let e={};return Object.keys(t).forEach((s=>{const i=s.split(" "),n=i.shift(),r=i.join(" ");let h=t[s];h=("string"==typeof h?this[h]:h).bind(this),e[n]||(e[n]=[]),e[n].push({selector:r,listener:h})})),Object.keys(e).forEach((t=>{const s=s=>{e[t].forEach((({selector:t,listener:e})=>{t&&!s.target.closest(t)||e(s,this)}))};this.delegatedEventListeners.push({type:t,listener:s}),this.el.addEventListener(t,s)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}}n.uid=0;const r=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...r(e)):t.push(e),t)),[]),h=(t,e)=>s(t,e,e),l=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(f.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),o=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),i=new RegExp(`${s}`,"g"),n=[];let r,h=0;for(;null!==(r=i.exec(t));){const s=t.slice(h,r.index);n.push(s,e[r[1]]),h=r.index+r[0].length}return n.push(t.slice(h)),n},c=(t,e)=>{const s=t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t.all=Object.assign(t.all,i):"string"==typeof i&&(t.all[i]=!0);else{const n=e(s[1]);t.all[i]=n}return t}),{all:{},events:{},attributes:{}});return Object.keys(s.all).forEach((t=>{const e=t.match(/on(([A-Z]{1}[a-z]+)+)/);e&&e[1]?s.events[e[1].toLowerCase()]=s.all[t]:s.attributes[t]=s.all[t]})),s},a=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)");return t.replace(new RegExp(`<(${s})([^>]*)>([\\s\\S]*?)</(${s})>|<(${s})([^>]*)/>`,"g"),(function(){const{tag:t,attributes:s,inner:i,close:n,raw:l}=d(arguments,e);if(!(t.prototype instanceof f))return l;let u;if(n){if(t!==n)return l;const s=o(a(i,e),e);u=function(){return r(s.map((t=>h(t,this))))}}return e.push((function(){const e=c(s,(t=>h(t,this))).all;return u&&(e.renderChildren=u.bind(this)),t.mount(e)})),f.PLACEHOLDER_EXPRESSION(e.length-1)}))},d=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,l,o,c,a,d,u]=t,E={raw:i,attributes:[]};let p;n?(E.tag=void 0!==r?e[r]:n,E.inner=l,E.close=void 0!==c?e[c]:o,p=h):(E.tag=void 0!==d?e[d]:a,p=u);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(p));){const[,t,s,,i,n]=g,r=void 0!==s?e[s]:t,h=void 0!==i?e[i]:n;void 0!==h?E.attributes.push([r,h]):E.attributes.push([r])}return E},u={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,source:!0,track:!0,wbr:!0},E={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0};class f extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{E[e]&&(this[e]=t[e])})),this.options=t,this.onCreate.apply(this,arguments)}subscribe(t){if(!t.on)return;const e=this.onChange.bind(this),s=t.on("change",e);return this.destroyQueue.push("function"==typeof s?s:()=>t.off("change",e)),this}isContainer(){return!(this.tag||!this.template)}ensureElement(){this.el&&(this.el=s(this.el,this),this.delegateEvents())}findElement(t){return(t||document).querySelector(`[${f.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],n={[f.DATA_ATTRIBUTE_UID]:this.uid};return this.attributes&&Object.assign(n,s(this.attributes,this)),Object.keys(n).forEach((s=>{let r=n[s];!1===r?e[s]=!0:!0===r?(t[s]="",i.push(s)):(null==r&&(r=""),t[s]=r,i.push(`${s}="${r}"`))})),{add:t,remove:e,html:i.join(" ")}}hydrate(t){return this.model&&this.subscribe(this.model),this.state&&this.subscribe(this.state),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.el=this.findElement(t),this.delegateEvents(),this.children.forEach((t=>t.hydrate(this.el)))),this.onRender.call(this,f.RENDER_TYPE_HYDRATE),this}recycle(t){if(this.isContainer())return this.children[0].recycle(t);return this.findElement(t).replaceWith(this.el),this.onRender.call(this,f.RENDER_TYPE_RECYCLE),this}destroy(){return super.destroy.apply(this,arguments),this.destroyed=!0,this}onCreate(){}onChange(){this.render()}onRender(){}onDestroy(){}partial(t,...e){return r(o(a(l(t,e),e),e).map((t=>h(t,this))))}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${f.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!u[t]?`<${t} ${e}></${t}>`:`<${t} ${e} />`}toString(){if(this.destroyChildren(),this.isContainer())return this.template.call(this,this.addChild.bind(this));const t=s(this.tag,this)||"div",e=this.getAttributes().html,i=this.template?this.template.call(this,this.addChild.bind(this)):"";return this.template||!u[t]?`<${t} ${e}>${i}</${t}>`:`<${t} ${e} />`}render(){if(this.destroyed)return this;if(!this.isContainer()){if(!this.el){const t=this.createElement("template");return t.innerHTML=this,this.hydrate(t.content),this}const t=this.getAttributes();Object.keys(t.remove).forEach((t=>{this.el.removeAttribute(t)})),Object.keys(t.add).forEach((e=>{this.el.setAttribute(e,t.add[e])}))}if(this.template){const t=document.activeElement,e=[],s=[],i=this.children;this.children=[];const n=this.template.call(this,(t=>{let n=t;const r=t.key&&i.find((e=>e.key===t.key));return r?(n=r.getRecyclePlaceholder(),s.push(r),t.destroy()):e.push(t),n}));if(this.isContainer())if(e[0]){const t=this.createElement("template");t.innerHTML=n,this.addChild(e[0]).hydrate(t.content);const s=t.content.children[0];this.el&&this.el.replaceWith(s),this.el=s}else{if(!s[0])throw new Error("Container component must have a child component");this.addChild(s[0])}else this.el.innerHTML=n,e.forEach((t=>{this.addChild(t).hydrate(this.el)})),s.forEach((t=>{this.addChild(t).recycle(this.el)}));i.forEach((t=>{s.indexOf(t)>-1||t.destroy()})),this.el.contains(t)&&t.focus()}return this.onRender.call(this,f.RENDER_TYPE_RENDER),this}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.appendChild(i.render().el)),i}static create(t,...e){const s=f.PLACEHOLDER_EXPRESSION("(\\d+)");let i,n,u,E;"function"==typeof t&&(e=[t],t=["",""]);const p=a(l(t,e),e);let y=p.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(y){const{tag:t,attributes:s,inner:l,close:a}=d(y,e);if(i=function(){return h(t,this)},n=function(){return c(s,(t=>h(t,this))).attributes},u=function(){const t=c(s,(t=>h(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=h(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},a){const t=l?o(l,e):[];E=function(e){return r(t.map((t=>h(t,this)))).map((t=>null!=t&&!1!==t&&!0!==t?t instanceof f?e(t):t:"")).join("")}}}else{if(y=p.match(new RegExp(`^\\s*${s}\\s*$`)),!y)throw new SyntaxError("Invalid component");E=function(t){return t(h(e[y[1]],this)).toString()}}return this.extend({tag:i,attributes:n,events:u,template:E})}}f.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,f.DATA_ATTRIBUTE_UID="data-rasti-uid",f.RENDER_TYPE_HYDRATE="hydrate",f.RENDER_TYPE_RECYCLE="recycle",f.RENDER_TYPE_RENDER="render",t.Component=f,t.Emitter=e,t.Model=class extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments);const e=s(this.defaults,this)||{};this.attributes=Object.assign({},e,t),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,t,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const l=Object.keys(h);if(l.length&&(this._pending=["change",this,h,...n]),l.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}toJSON(){return Object.assign({},this.attributes)}},t.View=n}));