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 +26 -24
- package/dist/rasti.js +106 -87
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +104 -85
- package/es/View.js +2 -2
- package/lib/Component.cjs +104 -85
- package/lib/View.cjs +2 -2
- package/package.json +2 -2
- package/src/Component.js +104 -85
- package/src/View.js +2 -2
package/README.md
CHANGED
|
@@ -20,23 +20,23 @@
|
|
|
20
20
|
## Key Features
|
|
21
21
|
|
|
22
22
|
- **Declarative Components** 🌟
|
|
23
|
-
|
|
23
|
+
Build dynamic UI components using intuitive template literals.
|
|
24
24
|
- **Event Delegation** 🎯
|
|
25
|
-
|
|
25
|
+
Simplify event handling with built-in delegation.
|
|
26
26
|
- **Model-View Binding** 🔗
|
|
27
|
-
|
|
27
|
+
Keep your UI and data in sync with ease.
|
|
28
28
|
- **Server-Side Rendering** 🌐
|
|
29
|
-
|
|
29
|
+
Render as plain text for server-side use or static builds.
|
|
30
30
|
- **Lightweight and Fast** ⚡
|
|
31
|
-
|
|
31
|
+
Minimal overhead with efficient rendering.
|
|
32
32
|
- **Legacy Compatibility** 🕰️
|
|
33
|
-
|
|
33
|
+
Seamlessly integrates into existing **Backbone.js** projects.
|
|
34
34
|
- **Standards-Based** 📐
|
|
35
|
-
|
|
35
|
+
Built on modern web standards, no tooling required.
|
|
36
36
|
|
|
37
37
|
## Getting Started
|
|
38
38
|
|
|
39
|
-
###
|
|
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.
|
|
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
|
-
${
|
|
108
|
-
({ 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
|
|
114
|
-
const
|
|
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
|
-
${({
|
|
120
|
-
({ href }) => href === (
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
737
|
+
out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
|
|
738
738
|
lastIndex = match.index + match[0].length;
|
|
739
739
|
}
|
|
740
|
-
out.push(
|
|
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(`${
|
|
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}>${
|
|
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
|
-
*
|
|
1360
|
-
*
|
|
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
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
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
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
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
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
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
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
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
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
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
|
|
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
|
|
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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[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(
|
|
61
|
+
out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
|
|
62
62
|
lastIndex = match.index + match[0].length;
|
|
63
63
|
}
|
|
64
|
-
out.push(
|
|
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(`${
|
|
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}>${
|
|
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
|
-
*
|
|
684
|
-
*
|
|
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
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
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
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
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
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
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
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
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
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
63
|
+
out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
|
|
64
64
|
lastIndex = match.index + match[0].length;
|
|
65
65
|
}
|
|
66
|
-
out.push(
|
|
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(`${
|
|
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}>${
|
|
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
|
-
*
|
|
686
|
-
*
|
|
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
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
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
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
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
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
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
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
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
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 --
|
|
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(
|
|
60
|
+
out.push(Component.markAsSafeHTML(before), expressions[match[1]]);
|
|
61
61
|
lastIndex = match.index + match[0].length;
|
|
62
62
|
}
|
|
63
|
-
out.push(
|
|
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(`${
|
|
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}>${
|
|
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
|
-
*
|
|
683
|
-
*
|
|
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
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
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
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
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
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
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
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|