rasti 3.0.0-alpha.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,23 +20,23 @@
20
20
  ## Key Features
21
21
 
22
22
  - **Declarative Components** 🌟
23
- Build dynamic UI components using intuitive template literals.
23
+ Build dynamic UI components using intuitive template literals.
24
24
  - **Event Delegation** 🎯
25
- Simplify event handling with built-in delegation.
25
+ Simplify event handling with built-in delegation.
26
26
  - **Model-View Binding** 🔗
27
- Keep your UI and data in sync with ease.
27
+ Keep your UI and data in sync with ease.
28
28
  - **Server-Side Rendering** 🌐
29
- Render as plain text for server-side use or static builds.
29
+ Render as plain text for server-side use or static builds.
30
30
  - **Lightweight and Fast** ⚡
31
- Minimal overhead with efficient rendering.
31
+ Minimal overhead with efficient rendering.
32
32
  - **Legacy Compatibility** 🕰️
33
- Seamlessly integrates into existing **Backbone.js** projects.
33
+ Seamlessly integrates into existing **Backbone.js** projects.
34
34
  - **Standards-Based** 📐
35
- Built on modern web standards, no tooling required.
35
+ Built on modern web standards, no tooling required.
36
36
 
37
37
  ## Getting Started
38
38
 
39
- ### Using npm
39
+ ### Installing via npm
40
40
 
41
41
  ```bash
42
42
  $ npm install rasti
@@ -52,7 +52,7 @@ import { Model, Component } from 'rasti';
52
52
  import { Model, Component } from 'https://esm.run/rasti';
53
53
  ```
54
54
 
55
- ### Using `<script>` tag
55
+ ### Using a `<script>` tag
56
56
 
57
57
  ```html
58
58
  <script src="https://cdn.jsdelivr.net/npm/rasti/dist/rasti.min.js"></script>
@@ -97,27 +97,27 @@ const routes = [
97
97
  // Create a Link component for navigation items.
98
98
  const Link = Component.create`
99
99
  <a href="${({ options }) => options.href}">
100
- ${({ options }) => options.label}
100
+ ${({ options }) => options.renderChildren()}
101
101
  </a>
102
102
  `;
103
103
 
104
104
  // Create a Navigation component that renders Link components for each route.
105
105
  const Navigation = Component.create`
106
106
  <nav>
