rasti 3.0.0-alpha.2 → 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.
@@ -734,10 +734,10 @@
734
734
  // so all the components are added as children by the parent component.
735
735
  while ((match = regExp.exec(main)) !== null) {
736
736
  const before = main.slice(lastIndex, match.index);
737
- out.push(new SafeHTML(before), expressions[match[1]]);
737
+ out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
738
738
  lastIndex = match.index + match[0].length;
739
739
  }
740
- out.push(new SafeHTML(main.slice(lastIndex)));
740
+ out.push(Component.markAsSafeHTML(main.slice(lastIndex)));
741
741
 
742
742
  return out;
743
743
  };
@@ -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
  }
@@ -1031,7 +1033,7 @@
1031
1033
  if (value === null || typeof value === 'undefined') value = '';
1032
1034
 
1033
1035
  add[key] = value;
1034
- html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
1036
+ html.push(`${Component.sanitize(key)}="${Component.sanitize(value)}"`);
1035
1037
  }
1036
1038
  });
1037
1039
  // Remove attributes that were in previousAttributes but not in current attributes.
@@ -1135,8 +1137,11 @@
1135
1137
 
1136
1138
  /**
1137
1139
  * Tagged template helper method.
1138
- * Used to create a partial template.
1139
- * It will return a one-dimensional array with strings and expressions.
1140
+ * Used to create a partial template.
1141
+ * It will return a one-dimensional array with strings and expressions.
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.
1140
1145
  * @param {TemplateStringsArray} strings - Template strings.
1141
1146
  * @param {...any} expressions - Template expressions.
1142
1147
  * @return {Array} Array containing strings and expressions.
@@ -1157,7 +1162,7 @@
1157
1162
  * renderHeader() {
1158
1163
  * return this.partial`
1159
1164
  * <header>
1160
- * <${Title}>${self => self.model.title}</${Title}>
1165
+ * <${Title}>${({ model }) => model.title}</${Title}>
1161
1166
  * </header>
1162
1167
  * `;
1163
1168
  * }
@@ -1305,6 +1310,20 @@
1305
1310
  return this;
1306
1311
  }
1307
1312
 
1313
+ /**
1314
+ * Mark a string as safe HTML to be rendered.
1315
+ * Normally you don't need to use this method, as Rasti will automatically mark strings
1316
+ * as safe HTML when the component is @link{#module_component_create created} and when
1317
+ * using the @link{#module_component__partial Component.partial} method.
1318
+ * Be sure that the string is safe to be rendered, as it will be inserted into the DOM without any sanitization.
1319
+ * @static
1320
+ * @param {string} value
1321
+ * @return {Rasti.SafeHTML} A safe HTML object.
1322
+ */
1323
+ static markAsSafeHTML(value) {
1324
+ return new SafeHTML(value);
1325
+ }
1326
+
1308
1327
  /**
1309
1328
  * Helper method used to extend a `Component`, creating a subclass.
1310
1329
  * @static
@@ -1356,89 +1375,89 @@
1356
1375
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
1357
1376
  * - The template outer tag and attributes will be used to create the view's root element.
1358
1377
  * - The template inner HTML will be used as the view's template.
1359
- * ```javascript
1360
- * const Button = Component.create`<button class="button">Click me</button>`;
1361
- * ```
1378
+ * ```javascript
1379
+ * const Button = Component.create`<button class="button">Click me</button>`;
1380
+ * ```
1362
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.
1363
- * ```javascript
1364
- * const Button = Component.create`
1365
- * <button class="${({ options }) => options.className}">
1366
- * ${({ options }) => options.renderChildren()}
1367
- * </button>
1368
- * `;
1369
- * ```
1382
+ * ```javascript
1383
+ * const Button = Component.create`
1384
+ * <button class="${({ options }) => options.className}">
1385
+ * ${({ options }) => options.renderChildren()}
1386
+ * </button>
1387
+ * `;
1388
+ * ```
1370
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}.
1371
- * - 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.
1372
- * ```javascript
1373
- * const Input = Component.create`
1374
- * <input type="text" disabled=${({ options }) => options.disabled} />
1375
- * `;
1376
- * ```
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
+ * ```
1377
1396
  * - If the interpolated function returns a component instance, it will be added as a child component.
1378
1397
  * - If the interpolated function returns an array, each item will be evaluated as above.
1379
- * ```javascript
1380
- * // Create a button component.
1381
- * const Button = Component.create`
1382
- * <button class="button">
1383
- * ${({ options }) => options.renderChildren()}
1384
- * </button>
1385
- * `;
1386
- * // Create a navigation component. Add buttons as children. Iterate over items.
1387
- * const Navigation = Component.create`
1388
- * <nav>
1389
- * ${({ options }) => options.items.map(
1390
- * item => Button.mount({ renderChildren: () => item.label })
1391
- * )}
1392
- * </nav>
1393
- * `;
1394
- * // Create a header component. Add navigation as a child.
1395
- * const Header = Component.create`
1396
- * <header>
1397
- * ${({ options }) => Navigation.mount({ items : options.items})}
1398
- * </header>
1399
- * `;
1400
- * ```
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
+ * ```
1401
1420
  * - Child components can be added using a component tag.
1402
- * ```javascript
1403
- * // Create a button component.
1404
- * const Button = Component.create`
1405
- * <button class="button">
1406
- * ${({ options }) => options.renderChildren()}
1407
- * </button>
1408
- * `;
1409
- * // Create a navigation component. Add buttons as children. Iterate over items.
1410
- * const Navigation = Component.create`
1411
- * <nav>
1412
- * ${self => self.options.items.map(
1413
- * item => self.partial`<${Button}>${item.label}</${Button}>`
1414
- * )}
1415
- * </nav>
1416
- * `;
1417
- * // Create a header component. Add navigation as a child.
1418
- * const Header = Component.create`
1419
- * <header>
1420
- * <${Navigation} items="${({ options }) => options.items}" />
1421
- * </header>
1422
- * `;
1423
- * ```
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
+ * ```
1424
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.
1425
- * ```javascript
1426
- * // Create a button component.
1427
- * const Button = Component.create`
1428
- * <button class="${({ options }) => options.className}">
1429
- * ${self => self.renderChildren()}
1430
- * </button>
1431
- * `;
1432
- * // Create a container using the button component
1433
- * const ButtonOk = Component.create`
1434
- * <${Button} className="ok">Ok</${Button}>
1435
- * `;
1436
- * // Create a button component using a function
1437
- * const ButtonCancel = Component.create(() => Button.mount({
1438
- * className: 'cancel',
1439
- * renderChildren: () => 'Cancel'
1440
- * }));
1441
- * ```
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
+ * ```
1442
1461
  * @static
1443
1462
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
1444
1463
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -1465,7 +1484,7 @@
1465
1484
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
1466
1485
  // Get tag, attributes.
1467
1486
  tag = function() {
1468
- return View.sanitize(getExpressionResult(tagExpression, this));
1487
+ return Component.sanitize(getExpressionResult(tagExpression, this));
1469
1488
  };
1470
1489
  // Get attributes.
1471
1490
  attributes = function() {
@@ -1493,7 +1512,7 @@
1493
1512
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
1494
1513
  if (item instanceof SafeHTML) return item;
1495
1514
  if (item instanceof Component) return addChild(item);
1496
- return View.sanitize(`${item}`);
1515
+ return Component.sanitize(item);
1497
1516
  }
1498
1517
  return '';
1499
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(f.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),a=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),i=new RegExp(`${s}`,"g"),n=[];let r,o=0;for(;null!==(r=i.exec(t));){const s=t.slice(o,r.index);n.push(new h(s),e[r[1]]),o=r.index+r[0].length}return n.push(new h(t.slice(o))),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=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:h}=d(arguments,e);if(!(t.prototype instanceof f))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)})),f.PLACEHOLDER_EXPRESSION(e.length-1)}))},d=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,o,l,a,c,u,d]=t,E={raw:i,attributes:[]};let p;n?(E.tag=void 0!==r?e[r]:n,E.inner=o,E.close=void 0!==a?e[a]:l,p=h):(E.tag=void 0!==u?e[u]:c,p=d);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},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},p={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0};class f extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{p[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=[],r={[f.DATA_ATTRIBUTE_UID]:this.uid};this.attributes&&Object.assign(r,s(this.attributes,this));const h=this.previousAttributes||{};return this.previousAttributes=r,Object.keys(r).forEach((s=>{let h=r[s];!1===h?e[s]=!0:!0===h?(t[s]="",i.push(s)):(null==h&&(h=""),t[s]=h,i.push(`${n.sanitize(s)}="${n.sanitize(h)}"`))})),Object.keys(h).forEach((t=>{t in r||(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,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(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=`${f.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,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,E,p,y;"function"==typeof t&&(e=[t],t=["",""]);const g=u(l(t,e),e);let b=g.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(b){const{tag:t,attributes:s,inner:l,close:u}=d(b,e);if(i=function(){return n.sanitize(o(t,this))},E=function(){return c(s,(t=>o(t,this))).attributes},p=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):[];y=function(e){return r(t.map((t=>o(t,this)))).map((t=>null!=t&&!1!==t&&!0!==t?t instanceof h?t:t instanceof f?e(t):n.sanitize(`${t}`):"")).join("")}}}else{if(b=g.match(new RegExp(`^\\s*${s}\\s*$`)),!b)throw new SyntaxError("Invalid component");y=function(t){return t(o(e[b[1]],this)).toString()}}return this.extend({tag:i,attributes:E,events:p,template:y})}}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 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
@@ -58,10 +58,10 @@ const splitPlaceholders = (main, expressions) => {
58
58
  // so all the components are added as children by the parent component.
59
59
  while ((match = regExp.exec(main)) !== null) {
60
60
  const before = main.slice(lastIndex, match.index);
61
- out.push(new SafeHTML(before), expressions[match[1]]);
61
+ out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
62
62
  lastIndex = match.index + match[0].length;
63
63
  }
64
- out.push(new SafeHTML(main.slice(lastIndex)));
64
+ out.push(Component.markAsSafeHTML(main.slice(lastIndex)));
65
65
 
66
66
  return out;
67
67
  };
@@ -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
  }
@@ -355,7 +357,7 @@ class Component extends View {
355
357
  if (value === null || typeof value === 'undefined') value = '';
356
358
 
357
359
  add[key] = value;
358
- html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
360
+ html.push(`${Component.sanitize(key)}="${Component.sanitize(value)}"`);
359
361
  }
360
362
  });
361
363
  // Remove attributes that were in previousAttributes but not in current attributes.
@@ -459,8 +461,11 @@ class Component extends View {
459
461
 
460
462
  /**
461
463
  * Tagged template helper method.
462
- * Used to create a partial template.
463
- * It will return a one-dimensional array with strings and expressions.
464
+ * Used to create a partial template.
465
+ * It will return a one-dimensional array with strings and expressions.
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.
464
469
  * @param {TemplateStringsArray} strings - Template strings.
465
470
  * @param {...any} expressions - Template expressions.
466
471
  * @return {Array} Array containing strings and expressions.
@@ -481,7 +486,7 @@ class Component extends View {
481
486
  * renderHeader() {
482
487
  * return this.partial`
483
488
  * <header>
484
- * <${Title}>${self => self.model.title}</${Title}>
489
+ * <${Title}>${({ model }) => model.title}</${Title}>
485
490
  * </header>
486
491
  * `;
487
492
  * }
@@ -629,6 +634,20 @@ class Component extends View {
629
634
  return this;
630
635
  }
631
636
 
637
+ /**
638
+ * Mark a string as safe HTML to be rendered.
639
+ * Normally you don't need to use this method, as Rasti will automatically mark strings
640
+ * as safe HTML when the component is @link{#module_component_create created} and when
641
+ * using the @link{#module_component__partial Component.partial} method.
642
+ * Be sure that the string is safe to be rendered, as it will be inserted into the DOM without any sanitization.
643
+ * @static
644
+ * @param {string} value
645
+ * @return {Rasti.SafeHTML} A safe HTML object.
646
+ */
647
+ static markAsSafeHTML(value) {
648
+ return new SafeHTML(value);
649
+ }
650
+
632
651
  /**
633
652
  * Helper method used to extend a `Component`, creating a subclass.
634
653
  * @static
@@ -680,89 +699,89 @@ class Component extends View {
680
699
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
681
700
  * - The template outer tag and attributes will be used to create the view's root element.
682
701
  * - The template inner HTML will be used as the view's template.
683
- * ```javascript
684
- * const Button = Component.create`<button class="button">Click me</button>`;
685
- * ```
702
+ * ```javascript
703
+ * const Button = Component.create`<button class="button">Click me</button>`;
704
+ * ```
686
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.
687
- * ```javascript
688
- * const Button = Component.create`
689
- * <button class="${({ options }) => options.className}">
690
- * ${({ options }) => options.renderChildren()}
691
- * </button>
692
- * `;
693
- * ```
706
+ * ```javascript
707
+ * const Button = Component.create`
708
+ * <button class="${({ options }) => options.className}">
709
+ * ${({ options }) => options.renderChildren()}
710
+ * </button>
711
+ * `;
712
+ * ```
694
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}.
695
- * - 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.
696
- * ```javascript
697
- * const Input = Component.create`
698
- * <input type="text" disabled=${({ options }) => options.disabled} />
699
- * `;
700
- * ```
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
+ * ```
701
720
  * - If the interpolated function returns a component instance, it will be added as a child component.
702
721
  * - If the interpolated function returns an array, each item will be evaluated as above.
703
- * ```javascript
704
- * // Create a button component.
705
- * const Button = Component.create`
706
- * <button class="button">
707
- * ${({ options }) => options.renderChildren()}
708
- * </button>
709
- * `;
710
- * // Create a navigation component. Add buttons as children. Iterate over items.
711
- * const Navigation = Component.create`
712
- * <nav>
713
- * ${({ options }) => options.items.map(
714
- * item => Button.mount({ renderChildren: () => item.label })
715
- * )}
716
- * </nav>
717
- * `;
718
- * // Create a header component. Add navigation as a child.
719
- * const Header = Component.create`
720
- * <header>
721
- * ${({ options }) => Navigation.mount({ items : options.items})}
722
- * </header>
723
- * `;
724
- * ```
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
+ * ```
725
744
  * - Child components can be added using a component tag.
726
- * ```javascript
727
- * // Create a button component.
728
- * const Button = Component.create`
729
- * <button class="button">
730
- * ${({ options }) => options.renderChildren()}
731
- * </button>
732
- * `;
733
- * // Create a navigation component. Add buttons as children. Iterate over items.
734
- * const Navigation = Component.create`
735
- * <nav>
736
- * ${self => self.options.items.map(
737
- * item => self.partial`<${Button}>${item.label}</${Button}>`
738
- * )}
739
- * </nav>
740
- * `;
741
- * // Create a header component. Add navigation as a child.
742
- * const Header = Component.create`
743
- * <header>
744
- * <${Navigation} items="${({ options }) => options.items}" />
745
- * </header>
746
- * `;
747
- * ```
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
+ * ```
748
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.
749
- * ```javascript
750
- * // Create a button component.
751
- * const Button = Component.create`
752
- * <button class="${({ options }) => options.className}">
753
- * ${self => self.renderChildren()}
754
- * </button>
755
- * `;
756
- * // Create a container using the button component
757
- * const ButtonOk = Component.create`
758
- * <${Button} className="ok">Ok</${Button}>
759
- * `;
760
- * // Create a button component using a function
761
- * const ButtonCancel = Component.create(() => Button.mount({
762
- * className: 'cancel',
763
- * renderChildren: () => 'Cancel'
764
- * }));
765
- * ```
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
+ * ```
766
785
  * @static
767
786
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
768
787
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -789,7 +808,7 @@ class Component extends View {
789
808
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
790
809
  // Get tag, attributes.
791
810
  tag = function() {
792
- return View.sanitize(getExpressionResult(tagExpression, this));
811
+ return Component.sanitize(getExpressionResult(tagExpression, this));
793
812
  };
794
813
  // Get attributes.
795
814
  attributes = function() {
@@ -817,7 +836,7 @@ class Component extends View {
817
836
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
818
837
  if (item instanceof SafeHTML) return item;
819
838
  if (item instanceof Component) return addChild(item);
820
- return View.sanitize(`${item}`);
839
+ return Component.sanitize(item);
821
840
  }
822
841
  return '';
823
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
@@ -60,10 +60,10 @@ const splitPlaceholders = (main, expressions) => {
60
60
  // so all the components are added as children by the parent component.
61
61
  while ((match = regExp.exec(main)) !== null) {
62
62
  const before = main.slice(lastIndex, match.index);
63
- out.push(new SafeHTML(before), expressions[match[1]]);
63
+ out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
64
64
  lastIndex = match.index + match[0].length;
65
65
  }
66
- out.push(new SafeHTML(main.slice(lastIndex)));
66
+ out.push(Component.markAsSafeHTML(main.slice(lastIndex)));
67
67
 
68
68
  return out;
69
69
  };
@@ -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
  }
@@ -357,7 +359,7 @@ class Component extends View {
357
359
  if (value === null || typeof value === 'undefined') value = '';
358
360
 
359
361
  add[key] = value;
360
- html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
362
+ html.push(`${Component.sanitize(key)}="${Component.sanitize(value)}"`);
361
363
  }
362
364
  });
363
365
  // Remove attributes that were in previousAttributes but not in current attributes.
@@ -461,8 +463,11 @@ class Component extends View {
461
463
 
462
464
  /**
463
465
  * Tagged template helper method.
464
- * Used to create a partial template.
465
- * It will return a one-dimensional array with strings and expressions.
466
+ * Used to create a partial template.
467
+ * It will return a one-dimensional array with strings and expressions.
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.
466
471
  * @param {TemplateStringsArray} strings - Template strings.
467
472
  * @param {...any} expressions - Template expressions.
468
473
  * @return {Array} Array containing strings and expressions.
@@ -483,7 +488,7 @@ class Component extends View {
483
488
  * renderHeader() {
484
489
  * return this.partial`
485
490
  * <header>
486
- * <${Title}>${self => self.model.title}</${Title}>
491
+ * <${Title}>${({ model }) => model.title}</${Title}>
487
492
  * </header>
488
493
  * `;
489
494
  * }
@@ -631,6 +636,20 @@ class Component extends View {
631
636
  return this;
632
637
  }
633
638
 
639
+ /**
640
+ * Mark a string as safe HTML to be rendered.
641
+ * Normally you don't need to use this method, as Rasti will automatically mark strings
642
+ * as safe HTML when the component is @link{#module_component_create created} and when
643
+ * using the @link{#module_component__partial Component.partial} method.
644
+ * Be sure that the string is safe to be rendered, as it will be inserted into the DOM without any sanitization.
645
+ * @static
646
+ * @param {string} value
647
+ * @return {Rasti.SafeHTML} A safe HTML object.
648
+ */
649
+ static markAsSafeHTML(value) {
650
+ return new SafeHTML(value);
651
+ }
652
+
634
653
  /**
635
654
  * Helper method used to extend a `Component`, creating a subclass.
636
655
  * @static
@@ -682,89 +701,89 @@ class Component extends View {
682
701
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
683
702
  * - The template outer tag and attributes will be used to create the view's root element.
684
703
  * - The template inner HTML will be used as the view's template.
685
- * ```javascript
686
- * const Button = Component.create`<button class="button">Click me</button>`;
687
- * ```
704
+ * ```javascript
705
+ * const Button = Component.create`<button class="button">Click me</button>`;
706
+ * ```
688
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.
689
- * ```javascript
690
- * const Button = Component.create`
691
- * <button class="${({ options }) => options.className}">
692
- * ${({ options }) => options.renderChildren()}
693
- * </button>
694
- * `;
695
- * ```
708
+ * ```javascript
709
+ * const Button = Component.create`
710
+ * <button class="${({ options }) => options.className}">
711
+ * ${({ options }) => options.renderChildren()}
712
+ * </button>
713
+ * `;
714
+ * ```
696
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}.
697
- * - 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.
698
- * ```javascript
699
- * const Input = Component.create`
700
- * <input type="text" disabled=${({ options }) => options.disabled} />
701
- * `;
702
- * ```
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
+ * ```
703
722
  * - If the interpolated function returns a component instance, it will be added as a child component.
704
723
  * - If the interpolated function returns an array, each item will be evaluated as above.
705
- * ```javascript
706
- * // Create a button component.
707
- * const Button = Component.create`
708
- * <button class="button">
709
- * ${({ options }) => options.renderChildren()}
710
- * </button>
711
- * `;
712
- * // Create a navigation component. Add buttons as children. Iterate over items.
713
- * const Navigation = Component.create`
714
- * <nav>
715
- * ${({ options }) => options.items.map(
716
- * item => Button.mount({ renderChildren: () => item.label })
717
- * )}
718
- * </nav>
719
- * `;
720
- * // Create a header component. Add navigation as a child.
721
- * const Header = Component.create`
722
- * <header>
723
- * ${({ options }) => Navigation.mount({ items : options.items})}
724
- * </header>
725
- * `;
726
- * ```
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
+ * ```
727
746
  * - Child components can be added using a component tag.
728
- * ```javascript
729
- * // Create a button component.
730
- * const Button = Component.create`
731
- * <button class="button">
732
- * ${({ options }) => options.renderChildren()}
733
- * </button>
734
- * `;
735
- * // Create a navigation component. Add buttons as children. Iterate over items.
736
- * const Navigation = Component.create`
737
- * <nav>
738
- * ${self => self.options.items.map(
739
- * item => self.partial`<${Button}>${item.label}</${Button}>`
740
- * )}
741
- * </nav>
742
- * `;
743
- * // Create a header component. Add navigation as a child.
744
- * const Header = Component.create`
745
- * <header>
746
- * <${Navigation} items="${({ options }) => options.items}" />
747
- * </header>
748
- * `;
749
- * ```
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
+ * ```
750
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.
751
- * ```javascript
752
- * // Create a button component.
753
- * const Button = Component.create`
754
- * <button class="${({ options }) => options.className}">
755
- * ${self => self.renderChildren()}
756
- * </button>
757
- * `;
758
- * // Create a container using the button component
759
- * const ButtonOk = Component.create`
760
- * <${Button} className="ok">Ok</${Button}>
761
- * `;
762
- * // Create a button component using a function
763
- * const ButtonCancel = Component.create(() => Button.mount({
764
- * className: 'cancel',
765
- * renderChildren: () => 'Cancel'
766
- * }));
767
- * ```
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
+ * ```
768
787
  * @static
769
788
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
770
789
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -791,7 +810,7 @@ class Component extends View {
791
810
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
792
811
  // Get tag, attributes.
793
812
  tag = function() {
794
- return View.sanitize(getExpressionResult(tagExpression, this));
813
+ return Component.sanitize(getExpressionResult(tagExpression, this));
795
814
  };
796
815
  // Get attributes.
797
816
  attributes = function() {
@@ -819,7 +838,7 @@ class Component extends View {
819
838
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
820
839
  if (item instanceof SafeHTML) return item;
821
840
  if (item instanceof Component) return addChild(item);
822
- return View.sanitize(`${item}`);
841
+ return Component.sanitize(item);
823
842
  }
824
843
  return '';
825
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.2",
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
@@ -57,10 +57,10 @@ const splitPlaceholders = (main, expressions) => {
57
57
  // so all the components are added as children by the parent component.
58
58
  while ((match = regExp.exec(main)) !== null) {
59
59
  const before = main.slice(lastIndex, match.index);
60
- out.push(new SafeHTML(before), expressions[match[1]]);
60
+ out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
61
61
  lastIndex = match.index + match[0].length;
62
62
  }
63
- out.push(new SafeHTML(main.slice(lastIndex)));
63
+ out.push(Component.markAsSafeHTML(main.slice(lastIndex)));
64
64
 
65
65
  return out;
66
66
  };
@@ -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
  }
@@ -354,7 +356,7 @@ export default class Component extends View {
354
356
  if (value === null || typeof value === 'undefined') value = '';
355
357
 
356
358
  add[key] = value;
357
- html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
359
+ html.push(`${Component.sanitize(key)}="${Component.sanitize(value)}"`);
358
360
  }
359
361
  });
360
362
  // Remove attributes that were in previousAttributes but not in current attributes.
@@ -458,8 +460,11 @@ export default class Component extends View {
458
460
 
459
461
  /**
460
462
  * Tagged template helper method.
461
- * Used to create a partial template.
462
- * It will return a one-dimensional array with strings and expressions.
463
+ * Used to create a partial template.
464
+ * It will return a one-dimensional array with strings and expressions.
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.
463
468
  * @param {TemplateStringsArray} strings - Template strings.
464
469
  * @param {...any} expressions - Template expressions.
465
470
  * @return {Array} Array containing strings and expressions.
@@ -480,7 +485,7 @@ export default class Component extends View {
480
485
  * renderHeader() {
481
486
  * return this.partial`
482
487
  * <header>
483
- * <${Title}>${self => self.model.title}</${Title}>
488
+ * <${Title}>${({ model }) => model.title}</${Title}>
484
489
  * </header>
485
490
  * `;
486
491
  * }
@@ -628,6 +633,20 @@ export default class Component extends View {
628
633
  return this;
629
634
  }
630
635
 
636
+ /**
637
+ * Mark a string as safe HTML to be rendered.
638
+ * Normally you don't need to use this method, as Rasti will automatically mark strings
639
+ * as safe HTML when the component is @link{#module_component_create created} and when
640
+ * using the @link{#module_component__partial Component.partial} method.
641
+ * Be sure that the string is safe to be rendered, as it will be inserted into the DOM without any sanitization.
642
+ * @static
643
+ * @param {string} value
644
+ * @return {Rasti.SafeHTML} A safe HTML object.
645
+ */
646
+ static markAsSafeHTML(value) {
647
+ return new SafeHTML(value);
648
+ }
649
+
631
650
  /**
632
651
  * Helper method used to extend a `Component`, creating a subclass.
633
652
  * @static
@@ -679,89 +698,89 @@ export default class Component extends View {
679
698
  * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
680
699
  * - The template outer tag and attributes will be used to create the view's root element.
681
700
  * - The template inner HTML will be used as the view's template.
682
- * ```javascript
683
- * const Button = Component.create`<button class="button">Click me</button>`;
684
- * ```
701
+ * ```javascript
702
+ * const Button = Component.create`<button class="button">Click me</button>`;
703
+ * ```
685
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.
686
- * ```javascript
687
- * const Button = Component.create`
688
- * <button class="${({ options }) => options.className}">
689
- * ${({ options }) => options.renderChildren()}
690
- * </button>
691
- * `;
692
- * ```
705
+ * ```javascript
706
+ * const Button = Component.create`
707
+ * <button class="${({ options }) => options.className}">
708
+ * ${({ options }) => options.renderChildren()}
709
+ * </button>
710
+ * `;
711
+ * ```
693
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}.
694
- * - 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.
695
- * ```javascript
696
- * const Input = Component.create`
697
- * <input type="text" disabled=${({ options }) => options.disabled} />
698
- * `;
699
- * ```
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
+ * ```
700
719
  * - If the interpolated function returns a component instance, it will be added as a child component.
701
720
  * - If the interpolated function returns an array, each item will be evaluated as above.
702
- * ```javascript
703
- * // Create a button component.
704
- * const Button = Component.create`
705
- * <button class="button">
706
- * ${({ options }) => options.renderChildren()}
707
- * </button>
708
- * `;
709
- * // Create a navigation component. Add buttons as children. Iterate over items.
710
- * const Navigation = Component.create`
711
- * <nav>
712
- * ${({ options }) => options.items.map(
713
- * item => Button.mount({ renderChildren: () => item.label })
714
- * )}
715
- * </nav>
716
- * `;
717
- * // Create a header component. Add navigation as a child.
718
- * const Header = Component.create`
719
- * <header>
720
- * ${({ options }) => Navigation.mount({ items : options.items})}
721
- * </header>
722
- * `;
723
- * ```
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
+ * ```
724
743
  * - Child components can be added using a component tag.
725
- * ```javascript
726
- * // Create a button component.
727
- * const Button = Component.create`
728
- * <button class="button">
729
- * ${({ options }) => options.renderChildren()}
730
- * </button>
731
- * `;
732
- * // Create a navigation component. Add buttons as children. Iterate over items.
733
- * const Navigation = Component.create`
734
- * <nav>
735
- * ${self => self.options.items.map(
736
- * item => self.partial`<${Button}>${item.label}</${Button}>`
737
- * )}
738
- * </nav>
739
- * `;
740
- * // Create a header component. Add navigation as a child.
741
- * const Header = Component.create`
742
- * <header>
743
- * <${Navigation} items="${({ options }) => options.items}" />
744
- * </header>
745
- * `;
746
- * ```
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
+ * ```
747
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.
748
- * ```javascript
749
- * // Create a button component.
750
- * const Button = Component.create`
751
- * <button class="${({ options }) => options.className}">
752
- * ${self => self.renderChildren()}
753
- * </button>
754
- * `;
755
- * // Create a container using the button component
756
- * const ButtonOk = Component.create`
757
- * <${Button} className="ok">Ok</${Button}>
758
- * `;
759
- * // Create a button component using a function
760
- * const ButtonCancel = Component.create(() => Button.mount({
761
- * className: 'cancel',
762
- * renderChildren: () => 'Cancel'
763
- * }));
764
- * ```
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
+ * ```
765
784
  * @static
766
785
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
767
786
  * @param {...*} expressions - The expressions to be interpolated within the template.
@@ -788,7 +807,7 @@ export default class Component extends View {
788
807
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
789
808
  // Get tag, attributes.
790
809
  tag = function() {
791
- return View.sanitize(getExpressionResult(tagExpression, this));
810
+ return Component.sanitize(getExpressionResult(tagExpression, this));
792
811
  };
793
812
  // Get attributes.
794
813
  attributes = function() {
@@ -816,7 +835,7 @@ export default class Component extends View {
816
835
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
817
836
  if (item instanceof SafeHTML) return item;
818
837
  if (item instanceof Component) return addChild(item);
819
- return View.sanitize(`${item}`);
838
+ return Component.sanitize(item);
820
839
  }
821
840
  return '';
822
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.