107
- ${self => self.options.routes.map(
108
- ({ label, href }) => self.partial`<${Link} ${{ label, href }} />`
107
+ ${({ options, partial }) => options.routes.map(
108
+ ({ label, href }) => partial`<${Link} href="${href}">${label}</${Link}>`
109
109
  )}
110
110
  </nav>
111
111
  `;
112
112
 
113
- // Create a Page component that includes the Navigation and displays the current route's label as the title.
114
- const Page = Component.create`
113
+ // Create a Main component that includes the Navigation and displays the current route's label as the title.
114
+ const Main = Component.create`
115
115
  <main>
116
116
  <${Navigation} routes=${({ options }) => options.routes} />
117
117
  <section>
118
118
  <h1>
119
- ${({ state, options }) => options.routes.find(
120
- ({ href }) => href === (state.location || '#')
119
+ ${({ model, options }) => options.routes.find(
120
+ ({ href }) => href === (model.location || '#')
121
121
  ).label}
122
122
  </h1>
123
123
  </section>
@@ -130,24 +130,26 @@ const model = new Model({ location: document.location.hash });
130
130
  // Update the model's location state when the browser's history changes.
131
131
  window.addEventListener('popstate', () => model.location = document.location.hash);
132
132
 
133
- // Mount the Page component to the body, passing the routes and model as options.
134
- Page.mount({ routes, model }, document.body);
133
+ // Mount the Main component to the body, passing the routes and model as options.
134
+ Main.mount({ routes, model }, document.body);
135
135
  ```
136
136
 
137
137
  [Try it on CodePen](https://codepen.io/8tentaculos/pen/dyBMNbq?editors=0010)
138
138
 
139
139
  ## Why Choose **Rasti**?
140
140
 
141
- ### Why Choose Rasti?
142
-
143
- - **Lightweight Applications**: Designed for streamlined apps, free from unnecessary overhead or tooling.
144
- - **High-Performance Rendering**: Optimized for scenarios where speed matters, delivering fast updates and efficient rendering—ideal for large dynamic tables or datasets without needing virtual scrolling.
145
- - **Seamless Legacy Integration**: Modernize your **Backbone.js** views progressively, enabling incremental improvements without a full rewrite.
141
+ **Rasti** is built for developers who want a simple yet powerful way to create UI components without the complexity of heavy frameworks. Whether you're prototyping, building a high-performance dashboard, or modernizing a **Backbone.js** app, **Rasti** lets you:
146
142
 
143
+ - **Skip the Setup**
144
+ No installations, no build tools—just load it and start coding.
145
+ - **Lightweight and Efficient**
146
+ Minimal footprint with optimized performance, ensuring smooth updates.
147
+ - **Upgrade Legacy Code Without a Rewrite**
148
+ Incrementally enhance **Backbone.js** views while keeping existing functionality.
147
149
 
148
150
  ## Example
149
151
 
150
- The rasti [GitHub repository](https://github.com/8tentaculos/rasti) includes, in the [example folder](https://github.com/8tentaculos/rasti/tree/master/example/todo), an example [TODO application](https://rasti.js.org/example/todo/index.html) that can be used as starter project.
152
+ You can find a sample **TODO application** in the [example folder](https://github.com/8tentaculos/rasti/tree/master/example/todo) of the **Rasti** [GitHub repository](https://github.com/8tentaculos/rasti). This example serves as a great starting point for your own projects. Try it live [here](https://rasti.js.org/example/todo/index.html).
151
153
 
152
154
  ## API Documentation
153
155
 
package/dist/rasti.js CHANGED
@@ -645,9 +645,9 @@
645
645
 
646
646
  /**
647
647
  * Escape HTML entities in a string.
648
- * Use method to sanitize user-generated content before inserting it into the DOM.
648
+ * Use this method to sanitize user-generated content before inserting it into the DOM.
649
649
  * Override this method to provide a custom escape function.
650
- * This method is used by `Component` to escape template interpolations.
650
+ * This method is inherited by {@link #module_component Component} and used to escape template interpolations.
651
651
  * @static
652
652
  * @param {string} str String to escape.
653
653
  * @return {string} Escaped string.
@@ -941,6 +941,8 @@
941
941
  });
942
942
  // Store options by default.
943
943
  this.options = options;
944
+ // Bind `partial` method to `this`.
945
+ this.partial = this.partial.bind(this);
944
946
  // Call lifecycle method.
945
947
  this.onCreate.apply(this, arguments);
946
948
  }
@@ -1137,7 +1139,9 @@
1137
1139
  * Tagged template helper method.
1138
1140
  * Used to create a partial template.
1139
1141
  * It will return a one-dimensional array with strings and expressions.
1140
- * Components will be added as children by the parent component. Template strings will be marked as safe HTML to be rendered.
1142
+ * Components will be added as children by the parent component. Template strings literals
1143
+ * will be marked as safe HTML to be rendered.
1144
+ * This method is bound to the component instance by default.
1141
1145
  * @param {TemplateStringsArray} strings - Template strings.
1142
1146
  * @param {...any} expressions - Template expressions.
1143
1147
  * @return {Array} Array containing strings and expressions.
@@ -1158,7 +1162,7 @@
1158
1162
  * renderHeader() {
1159
1163
  * return this.partial`
1160
1164
  * <header>
1161
- * <${Title}>${self => self.model.title}</${Title}>
1165
+ * <${Title}>${({ model }) => model.title}</${Title}>
1162
1166
  * </header>
1163
1167
  * `;
1164
1168
  * }
@@ -1371,89 +1375,89 @@
1371
1375
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
1372
1376
  * - The template outer tag and attributes will be used to create the view's root element.
1373
1377
  * - The template inner HTML will be used as the view's template.
1374
- * ```javascript
1375
- * const Button = Component.create`<button class="button">Click me</button>`;
1376
- * ```
1378
+ * ```javascript
1379
+ * const Button = Component.create`<button class="button">Click me</button>`;
1380
+ * ```
1377
1381
  * - 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.
1378
- * ```javascript
1379
- * const Button = Component.create`
1380
- * <button class="${({ options }) => options.className}">
1381
- * ${({ options }) => options.renderChildren()}
1382
- * </button>
1383
- * `;
1384
- * ```
1382
+ * ```javascript
1383
+ * const Button = Component.create`
1384
+ * <button class="${({ options }) => options.className}">
1385
+ * ${({ options }) => options.renderChildren()}
1386
+ * </button>
1387
+ * `;
1388
+ * ```
1385
1389
  * - 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}.
1386
- * - 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.
1387
- * ```javascript
1388
- * const Input = Component.create`
1389
- * <input type="text" disabled=${({ options }) => options.disabled} />
1390
- * `;
1391
- * ```
1390
+ * - Boolean attributes should be passed in the format `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
1391
+ * ```javascript
1392
+ * const Input = Component.create`
1393
+ * <input type="text" disabled=${({ options }) => options.disabled} />
1394
+ * `;
1395
+ * ```
1392
1396
  * - If the interpolated function returns a component instance, it will be added as a child component.
1393
1397
  * - If the interpolated function returns an array, each item will be evaluated as above.
1394
- * ```javascript
1395
- * // Create a button component.
1396
- * const Button = Component.create`
1397
- * <button class="button">
1398
- * ${({ options }) => options.renderChildren()}
1399
- * </button>
1400
- * `;
1401
- * // Create a navigation component. Add buttons as children. Iterate over items.
1402
- * const Navigation = Component.create`
1403
- * <nav>
1404
- * ${({ options }) => options.items.map(
1405
- * item => Button.mount({ renderChildren: () => item.label })
1406
- * )}
1407
- * </nav>
1408
- * `;
1409
- * // Create a header component. Add navigation as a child.
1410
- * const Header = Component.create`
1411
- * <header>
1412
- * ${({ options }) => Navigation.mount({ items : options.items})}
1413
- * </header>
1414
- * `;
1415
- * ```
1398
+ * ```javascript
1399
+ * // Create a button component.
1400
+ * const Button = Component.create`
1401
+ * <button class="button">
1402
+ * ${({ options }) => options.renderChildren()}
1403
+ * </button>
1404
+ * `;
1405
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1406
+ * const Navigation = Component.create`
1407
+ * <nav>
1408
+ * ${({ options }) => options.items.map(
1409
+ * item => Button.mount({ renderChildren: () => item.label })
1410
+ * )}
1411
+ * </nav>
1412
+ * `;
1413
+ * // Create a header component. Add navigation as a child.
1414
+ * const Header = Component.create`
1415
+ * <header>
1416
+ * ${({ options }) => Navigation.mount({ items : options.items})}
1417
+ * </header>
1418
+ * `;
1419
+ * ```
1416
1420
  * - Child components can be added using a component tag.
1417
- * ```javascript
1418
- * // Create a button component.
1419
- * const Button = Component.create`
1420
- * <button class="button">
1421
- * ${({ options }) => options.renderChildren()}
1422
- * </button>
1423
- * `;
1424
- * // Create a navigation component. Add buttons as children. Iterate over items.
1425
- * const Navigation = Component.create`
1426
- * <nav>
1427
- * ${self => self.options.items.map(
1428
- * item => self.partial`<${Button}>${item.label}</${Button}>`
1429
- * )}
1430
- * </nav>
1431
- * `;
1432
- * // Create a header component. Add navigation as a child.
1433
- * const Header = Component.create`
1434
- * <header>
1435
- * <${Navigation} items="${({ options }) => options.items}" />
1436
- * </header>
1437
- * `;
1438
- * ```
1421
+ * ```javascript
1422
+ * // Create a button component.
1423
+ * const Button = Component.create`
1424
+ * <button class="button">
1425
+ * ${({ options }) => options.renderChildren()}
1426
+ * </button>
1427
+ * `;
1428
+ * // Create a navigation component. Add buttons as children. Iterate over items.
1429
+ * const Navigation = Component.create`
1430
+ * <nav>
1431
+ * ${self => self.options.items.map(
1432
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
1433
+ * )}
1434
+ * </nav>
1435
+ * `;
1436
+ * // Create a header component. Add navigation as a child.
1437
+ * const Header = Component.create`
1438
+ * <header>
1439
+ * <${Navigation} items="${({ options }) => options.items}" />
1440
+ * </header>
1441
+ * `;
1442
+ * ```
1439
1443
  * - 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.
1440
- * ```javascript
1441
- * // Create a button component.
1442
- * const Button = Component.create`
1443
- * <button class="${({ options }) => options.className}">
1444
- * ${self => self.renderChildren()}
1445
- * </button>
1446
- * `;
1447
- * // Create a container using the button component
1448
- * const ButtonOk = Component.create`
1449
- * <${Button} className="ok">Ok</${Button}>
1450
- * `;
1451
- * // Create a button component using a function
1452
- * const ButtonCancel = Component.create(() => Button.mount({
1453
- * className: 'cancel',
1454
- * renderChildren: () => 'Cancel'
1455
- * }));
1456
- * ```
1444
+ * ```javascript
1445
+ * // Create a button component.
1446
+ * const Button = Component.create`
1447
+ * <button class="${({ options }) => options.className}">
1448
+ * ${self => self.renderChildren()}
1449
+ * </button>
1450
+ * `;
1451
+ * // Create a container using the button component
1452
+ * const ButtonOk = Component.create`
1453
+ * <${Button} className="ok">Ok</${Button}>
1454
+ * `;
1455
+ * // Create a button component using a function
1456
+ * const ButtonCancel = Component.create(() => Button.mount({
1457
+ * className: 'cancel',
1458
+ * renderChildren: () => 'Cancel'
1459
+ * }));
1460
+ * ```
1457
1461
  * @static
1458
1462
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
1459
1463
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -1508,7 +1512,7 @@
1508
1512
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
1509
1513
  if (item instanceof SafeHTML) return item;
1510
1514
  if (item instanceof Component) return addChild(item);
1511
- return Component.sanitize(`${item}`);
1515
+ return Component.sanitize(item);
1512
1516
  }
1513
1517
  return '';
1514
1518
  }).join('');
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}static sanitize(t){return`${t}`.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[t])))}}n.uid=0;const r=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...r(e)):t.push(e),t)),[]);class h{constructor(t){this.value=t}toString(){return this.value}}const o=(t,e)=>s(t,e,e),l=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(p.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),a=(t,e)=>{const s=p.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(p.markAsSafeHTML(s),e[r[1]]),h=r.index+r[0].length}return n.push(p.markAsSafeHTML(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},u=(t,e)=>{const s=p.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}=d(arguments,e);if(!(t.prototype instanceof p))return h;let l;if(n){if(t!==n)return h;const s=a(u(i,e),e);l=function(){return r(s.map((t=>o(t,this))))}}return e.push((function(){const e=c(s,(t=>o(t,this))).all;return l&&(e.renderChildren=l.bind(this)),t.mount(e)})),p.PLACEHOLDER_EXPRESSION(e.length-1)}))},d=(t,e)=>{const s=p.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,o,l,a,c,u,d]=t,E={raw:i,attributes:[]};let f;n?(E.tag=void 0!==r?e[r]:n,E.inner=o,E.close=void 0!==a?e[a]:l,f=h):(E.tag=void 0!==u?e[u]:c,f=d);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(f));){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},E={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},f={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0};class p extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{f[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(`[${p.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],n={[p.DATA_ATTRIBUTE_UID]:this.uid};this.attributes&&Object.assign(n,s(this.attributes,this));const r=this.previousAttributes||{};return this.previousAttributes=n,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(`${p.sanitize(s)}="${p.sanitize(r)}"`))})),Object.keys(r).forEach((t=>{t in n||(e[t]=!0)})),{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,p.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,p.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(a(u(l(t,e),e),e).map((t=>o(t,this))))}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${p.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!E[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||!E[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,p.RENDER_TYPE_RENDER),this}static markAsSafeHTML(t){return new h(t)}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=p.PLACEHOLDER_EXPRESSION("(\\d+)");let i,n,E,f;"function"==typeof t&&(e=[t],t=["",""]);const y=u(l(t,e),e);let g=y.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(g){const{tag:t,attributes:s,inner:l,close:u}=d(g,e);if(i=function(){return p.sanitize(o(t,this))},n=function(){return c(s,(t=>o(t,this))).attributes},E=function(){const t=c(s,(t=>o(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=o(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},u){const t=l?a(l,e):[];f=function(e){return r(t.map((t=>o(t,this)))).map((t=>null!=t&&!1!==t&&!0!==t?t instanceof h?t:t instanceof p?e(t):p.sanitize(`${t}`):"")).join("")}}}else{if(g=y.match(new RegExp(`^\\s*${s}\\s*$`)),!g)throw new SyntaxError("Invalid component");f=function(t){return t(o(e[g[1]],this)).toString()}}return this.extend({tag:i,attributes:n,events:E,template:f})}}p.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,p.DATA_ATTRIBUTE_UID="data-rasti-uid",p.RENDER_TYPE_HYDRATE="hydrate",p.RENDER_TYPE_RECYCLE="recycle",p.RENDER_TYPE_RENDER="render",t.Component=p,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 o=Object.keys(h);if(o.length&&(this._pending=["change",this,h,...n]),o.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}static sanitize(t){return`${t}`.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[t])))}}n.uid=0;const r=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...r(e)):t.push(e),t)),[]);class h{constructor(t){this.value=t}toString(){return this.value}}const l=(t,e)=>s(t,e,e),o=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(p.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),a=(t,e)=>{const s=p.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(p.markAsSafeHTML(s),e[r[1]]),h=r.index+r[0].length}return n.push(p.markAsSafeHTML(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},u=(t,e)=>{const s=p.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}=d(arguments,e);if(!(t.prototype instanceof p))return h;let o;if(n){if(t!==n)return h;const s=a(u(i,e),e);o=function(){return r(s.map((t=>l(t,this))))}}return e.push((function(){const e=c(s,(t=>l(t,this))).all;return o&&(e.renderChildren=o.bind(this)),t.mount(e)})),p.PLACEHOLDER_EXPRESSION(e.length-1)}))},d=(t,e)=>{const s=p.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,l,o,a,c,u,d]=t,E={raw:i,attributes:[]};let f;n?(E.tag=void 0!==r?e[r]:n,E.inner=l,E.close=void 0!==a?e[a]:o,f=h):(E.tag=void 0!==u?e[u]:c,f=d);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(f));){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},E={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},f={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0};class p extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{f[e]&&(this[e]=t[e])})),this.options=t,this.partial=this.partial.bind(this),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(`[${p.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],n={[p.DATA_ATTRIBUTE_UID]:this.uid};this.attributes&&Object.assign(n,s(this.attributes,this));const r=this.previousAttributes||{};return this.previousAttributes=n,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(`${p.sanitize(s)}="${p.sanitize(r)}"`))})),Object.keys(r).forEach((t=>{t in n||(e[t]=!0)})),{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,p.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,p.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(a(u(o(t,e),e),e).map((t=>l(t,this))))}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${p.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!E[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||!E[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,p.RENDER_TYPE_RENDER),this}static markAsSafeHTML(t){return new h(t)}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=p.PLACEHOLDER_EXPRESSION("(\\d+)");let i,n,E,f;"function"==typeof t&&(e=[t],t=["",""]);const y=u(o(t,e),e);let g=y.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(g){const{tag:t,attributes:s,inner:o,close:u}=d(g,e);if(i=function(){return p.sanitize(l(t,this))},n=function(){return c(s,(t=>l(t,this))).attributes},E=function(){const t=c(s,(t=>l(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=l(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},u){const t=o?a(o,e):[];f=function(e){return r(t.map((t=>l(t,this)))).map((t=>null!=t&&!1!==t&&!0!==t?t instanceof h?t:t instanceof p?e(t):p.sanitize(t):"")).join("")}}}else{if(g=y.match(new RegExp(`^\\s*${s}\\s*$`)),!g)throw new SyntaxError("Invalid component");f=function(t){return t(l(e[g[1]],this)).toString()}}return this.extend({tag:i,attributes:n,events:E,template:f})}}p.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,p.DATA_ATTRIBUTE_UID="data-rasti-uid",p.RENDER_TYPE_HYDRATE="hydrate",p.RENDER_TYPE_RECYCLE="recycle",p.RENDER_TYPE_RENDER="render",t.Component=p,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}));
package/es/Component.js CHANGED
@@ -265,6 +265,8 @@ class Component extends View {
265
265
  });
266
266
  // Store options by default.
267
267
  this.options = options;
268
+ // Bind `partial` method to `this`.
269
+ this.partial = this.partial.bind(this);
268
270
  // Call lifecycle method.
269
271
  this.onCreate.apply(this, arguments);
270
272
  }
@@ -461,7 +463,9 @@ class Component extends View {
461
463
  * Tagged template helper method.
462
464
  * Used to create a partial template.
463
465
  * It will return a one-dimensional array with strings and expressions.
464
- * Components will be added as children by the parent component. Template strings will be marked as safe HTML to be rendered.
466
+ * Components will be added as children by the parent component. Template strings literals
467
+ * will be marked as safe HTML to be rendered.
468
+ * This method is bound to the component instance by default.
465
469
  * @param {TemplateStringsArray} strings - Template strings.
466
470
  * @param {...any} expressions - Template expressions.
467
471
  * @return {Array} Array containing strings and expressions.
@@ -482,7 +486,7 @@ class Component extends View {
482
486
  * renderHeader() {
483
487
  * return this.partial`
484
488
  * <header>
485
- * <${Title}>${self => self.model.title}</${Title}>
489
+ * <${Title}>${({ model }) => model.title}</${Title}>
486
490
  * </header>
487
491
  * `;
488
492
  * }
@@ -695,89 +699,89 @@ class Component extends View {
695
699
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
696
700
  * - The template outer tag and attributes will be used to create the view's root element.
697
701
  * - The template inner HTML will be used as the view's template.
698
- * ```javascript
699
- * const Button = Component.create`<button class="button">Click me</button>`;
700
- * ```
702
+ * ```javascript
703
+ * const Button = Component.create`<button class="button">Click me</button>`;
704
+ * ```
701
705
  * - 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.
702
- * ```javascript
703
- * const Button = Component.create`
704
- * <button class="${({ options }) => options.className}">
705
- * ${({ options }) => options.renderChildren()}
706
- * </button>
707
- * `;
708
- * ```
706
+ * ```javascript
707
+ * const Button = Component.create`
708
+ * <button class="${({ options }) => options.className}">
709
+ * ${({ options }) => options.renderChildren()}
710
+ * </button>
711
+ * `;
712
+ * ```
709
713
  * - 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}.
710
- * - 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.
711
- * ```javascript
712
- * const Input = Component.create`
713
- * <input type="text" disabled=${({ options }) => options.disabled} />
714
- * `;
715
- * ```
714
+ * - Boolean attributes should be passed in the format `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
715
+ * ```javascript
716
+ * const Input = Component.create`
717
+ * <input type="text" disabled=${({ options }) => options.disabled} />
718
+ * `;
719
+ * ```
716
720
  * - If the interpolated function returns a component instance, it will be added as a child component.
717
721
  * - If the interpolated function returns an array, each item will be evaluated as above.
718
- * ```javascript
719
- * // Create a button component.
720
- * const Button = Component.create`
721
- * <button class="button">
722
- * ${({ options }) => options.renderChildren()}
723
- * </button>
724
- * `;
725
- * // Create a navigation component. Add buttons as children. Iterate over items.
726
- * const Navigation = Component.create`
727
- * <nav>
728
- * ${({ options }) => options.items.map(
729
- * item => Button.mount({ renderChildren: () => item.label })
730
- * )}
731
- * </nav>
732
- * `;
733
- * // Create a header component. Add navigation as a child.
734
- * const Header = Component.create`
735
- * <header>
736
- * ${({ options }) => Navigation.mount({ items : options.items})}
737
- * </header>
738
- * `;
739
- * ```
722
+ * ```javascript
723
+ * // Create a button component.
724
+ * const Button = Component.create`
725
+ * <button class="button">
726
+ * ${({ options }) => options.renderChildren()}
727
+ * </button>
728
+ * `;
729
+ * // Create a navigation component. Add buttons as children. Iterate over items.
730
+ * const Navigation = Component.create`
731
+ * <nav>
732
+ * ${({ options }) => options.items.map(
733
+ * item => Button.mount({ renderChildren: () => item.label })
734
+ * )}
735
+ * </nav>
736
+ * `;
737
+ * // Create a header component. Add navigation as a child.
738
+ * const Header = Component.create`
739
+ * <header>
740
+ * ${({ options }) => Navigation.mount({ items : options.items})}
741
+ * </header>
742
+ * `;
743
+ * ```
740
744
  * - Child components can be added using a component tag.
741
- * ```javascript
742
- * // Create a button component.
743
- * const Button = Component.create`
744
- * <button class="button">
745
- * ${({ options }) => options.renderChildren()}
746
- * </button>
747
- * `;
748
- * // Create a navigation component. Add buttons as children. Iterate over items.
749
- * const Navigation = Component.create`
750
- * <nav>
751
- * ${self => self.options.items.map(
752
- * item => self.partial`<${Button}>${item.label}</${Button}>`
753
- * )}
754
- * </nav>
755
- * `;
756
- * // Create a header component. Add navigation as a child.
757
- * const Header = Component.create`
758
- * <header>
759
- * <${Navigation} items="${({ options }) => options.items}" />
760
- * </header>
761
- * `;
762
- * ```
745
+ * ```javascript
746
+ * // Create a button component.
747
+ * const Button = Component.create`
748
+ * <button class="button">
749
+ * ${({ options }) => options.renderChildren()}
750
+ * </button>
751
+ * `;
752
+ * // Create a navigation component. Add buttons as children. Iterate over items.
753
+ * const Navigation = Component.create`
754
+ * <nav>
755
+ * ${self => self.options.items.map(
756
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
757
+ * )}
758
+ * </nav>
759
+ * `;
760
+ * // Create a header component. Add navigation as a child.
761
+ * const Header = Component.create`
762
+ * <header>
763
+ * <${Navigation} items="${({ options }) => options.items}" />
764
+ * </header>
765
+ * `;
766
+ * ```
763
767
  * - 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.
764
- * ```javascript
765
- * // Create a button component.
766
- * const Button = Component.create`
767
- * <button class="${({ options }) => options.className}">
768
- * ${self => self.renderChildren()}
769
- * </button>
770
- * `;
771
- * // Create a container using the button component
772
- * const ButtonOk = Component.create`
773
- * <${Button} className="ok">Ok</${Button}>
774
- * `;
775
- * // Create a button component using a function
776
- * const ButtonCancel = Component.create(() => Button.mount({
777
- * className: 'cancel',
778
- * renderChildren: () => 'Cancel'
779
- * }));
780
- * ```
768
+ * ```javascript
769
+ * // Create a button component.
770
+ * const Button = Component.create`
771
+ * <button class="${({ options }) => options.className}">
772
+ * ${self => self.renderChildren()}
773
+ * </button>
774
+ * `;
775
+ * // Create a container using the button component
776
+ * const ButtonOk = Component.create`
777
+ * <${Button} className="ok">Ok</${Button}>
778
+ * `;
779
+ * // Create a button component using a function
780
+ * const ButtonCancel = Component.create(() => Button.mount({
781
+ * className: 'cancel',
782
+ * renderChildren: () => 'Cancel'
783
+ * }));
784
+ * ```
781
785
  * @static
782
786
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
783
787
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -832,7 +836,7 @@ class Component extends View {
832
836
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
833
837
  if (item instanceof SafeHTML) return item;
834
838
  if (item instanceof Component) return addChild(item);
835
- return Component.sanitize(`${item}`);
839
+ return Component.sanitize(item);
836
840
  }
837
841
  return '';
838
842
  }).join('');
package/es/View.js CHANGED
@@ -326,9 +326,9 @@ class View extends Emitter {
326
326
 
327
327
  /**
328
328
  * Escape HTML entities in a string.
329
- * Use method to sanitize user-generated content before inserting it into the DOM.
329
+ * Use this method to sanitize user-generated content before inserting it into the DOM.
330
330
  * Override this method to provide a custom escape function.
331
- * This method is used by `Component` to escape template interpolations.
331
+ * This method is inherited by {@link #module_component Component} and used to escape template interpolations.
332
332
  * @static
333
333
  * @param {string} str String to escape.
334
334
  * @return {string} Escaped string.
package/lib/Component.cjs CHANGED
@@ -267,6 +267,8 @@ class Component extends View {
267
267
  });
268
268
  // Store options by default.
269
269
  this.options = options;
270
+ // Bind `partial` method to `this`.
271
+ this.partial = this.partial.bind(this);
270
272
  // Call lifecycle method.
271
273
  this.onCreate.apply(this, arguments);
272
274
  }
@@ -463,7 +465,9 @@ class Component extends View {
463
465
  * Tagged template helper method.
464
466
  * Used to create a partial template.
465
467
  * It will return a one-dimensional array with strings and expressions.
466
- * Components will be added as children by the parent component. Template strings will be marked as safe HTML to be rendered.
468
+ * Components will be added as children by the parent component. Template strings literals
469
+ * will be marked as safe HTML to be rendered.
470
+ * This method is bound to the component instance by default.
467
471
  * @param {TemplateStringsArray} strings - Template strings.
468
472
  * @param {...any} expressions - Template expressions.
469
473
  * @return {Array} Array containing strings and expressions.
@@ -484,7 +488,7 @@ class Component extends View {
484
488
  * renderHeader() {
485
489
  * return this.partial`
486
490
  * <header>
487
- * <${Title}>${self => self.model.title}</${Title}>
491
+ * <${Title}>${({ model }) => model.title}</${Title}>
488
492
  * </header>
489
493
  * `;
490
494
  * }
@@ -697,89 +701,89 @@ class Component extends View {
697
701
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
698
702
  * - The template outer tag and attributes will be used to create the view's root element.
699
703
  * - The template inner HTML will be used as the view's template.
700
- * ```javascript
701
- * const Button = Component.create`<button class="button">Click me</button>`;
702
- * ```
704
+ * ```javascript
705
+ * const Button = Component.create`<button class="button">Click me</button>`;
706
+ * ```
703
707
  * - 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.
704
- * ```javascript
705
- * const Button = Component.create`
706
- * <button class="${({ options }) => options.className}">
707
- * ${({ options }) => options.renderChildren()}
708
- * </button>
709
- * `;
710
- * ```
708
+ * ```javascript
709
+ * const Button = Component.create`
710
+ * <button class="${({ options }) => options.className}">
711
+ * ${({ options }) => options.renderChildren()}
712
+ * </button>
713
+ * `;
714
+ * ```
711
715
  * - 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}.
712
- * - 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.
713
- * ```javascript
714
- * const Input = Component.create`
715
- * <input type="text" disabled=${({ options }) => options.disabled} />
716
- * `;
717
- * ```
716
+ * - Boolean attributes should be passed in the format `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
717
+ * ```javascript
718
+ * const Input = Component.create`
719
+ * <input type="text" disabled=${({ options }) => options.disabled} />
720
+ * `;
721
+ * ```
718
722
  * - If the interpolated function returns a component instance, it will be added as a child component.
719
723
  * - If the interpolated function returns an array, each item will be evaluated as above.
720
- * ```javascript
721
- * // Create a button component.
722
- * const Button = Component.create`
723
- * <button class="button">
724
- * ${({ options }) => options.renderChildren()}
725
- * </button>
726
- * `;
727
- * // Create a navigation component. Add buttons as children. Iterate over items.
728
- * const Navigation = Component.create`
729
- * <nav>
730
- * ${({ options }) => options.items.map(
731
- * item => Button.mount({ renderChildren: () => item.label })
732
- * )}
733
- * </nav>
734
- * `;
735
- * // Create a header component. Add navigation as a child.
736
- * const Header = Component.create`
737
- * <header>
738
- * ${({ options }) => Navigation.mount({ items : options.items})}
739
- * </header>
740
- * `;
741
- * ```
724
+ * ```javascript
725
+ * // Create a button component.
726
+ * const Button = Component.create`
727
+ * <button class="button">
728
+ * ${({ options }) => options.renderChildren()}
729
+ * </button>
730
+ * `;
731
+ * // Create a navigation component. Add buttons as children. Iterate over items.
732
+ * const Navigation = Component.create`
733
+ * <nav>
734
+ * ${({ options }) => options.items.map(
735
+ * item => Button.mount({ renderChildren: () => item.label })
736
+ * )}
737
+ * </nav>
738
+ * `;
739
+ * // Create a header component. Add navigation as a child.
740
+ * const Header = Component.create`
741
+ * <header>
742
+ * ${({ options }) => Navigation.mount({ items : options.items})}
743
+ * </header>
744
+ * `;
745
+ * ```
742
746
  * - Child components can be added using a component tag.
743
- * ```javascript
744
- * // Create a button component.
745
- * const Button = Component.create`
746
- * <button class="button">
747
- * ${({ options }) => options.renderChildren()}
748
- * </button>
749
- * `;
750
- * // Create a navigation component. Add buttons as children. Iterate over items.
751
- * const Navigation = Component.create`
752
- * <nav>
753
- * ${self => self.options.items.map(
754
- * item => self.partial`<${Button}>${item.label}</${Button}>`
755
- * )}
756
- * </nav>
757
- * `;
758
- * // Create a header component. Add navigation as a child.
759
- * const Header = Component.create`
760
- * <header>
761
- * <${Navigation} items="${({ options }) => options.items}" />
762
- * </header>
763
- * `;
764
- * ```
747
+ * ```javascript
748
+ * // Create a button component.
749
+ * const Button = Component.create`
750
+ * <button class="button">
751
+ * ${({ options }) => options.renderChildren()}
752
+ * </button>
753
+ * `;
754
+ * // Create a navigation component. Add buttons as children. Iterate over items.
755
+ * const Navigation = Component.create`
756
+ * <nav>
757
+ * ${self => self.options.items.map(
758
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
759
+ * )}
760
+ * </nav>
761
+ * `;
762
+ * // Create a header component. Add navigation as a child.
763
+ * const Header = Component.create`
764
+ * <header>
765
+ * <${Navigation} items="${({ options }) => options.items}" />
766
+ * </header>
767
+ * `;
768
+ * ```
765
769
  * - 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.
766
- * ```javascript
767
- * // Create a button component.
768
- * const Button = Component.create`
769
- * <button class="${({ options }) => options.className}">
770
- * ${self => self.renderChildren()}
771
- * </button>
772
- * `;
773
- * // Create a container using the button component
774
- * const ButtonOk = Component.create`
775
- * <${Button} className="ok">Ok</${Button}>
776
- * `;
777
- * // Create a button component using a function
778
- * const ButtonCancel = Component.create(() => Button.mount({
779
- * className: 'cancel',
780
- * renderChildren: () => 'Cancel'
781
- * }));
782
- * ```
770
+ * ```javascript
771
+ * // Create a button component.
772
+ * const Button = Component.create`
773
+ * <button class="${({ options }) => options.className}">
774
+ * ${self => self.renderChildren()}
775
+ * </button>
776
+ * `;
777
+ * // Create a container using the button component
778
+ * const ButtonOk = Component.create`
779
+ * <${Button} className="ok">Ok</${Button}>
780
+ * `;
781
+ * // Create a button component using a function
782
+ * const ButtonCancel = Component.create(() => Button.mount({
783
+ * className: 'cancel',
784
+ * renderChildren: () => 'Cancel'
785
+ * }));
786
+ * ```
783
787
  * @static
784
788
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
785
789
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -834,7 +838,7 @@ class Component extends View {
834
838
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
835
839
  if (item instanceof SafeHTML) return item;
836
840
  if (item instanceof Component) return addChild(item);
837
- return Component.sanitize(`${item}`);
841
+ return Component.sanitize(item);
838
842
  }
839
843
  return '';
840
844
  }).join('');
package/lib/View.cjs CHANGED
@@ -328,9 +328,9 @@ class View extends Emitter {
328
328
 
329
329
  /**
330
330
  * Escape HTML entities in a string.
331
- * Use method to sanitize user-generated content before inserting it into the DOM.
331
+ * Use this method to sanitize user-generated content before inserting it into the DOM.
332
332
  * Override this method to provide a custom escape function.
333
- * This method is used by `Component` to escape template interpolations.
333
+ * This method is inherited by {@link #module_component Component} and used to escape template interpolations.
334
334
  * @static
335
335
  * @param {string} str String to escape.
336
336
  * @return {string} Escaped string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasti",
3
- "version": "3.0.0-alpha.3",
3
+ "version": "3.0.0",
4
4
  "description": "Modern MVC for building user interfaces",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -30,7 +30,7 @@
30
30
  "posttest": "npm run lint",
31
31
  "lint": "eslint src test",
32
32
  "test": "mocha --require jsdom-global/register --reporter nyan test/*.js",
33
- "docs:api": "jsdoc2md --property-list-format list --helper jsdoc2md/helper.cjs --partial jsdoc2md/header.hbs --partial jsdoc2md/sig-link.hbs --partial jsdoc2md/sig-link-html.hbs --partial jsdoc2md/sig-link-parent.hbs --partial jsdoc2md/link.hbs --files 'src/**/*.js' > docs/api.md"
33
+ "docs:api": "jsdoc2md --module-index-format grouped --helper jsdoc2md/helper.cjs --partial jsdoc2md/header.hbs --partial jsdoc2md/sig-link.hbs --partial jsdoc2md/sig-link-html.hbs --partial jsdoc2md/sig-link-parent.hbs --partial jsdoc2md/link.hbs --files 'src/**/*.js' > docs/api.md"
34
34
  },
35
35
  "repository": {
36
36
  "type": "git",
package/src/Component.js CHANGED
@@ -264,6 +264,8 @@ export default class Component extends View {
264
264
  });
265
265
  // Store options by default.
266
266
  this.options = options;
267
+ // Bind `partial` method to `this`.
268
+ this.partial = this.partial.bind(this);
267
269
  // Call lifecycle method.
268
270
  this.onCreate.apply(this, arguments);
269
271
  }
@@ -460,7 +462,9 @@ export default class Component extends View {
460
462
  * Tagged template helper method.
461
463
  * Used to create a partial template.
462
464
  * It will return a one-dimensional array with strings and expressions.
463
- * Components will be added as children by the parent component. Template strings will be marked as safe HTML to be rendered.
465
+ * Components will be added as children by the parent component. Template strings literals
466
+ * will be marked as safe HTML to be rendered.
467
+ * This method is bound to the component instance by default.
464
468
  * @param {TemplateStringsArray} strings - Template strings.
465
469
  * @param {...any} expressions - Template expressions.
466
470
  * @return {Array} Array containing strings and expressions.
@@ -481,7 +485,7 @@ export default class Component extends View {
481
485
  * renderHeader() {
482
486
  * return this.partial`
483
487
  * <header>
484
- * <${Title}>${self => self.model.title}</${Title}>
488
+ * <${Title}>${({ model }) => model.title}</${Title}>
485
489
  * </header>
486
490
  * `;
487
491
  * }
@@ -694,89 +698,89 @@ export default class Component extends View {
694
698
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
695
699
  * - The template outer tag and attributes will be used to create the view's root element.
696
700
  * - The template inner HTML will be used as the view's template.
697
- * ```javascript
698
- * const Button = Component.create`<button class="button">Click me</button>`;
699
- * ```
701
+ * ```javascript
702
+ * const Button = Component.create`<button class="button">Click me</button>`;
703
+ * ```
700
704
  * - 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.
701
- * ```javascript
702
- * const Button = Component.create`
703
- * <button class="${({ options }) => options.className}">
704
- * ${({ options }) => options.renderChildren()}
705
- * </button>
706
- * `;
707
- * ```
705
+ * ```javascript
706
+ * const Button = Component.create`
707
+ * <button class="${({ options }) => options.className}">
708
+ * ${({ options }) => options.renderChildren()}
709
+ * </button>
710
+ * `;
711
+ * ```
708
712
  * - 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}.
709
- * - 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.
710
- * ```javascript
711
- * const Input = Component.create`
712
- * <input type="text" disabled=${({ options }) => options.disabled} />
713
- * `;
714
- * ```
713
+ * - Boolean attributes should be passed in the format `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
714
+ * ```javascript
715
+ * const Input = Component.create`
716
+ * <input type="text" disabled=${({ options }) => options.disabled} />
717
+ * `;
718
+ * ```
715
719
  * - If the interpolated function returns a component instance, it will be added as a child component.
716
720
  * - If the interpolated function returns an array, each item will be evaluated as above.
717
- * ```javascript
718
- * // Create a button component.
719
- * const Button = Component.create`
720
- * <button class="button">
721
- * ${({ options }) => options.renderChildren()}
722
- * </button>
723
- * `;
724
- * // Create a navigation component. Add buttons as children. Iterate over items.
725
- * const Navigation = Component.create`
726
- * <nav>
727
- * ${({ options }) => options.items.map(
728
- * item => Button.mount({ renderChildren: () => item.label })
729
- * )}
730
- * </nav>
731
- * `;
732
- * // Create a header component. Add navigation as a child.
733
- * const Header = Component.create`
734
- * <header>
735
- * ${({ options }) => Navigation.mount({ items : options.items})}
736
- * </header>
737
- * `;
738
- * ```
721
+ * ```javascript
722
+ * // Create a button component.
723
+ * const Button = Component.create`
724
+ * <button class="button">
725
+ * ${({ options }) => options.renderChildren()}
726
+ * </button>
727
+ * `;
728
+ * // Create a navigation component. Add buttons as children. Iterate over items.
729
+ * const Navigation = Component.create`
730
+ * <nav>
731
+ * ${({ options }) => options.items.map(
732
+ * item => Button.mount({ renderChildren: () => item.label })
733
+ * )}
734
+ * </nav>
735
+ * `;
736
+ * // Create a header component. Add navigation as a child.
737
+ * const Header = Component.create`
738
+ * <header>
739
+ * ${({ options }) => Navigation.mount({ items : options.items})}
740
+ * </header>
741
+ * `;
742
+ * ```
739
743
  * - Child components can be added using a component tag.
740
- * ```javascript
741
- * // Create a button component.
742
- * const Button = Component.create`
743
- * <button class="button">
744
- * ${({ options }) => options.renderChildren()}
745
- * </button>
746
- * `;
747
- * // Create a navigation component. Add buttons as children. Iterate over items.
748
- * const Navigation = Component.create`
749
- * <nav>
750
- * ${self => self.options.items.map(
751
- * item => self.partial`<${Button}>${item.label}</${Button}>`
752
- * )}
753
- * </nav>
754
- * `;
755
- * // Create a header component. Add navigation as a child.
756
- * const Header = Component.create`
757
- * <header>
758
- * <${Navigation} items="${({ options }) => options.items}" />
759
- * </header>
760
- * `;
761
- * ```
744
+ * ```javascript
745
+ * // Create a button component.
746
+ * const Button = Component.create`
747
+ * <button class="button">
748
+ * ${({ options }) => options.renderChildren()}
749
+ * </button>
750
+ * `;
751
+ * // Create a navigation component. Add buttons as children. Iterate over items.
752
+ * const Navigation = Component.create`
753
+ * <nav>
754
+ * ${self => self.options.items.map(
755
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
756
+ * )}
757
+ * </nav>
758
+ * `;
759
+ * // Create a header component. Add navigation as a child.
760
+ * const Header = Component.create`
761
+ * <header>
762
+ * <${Navigation} items="${({ options }) => options.items}" />
763
+ * </header>
764
+ * `;
765
+ * ```
762
766
  * - 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.
763
- * ```javascript
764
- * // Create a button component.
765
- * const Button = Component.create`
766
- * <button class="${({ options }) => options.className}">
767
- * ${self => self.renderChildren()}
768
- * </button>
769
- * `;
770
- * // Create a container using the button component
771
- * const ButtonOk = Component.create`
772
- * <${Button} className="ok">Ok</${Button}>
773
- * `;
774
- * // Create a button component using a function
775
- * const ButtonCancel = Component.create(() => Button.mount({
776
- * className: 'cancel',
777
- * renderChildren: () => 'Cancel'
778
- * }));
779
- * ```
767
+ * ```javascript
768
+ * // Create a button component.
769
+ * const Button = Component.create`
770
+ * <button class="${({ options }) => options.className}">
771
+ * ${self => self.renderChildren()}
772
+ * </button>
773
+ * `;
774
+ * // Create a container using the button component
775
+ * const ButtonOk = Component.create`
776
+ * <${Button} className="ok">Ok</${Button}>
777
+ * `;
778
+ * // Create a button component using a function
779
+ * const ButtonCancel = Component.create(() => Button.mount({
780
+ * className: 'cancel',
781
+ * renderChildren: () => 'Cancel'
782
+ * }));
783
+ * ```
780
784
  * @static
781
785
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
782
786
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -831,7 +835,7 @@ export default class Component extends View {
831
835
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
832
836
  if (item instanceof SafeHTML) return item;
833
837
  if (item instanceof Component) return addChild(item);
834
- return Component.sanitize(`${item}`);
838
+ return Component.sanitize(item);
835
839
  }
836
840
  return '';
837
841
  }).join('');
package/src/View.js CHANGED
@@ -326,9 +326,9 @@ export default class View extends Emitter {
326
326
 
327
327
  /**
328
328
  * Escape HTML entities in a string.
329
- * Use method to sanitize user-generated content before inserting it into the DOM.
329
+ * Use this method to sanitize user-generated content before inserting it into the DOM.
330
330
  * Override this method to provide a custom escape function.
331
- * This method is used by `Component` to escape template interpolations.
331
+ * This method is inherited by {@link #module_component Component} and used to escape template interpolations.
332
332
  * @static
333
333
  * @param {string} str String to escape.
334
334
  * @return {string} Escaped string.