web-signature 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +294 -1
- package/bundle/d/index.d.ts +17 -5
- package/bundle/index.js +85 -31
- package/bundle/index.min.js +1 -1
- package/dist/Errors.js +2 -1
- package/dist/Signature.js +78 -29
- package/dist/d/Component.d.ts +2 -2
- package/dist/d/Signature.d.ts +3 -0
- package/dist/d/html.d.ts +4 -0
- package/dist/d/index.d.ts +1 -0
- package/dist/d/types/Component.d.ts +7 -3
- package/dist/d/types/Errors.d.ts +7 -2
- package/dist/html.js +3 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/Component.ts +2 -2
- package/src/Errors.ts +2 -1
- package/src/Signature.ts +107 -32
- package/src/html.ts +6 -0
- package/src/index.ts +2 -1
- package/src/types/Component.ts +8 -3
- package/src/types/Errors.ts +11 -2
package/README.MD
CHANGED
|
@@ -29,4 +29,297 @@ npm install
|
|
|
29
29
|
npm run dev
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
#
|
|
32
|
+
# Introduction
|
|
33
|
+
|
|
34
|
+
Signature uses four classes as its basis:
|
|
35
|
+
|
|
36
|
+
- [Signature](./docs/Signature.md) - which processes the entire page and components.
|
|
37
|
+
- [Component](./docs/Component.md) - which defines a standard structure for any element.
|
|
38
|
+
- [Prop](./docs/Prop.md) - which defines properties for components.
|
|
39
|
+
- [Library](./docs/Library.md) - which defines a set of components.
|
|
40
|
+
|
|
41
|
+
In short, Signature allows you to create a website using simple and straightforward components that are easy to
|
|
42
|
+
customize and extend.
|
|
43
|
+
|
|
44
|
+
## Briefly about [Signature](./docs/Signature.md)
|
|
45
|
+
|
|
46
|
+
The [signature](./docs/Signature.md) is the main class that is responsible for processing the entire page. It is used to
|
|
47
|
+
add [components](./docs/Component.md),
|
|
48
|
+
register [libraries](./docs/Library.md), and essentially render the page.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// Creating a Signature instance
|
|
52
|
+
|
|
53
|
+
import {Signature} from 'web-signature';
|
|
54
|
+
|
|
55
|
+
const si = new Signature();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To add a component, use the
|
|
59
|
+
[`Signature.add(component: ComponentConstructor, name?: string)`](./docs/Signature.md#signatureadd) method, which takes
|
|
60
|
+
the
|
|
61
|
+
[component](./docs/Component.md) class and its name. For example:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {Signature, Component} from 'web-signature';
|
|
65
|
+
|
|
66
|
+
class MyComponent extends Component {
|
|
67
|
+
/* ... */
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const si = new Signature();
|
|
71
|
+
|
|
72
|
+
si.add(MyComponent, 'my-component');
|
|
73
|
+
// The <my-component> tag will be processed by this class.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Although the [component](./docs/Component.md) name is not required,
|
|
77
|
+
it is recommended to specify it to avoid problems when building the project.**
|
|
78
|
+
|
|
79
|
+
To register a [library](./docs/Library.md), the [
|
|
80
|
+
`Signature.register(library: Library)`](./docs/Signature.md#signatureregister)
|
|
81
|
+
method is used, which takes an
|
|
82
|
+
instance of the [Library](./docs/Library.md).
|
|
83
|
+
For example:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import {Signature} from 'web-signature';
|
|
87
|
+
import {MyLibrary} from './my-library';
|
|
88
|
+
|
|
89
|
+
const si = new Signature();
|
|
90
|
+
si.register(new MyLibrary());
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For rendering the page there is a [
|
|
94
|
+
`Signature.contact(selector: string, callback?)`](./docs/Signature.md#signaturecontact)
|
|
95
|
+
method. For example:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {Signature} from 'web-signature';
|
|
99
|
+
|
|
100
|
+
const si = new Signature();
|
|
101
|
+
|
|
102
|
+
si.contact('#app', () => {
|
|
103
|
+
console.log('Page rendered successfully!');
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## About [Component](./docs/Component.md)
|
|
108
|
+
|
|
109
|
+
This is an abstract class from which all components inherit. It defines a standard structure for any element.
|
|
110
|
+
It has a main method [`Component.render()`](./docs/Component.md#componentrender), and component lifecycle hooks such
|
|
111
|
+
as [
|
|
112
|
+
`Component.onMount()`](./docs/Component.md#componentonmount).
|
|
113
|
+
|
|
114
|
+
To create a component, you need to inherit the [`Component`](./docs/Component.md) class and implement the
|
|
115
|
+
[`Component.render(): string`](./docs/Component.md#componentrender) method
|
|
116
|
+
and the
|
|
117
|
+
`name` field.
|
|
118
|
+
Let's create a simple component that simply displays text:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import {Component, html} from 'web-signature';
|
|
122
|
+
|
|
123
|
+
class MyComponent extends Component {
|
|
124
|
+
name = 'my-component';
|
|
125
|
+
|
|
126
|
+
render(): string {
|
|
127
|
+
return html`<div>Hello, World!</div>`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
|
|
134
|
+
<div id="app">
|
|
135
|
+
<my-component></my-component>
|
|
136
|
+
</div>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Now let's make a counter component that will increment the value when clicked. To do this,
|
|
140
|
+
we will need to update it somehow and track events.
|
|
141
|
+
For a component to be able to update, it must have its own ref. Ref - allows you to associate a
|
|
142
|
+
component instance with
|
|
143
|
+
its DOM
|
|
144
|
+
element, if the ref is not specified, the [Signature](./docs/Signature.md) will not remember the component instance.
|
|
145
|
+
And to track events, we will use the [`Component.onMount(el: Element)`](./docs/Component.md#componentonmount) hook,
|
|
146
|
+
which is
|
|
147
|
+
called after the component has been
|
|
148
|
+
added to
|
|
149
|
+
the page.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import {Signature, Component, html} from 'web-signature';
|
|
153
|
+
|
|
154
|
+
export class Counter extends Component {
|
|
155
|
+
name = "counter";
|
|
156
|
+
|
|
157
|
+
count = 0;
|
|
158
|
+
|
|
159
|
+
render(): string {
|
|
160
|
+
// Rendering the component as a button with the current count
|
|
161
|
+
return html`<button type="button">Count is ${this.count}</button>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onMount(el: Element): void {
|
|
165
|
+
el.addEventListener('click', () => {
|
|
166
|
+
// Since a ref is required to update a component, we check if it is defined.
|
|
167
|
+
if (this.ref === undefined) {
|
|
168
|
+
throw Error();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.count++;
|
|
172
|
+
|
|
173
|
+
// Update the component
|
|
174
|
+
this.ref.update();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const si = new Signature();
|
|
180
|
+
si.add(Counter, "Counter");
|
|
181
|
+
si.contact("#app");
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The HTML should look like this:
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
|
|
188
|
+
<div id="app">
|
|
189
|
+
<counter ref></counter>
|
|
190
|
+
</div>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It is not necessary to specify a specific ref, the signature will generate it itself,
|
|
194
|
+
but if you want the ref to have a
|
|
195
|
+
specific name, you can specify it in the attribute. You can also set the component options to auto-generate
|
|
196
|
+
the ref if it was not specified.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// ...
|
|
200
|
+
class Counter extends Component {
|
|
201
|
+
options = {
|
|
202
|
+
generateRefIfNotSpecified: true
|
|
203
|
+
}
|
|
204
|
+
// ...
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Let's make it possible to set the number of steps to increment the counter,
|
|
211
|
+
using [Props](./docs/Prop.md). We need to create a [Prop](./docs/Prop.md) `step` with type `number`.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import {Signature, Component, Prop, html} from 'web-signature';
|
|
215
|
+
|
|
216
|
+
export class StepCounter extends Component {
|
|
217
|
+
name = "step-counter";
|
|
218
|
+
|
|
219
|
+
count = 0;
|
|
220
|
+
|
|
221
|
+
// In this field, we create a Prop step with type number
|
|
222
|
+
props = {
|
|
223
|
+
// We have specified that this Prop is required and must be a number greater than or equal to 1.
|
|
224
|
+
step: new Prop("number", true, (val) => Number(val) >= 1)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(): string {
|
|
228
|
+
// Rendering the component as a button with the current count
|
|
229
|
+
return html`<button type="button">Count is ${this.count}</button>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
onMount(el: Element): void {
|
|
233
|
+
el.addEventListener('click', () => {
|
|
234
|
+
// Since a ref is required to update a component, we check if it is defined.
|
|
235
|
+
if (this.ref === undefined) {
|
|
236
|
+
throw Error();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//After processing Props, the specified values are written to this.data
|
|
240
|
+
this.count += this.data.step as number;
|
|
241
|
+
|
|
242
|
+
// Update the component
|
|
243
|
+
this.ref.update();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const si = new Signature();
|
|
249
|
+
si.add(StepCounter, "Step-counter");
|
|
250
|
+
si.contact("#app");
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The HTML should look like this:
|
|
254
|
+
|
|
255
|
+
```html
|
|
256
|
+
|
|
257
|
+
<div id="app">
|
|
258
|
+
<Step-counter ref step="2"></Step-counter>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Now, when you click the button, the counter will increase by the specified number of steps (in our case, it is 2 steps).
|
|
264
|
+
|
|
265
|
+
## Quick about the Library
|
|
266
|
+
|
|
267
|
+
The [Library](./docs/Library.md) class allows you to create a set of [components](./docs/Component.md) that can be used
|
|
268
|
+
in a
|
|
269
|
+
project.
|
|
270
|
+
It has no other purpose than to facilitate the registration of [components](./docs/Component.md)
|
|
271
|
+
in [Signature](./docs/Signature.md),
|
|
272
|
+
and avoiding [component](./docs/Component.md) name collisions by grouping them.
|
|
273
|
+
|
|
274
|
+
Let's create a [library](./docs/Library.md) that will contain our `Counter` and `StepCounter` components:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import {Library} from 'web-signature';
|
|
278
|
+
|
|
279
|
+
// Importing the components
|
|
280
|
+
import {Counter} from './Counter';
|
|
281
|
+
import {StepCounter} from './StepCounter';
|
|
282
|
+
|
|
283
|
+
// Creating a library instance
|
|
284
|
+
const myLibrary = new Library('ML');
|
|
285
|
+
|
|
286
|
+
// Adding components to the library
|
|
287
|
+
myLibrary.add(Counter, "Counter");
|
|
288
|
+
myLibrary.add(StepCounter, "Step-counter");
|
|
289
|
+
|
|
290
|
+
export default myLibrary;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
To register the library in [Signature](./docs/Signature.md#signatureregister), use the
|
|
294
|
+
[`Signature.register(library: Library)`](./docs/Signature.md#signatureregister)
|
|
295
|
+
method:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import {Signature} from 'web-signature';
|
|
299
|
+
|
|
300
|
+
// Importing the library
|
|
301
|
+
import myLibrary from './myLibrary';
|
|
302
|
+
|
|
303
|
+
const si = new Signature();
|
|
304
|
+
|
|
305
|
+
// Registering the library
|
|
306
|
+
si.register(myLibrary);
|
|
307
|
+
|
|
308
|
+
si.contact('#app');
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Now you can use the components from the [library](./docs/Library.md) in your HTML, but all components will be available
|
|
312
|
+
with
|
|
313
|
+
the prefix
|
|
314
|
+
`ML-`, i.e. `ML-counter` and `ML-step-counter`, the pattern is the library name
|
|
315
|
+
is used as a prefix for the [component](./docs/Component.md) name `[libName]-[componentName]`.
|
|
316
|
+
|
|
317
|
+
```html
|
|
318
|
+
|
|
319
|
+
<div id="app">
|
|
320
|
+
<ML-counter ref></ML-counter>
|
|
321
|
+
<ML-step-counter ref step="2"></ML-step-counter>
|
|
322
|
+
</div>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## [See more here](./docs)
|
package/bundle/d/index.d.ts
CHANGED
|
@@ -50,6 +50,10 @@ type Ref = {
|
|
|
50
50
|
*/
|
|
51
51
|
update: () => void;
|
|
52
52
|
};
|
|
53
|
+
type html$1 = {
|
|
54
|
+
strings: TemplateStringsArray;
|
|
55
|
+
values: any[];
|
|
56
|
+
};
|
|
53
57
|
interface Component$1 {
|
|
54
58
|
name: string;
|
|
55
59
|
options: Options;
|
|
@@ -71,10 +75,10 @@ interface Component$1 {
|
|
|
71
75
|
*/
|
|
72
76
|
data: Record<string, string | number | boolean | null>;
|
|
73
77
|
/**
|
|
74
|
-
* Returns the component as a string.
|
|
75
|
-
* @returns {
|
|
78
|
+
* Returns the component as a string (template).
|
|
79
|
+
* @returns {html | Promise<html>} The rendered component as a string (template).
|
|
76
80
|
*/
|
|
77
|
-
render():
|
|
81
|
+
render(): html$1 | Promise<html$1>;
|
|
78
82
|
/**
|
|
79
83
|
* Lifecycle hook that is called when the component is initialized.
|
|
80
84
|
*/
|
|
@@ -112,7 +116,7 @@ declare abstract class Component implements Component$1 {
|
|
|
112
116
|
ref?: Ref;
|
|
113
117
|
readonly props: Record<string, Prop$1>;
|
|
114
118
|
readonly data: Record<string, string | number | boolean | null>;
|
|
115
|
-
abstract render():
|
|
119
|
+
abstract render(): html$1 | Promise<html$1>;
|
|
116
120
|
onInit?(): void;
|
|
117
121
|
onRender?(): void;
|
|
118
122
|
onMount?(el: Element): void;
|
|
@@ -226,6 +230,9 @@ declare class Signature {
|
|
|
226
230
|
* @param {() => void} [callback] Optional callback that will be called after rendering is complete.
|
|
227
231
|
*/
|
|
228
232
|
contact(selector: string, callback?: () => void): void;
|
|
233
|
+
private templateToString;
|
|
234
|
+
private fillTemplate;
|
|
235
|
+
private templateToElement;
|
|
229
236
|
private render;
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -244,6 +251,11 @@ declare class Prop<T extends keyof TypesMap> implements Prop$1 {
|
|
|
244
251
|
isValid(value: TypesMap[keyof TypesMap]): boolean;
|
|
245
252
|
}
|
|
246
253
|
|
|
254
|
+
declare function html(strings: TemplateStringsArray, ...values: any[]): {
|
|
255
|
+
strings: TemplateStringsArray;
|
|
256
|
+
values: any[];
|
|
257
|
+
};
|
|
258
|
+
|
|
247
259
|
declare function export_default(): Signature;
|
|
248
260
|
|
|
249
|
-
export { Component, Library, Prop, Signature, export_default as default };
|
|
261
|
+
export { Component, Library, Prop, Signature, export_default as default, html };
|
package/bundle/index.js
CHANGED
|
@@ -16,7 +16,8 @@ const errorMessages = {
|
|
|
16
16
|
"ref-collision": "Ref collision detected for ref '#ref' in component '#component'.",
|
|
17
17
|
"unknown": "An unknown error occurred.",
|
|
18
18
|
"unknown-from": "An unknown error occurred in component '#from'.",
|
|
19
|
-
"stack-overflow": "Stack Overflow detected: possible recursive component rendering."
|
|
19
|
+
"stack-overflow": "Stack Overflow detected: possible recursive component rendering.",
|
|
20
|
+
"render-async-failed": "Error during asynchronous rendering of the component #component.",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
let _counter = 0;
|
|
@@ -97,14 +98,6 @@ class Signature {
|
|
|
97
98
|
};
|
|
98
99
|
return resolve(this.libs);
|
|
99
100
|
}
|
|
100
|
-
// /**
|
|
101
|
-
// * Returns a reference.
|
|
102
|
-
// * @param {string} name The name of the reference.
|
|
103
|
-
// * @return {Element | undefined} The element associated with the reference, or undefined if it does not exist.
|
|
104
|
-
// */
|
|
105
|
-
// public ref(name: string): Element | undefined {
|
|
106
|
-
// return this.refs[name]?.element;
|
|
107
|
-
// }
|
|
108
101
|
/**
|
|
109
102
|
* Contacts the Component.onContact method through its reference.
|
|
110
103
|
* @param {string} name The name of the reference.
|
|
@@ -128,7 +121,10 @@ class Signature {
|
|
|
128
121
|
throw new Error(`Ref with name ${name} does not exist.`);
|
|
129
122
|
}
|
|
130
123
|
const component = ref.instance;
|
|
131
|
-
let fragment =
|
|
124
|
+
let fragment = {
|
|
125
|
+
strings: Object.assign([], { raw: [] }),
|
|
126
|
+
values: []
|
|
127
|
+
};
|
|
132
128
|
try {
|
|
133
129
|
fragment = component.render();
|
|
134
130
|
}
|
|
@@ -139,18 +135,18 @@ class Signature {
|
|
|
139
135
|
}
|
|
140
136
|
const template = document.createElement("template");
|
|
141
137
|
((next) => {
|
|
142
|
-
if (
|
|
143
|
-
template.innerHTML = fragment.trim();
|
|
144
|
-
next();
|
|
145
|
-
}
|
|
146
|
-
else if (fragment instanceof Promise) {
|
|
138
|
+
if (fragment instanceof Promise) {
|
|
147
139
|
fragment.then((html) => {
|
|
148
|
-
template.
|
|
140
|
+
template.content.appendChild(this.templateToElement(html));
|
|
149
141
|
next();
|
|
150
142
|
}).catch((err) => {
|
|
151
143
|
throw { id: "unknown-from", from: component.name, err: err };
|
|
152
144
|
});
|
|
153
145
|
}
|
|
146
|
+
else if (typeof fragment === "object") {
|
|
147
|
+
template.content.appendChild(this.templateToElement(fragment));
|
|
148
|
+
next();
|
|
149
|
+
}
|
|
154
150
|
})(() => {
|
|
155
151
|
if (template.content.children.length !== 1) {
|
|
156
152
|
throw new Error(`Component '${component.name}' must render a single root element.`);
|
|
@@ -202,7 +198,7 @@ class Signature {
|
|
|
202
198
|
Object.keys(err).filter(key => !(key in ["id", "err"])).forEach((key) => {
|
|
203
199
|
message = message.replace(new RegExp(`#${key}`, "gm"), String(err[key]));
|
|
204
200
|
});
|
|
205
|
-
if (err.id in ["unknown", "unknown-from"]) {
|
|
201
|
+
if (err.id in ["unknown", "unknown-from", "render-async-failed"]) {
|
|
206
202
|
console.error(`[${err.id}] ${message}`, err.err);
|
|
207
203
|
}
|
|
208
204
|
else
|
|
@@ -210,6 +206,59 @@ class Signature {
|
|
|
210
206
|
throw "Page rendering was interrupted by Signature due to the above error.";
|
|
211
207
|
});
|
|
212
208
|
}
|
|
209
|
+
templateToString(template) {
|
|
210
|
+
let body = "";
|
|
211
|
+
for (let i = 0; i < template.strings.length; i++) {
|
|
212
|
+
body += template.strings[i];
|
|
213
|
+
if (i < template.values.length) {
|
|
214
|
+
body += `<!--si-mark-${i}-->`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
console.log(body, template);
|
|
218
|
+
return body;
|
|
219
|
+
}
|
|
220
|
+
fillTemplate(template, markup) {
|
|
221
|
+
let body = document.createElement("template");
|
|
222
|
+
body.innerHTML = markup;
|
|
223
|
+
// Processing si-mark comments
|
|
224
|
+
(() => {
|
|
225
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_COMMENT);
|
|
226
|
+
let node;
|
|
227
|
+
let marks = [];
|
|
228
|
+
while ((node = walker.nextNode())) {
|
|
229
|
+
if (/si-mark-\d+/gm.test(node.nodeValue ?? "")) {
|
|
230
|
+
marks.push(node);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const node of marks) {
|
|
234
|
+
node.replaceWith(document.createTextNode(String(template.values[Number((node.nodeValue ?? "").match(/si-mark-(\d+)/m)[1])])));
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
237
|
+
// Processing si-mark attributes
|
|
238
|
+
(() => {
|
|
239
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_ELEMENT);
|
|
240
|
+
let node;
|
|
241
|
+
while ((node = walker.nextNode())) {
|
|
242
|
+
for (const attr of Array.from(node.attributes)) {
|
|
243
|
+
if (/<!--si-mark-\d+-->/gm.test(attr.value)) {
|
|
244
|
+
const match = attr.value.match(/si-mark-(\d+)/m);
|
|
245
|
+
if (match) {
|
|
246
|
+
node.setAttribute(attr.name, String(template.values[Number(match[1])]));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
templateToElement(template) {
|
|
255
|
+
const markup = this.templateToString(template);
|
|
256
|
+
const body = this.fillTemplate(template, markup);
|
|
257
|
+
if (body.content.children.length !== 1) {
|
|
258
|
+
throw { id: "multiple-root-elements", elements: body.innerHTML };
|
|
259
|
+
}
|
|
260
|
+
return body.content.firstElementChild;
|
|
261
|
+
}
|
|
213
262
|
render(frame) {
|
|
214
263
|
for (const com of Object.keys(this.components)) {
|
|
215
264
|
const component = this.components[com];
|
|
@@ -307,7 +356,10 @@ class Signature {
|
|
|
307
356
|
}
|
|
308
357
|
// Create a template for rendering
|
|
309
358
|
const body = document.createElement("template");
|
|
310
|
-
let fragment =
|
|
359
|
+
let fragment = {
|
|
360
|
+
strings: Object.assign([], { raw: [] }),
|
|
361
|
+
values: []
|
|
362
|
+
};
|
|
311
363
|
try {
|
|
312
364
|
fragment = renderer.render();
|
|
313
365
|
}
|
|
@@ -317,30 +369,27 @@ class Signature {
|
|
|
317
369
|
}
|
|
318
370
|
}
|
|
319
371
|
((next) => {
|
|
320
|
-
if (
|
|
321
|
-
body.innerHTML = fragment.trim();
|
|
322
|
-
next();
|
|
323
|
-
}
|
|
324
|
-
else if (fragment instanceof Promise) {
|
|
372
|
+
if (fragment instanceof Promise) {
|
|
325
373
|
try {
|
|
326
374
|
fragment.then((html) => {
|
|
327
|
-
body.
|
|
375
|
+
body.appendChild(this.templateToElement(html));
|
|
328
376
|
next();
|
|
329
377
|
}).catch((err) => {
|
|
330
378
|
throw { id: "unknown-from", from: renderer.name, err: err };
|
|
331
379
|
});
|
|
332
380
|
}
|
|
333
381
|
catch (err) {
|
|
334
|
-
|
|
382
|
+
throw { id: "render-async-failed", component: com, err: err };
|
|
335
383
|
}
|
|
336
384
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
385
|
+
else if (typeof fragment === "object") {
|
|
386
|
+
body.appendChild(this.templateToElement(fragment));
|
|
387
|
+
next();
|
|
340
388
|
}
|
|
389
|
+
})(() => {
|
|
341
390
|
this.render(body.content);
|
|
342
391
|
renderer.onRender?.(); // lifecycle hook
|
|
343
|
-
const mountEl = body.
|
|
392
|
+
const mountEl = body.firstElementChild;
|
|
344
393
|
// Processing ref
|
|
345
394
|
if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
|
|
346
395
|
let refName = el.getAttribute("ref");
|
|
@@ -363,7 +412,8 @@ class Signature {
|
|
|
363
412
|
update: () => this.updateRef(refName)
|
|
364
413
|
};
|
|
365
414
|
}
|
|
366
|
-
el.replaceWith(body.
|
|
415
|
+
el.replaceWith(body.firstElementChild);
|
|
416
|
+
console.log(el, body);
|
|
367
417
|
renderer.onMount?.(mountEl); // lifecycle hook
|
|
368
418
|
});
|
|
369
419
|
}
|
|
@@ -510,8 +560,12 @@ class Library {
|
|
|
510
560
|
}
|
|
511
561
|
}
|
|
512
562
|
|
|
563
|
+
function html(strings, ...values) {
|
|
564
|
+
return { strings, values };
|
|
565
|
+
}
|
|
566
|
+
|
|
513
567
|
function index () {
|
|
514
568
|
return new Signature();
|
|
515
569
|
}
|
|
516
570
|
|
|
517
|
-
export { Component, Library, Prop, Signature, index as default };
|
|
571
|
+
export { Component, Library, Prop, Signature, index as default, html };
|
package/bundle/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class e{element;instance;constructor(e,
|
|
1
|
+
class e{element;instance;constructor(e,t){this.instance=e,this.element=t}}const t={"element-not-found":"Element not found for selector: #selector","prop-is-required":"Property '#prop' in component '#component' is required but not provided.","unsupported-type-for-property":"Unsupported type for property '#prop' in component '#component': #type","invalid-value-for-property":"Invalid value for property '#prop' in component '#component': #value (value: #attr)","multiple-root-elements":"Component '#component' must render a single root element. \n\t#elements","ref-collision":"Ref collision detected for ref '#ref' in component '#component'.",unknown:"An unknown error occurred.","unknown-from":"An unknown error occurred in component '#from'.","stack-overflow":"Stack Overflow detected: possible recursive component rendering.","render-async-failed":"Error during asynchronous rendering of the component #component."};let n=0;class r{components={};refs={};libs={};constructor(){}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}lib(e){return this.libs[e]}libraries(){const e=e=>{let t=e.name;return e.version&&(t+=`@${e.version}`),e.author&&(t+=`#${e.author}`),t},t=(n,r=new Set)=>{const o={};for(const[s,i]of Object.entries(n)){const n=e(i);r.has(n)||(r.add(n),o[n]={components:i.components,dependencies:t(i.dependencies,r)})}return o};return t(this.libs)}contactWith(e,...t){const n=this.refs[e];if(!n)throw new Error(`Ref with name ${e} does not exist.`);const r=n.instance;return r.onContact?.(...t)}updateRef(e){const t=this.refs[e];if(!t)throw new Error(`Ref with name ${e} does not exist.`);const n=t.instance;let r={strings:Object.assign([],{raw:[]}),values:[]};try{r=n.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:n.name,err:e}}const o=document.createElement("template");(e=>{r instanceof Promise?r.then(t=>{o.content.appendChild(this.templateToElement(t)),e()}).catch(e=>{throw{id:"unknown-from",from:n.name,err:e}}):"object"==typeof r&&(o.content.appendChild(this.templateToElement(r)),e())})(()=>{if(1!==o.content.children.length)throw new Error(`Component '${n.name}' must render a single root element.`);const e=o.content.firstElementChild;this.render(o.content),n.onRender?.(),t.element.replaceWith(e),t.element=e,n.onMount?.(e)})}contact(e,n){new Promise((t,r)=>{try{const t=document.querySelector(e);if(!t)return void r({id:"element-not-found",selector:e});const o=document.createElement("div");o.innerHTML=t.innerHTML,this.render(o),t.replaceChildren(...Array.from(o.childNodes)),n&&n()}catch(e){e instanceof Error?e instanceof RangeError&&e.message.includes("stack")?r({id:"stack-overflow",err:e}):r({id:"unknown",err:e}):r(e)}}).catch(e=>{let n=t[e.id];throw Object.keys(e).filter(e=>!(e in["id","err"])).forEach(t=>{n=n.replace(new RegExp(`#${t}`,"gm"),String(e[t]))}),e.id in["unknown","unknown-from","render-async-failed"]?console.error(`[${e.id}] ${n}`,e.err):console.error(`[${e.id}] ${n}`),"Page rendering was interrupted by Signature due to the above error."})}templateToString(e){let t="";for(let n=0;n<e.strings.length;n++)t+=e.strings[n],n<e.values.length&&(t+=`\x3c!--si-mark-${n}--\x3e`);return console.log(t,e),t}fillTemplate(e,t){let n=document.createElement("template");return n.innerHTML=t,(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_COMMENT),o=[];for(;t=r.nextNode();)/si-mark-\d+/gm.test(t.nodeValue??"")&&o.push(t);for(const t of o)t.replaceWith(document.createTextNode(String(e.values[Number((t.nodeValue??"").match(/si-mark-(\d+)/m)[1])])))})(),(()=>{let t,r=document.createTreeWalker(n.content,NodeFilter.SHOW_ELEMENT);for(;t=r.nextNode();)for(const n of Array.from(t.attributes))if(/<!--si-mark-\d+-->/gm.test(n.value)){const r=n.value.match(/si-mark-(\d+)/m);r&&t.setAttribute(n.name,String(e.values[Number(r[1])]))}})(),n}templateToElement(e){const t=this.templateToString(e),n=this.fillTemplate(e,t);if(1!==n.content.children.length)throw{id:"multiple-root-elements",elements:n.innerHTML};return n.content.firstElementChild}render(t){for(const r of Object.keys(this.components)){const o=this.components[r];for(const s of Array.from(t.querySelectorAll(r)).concat(Array.from(t.querySelectorAll(`[si-component="${r}"]`)))){const t=new o;if(t.onInit?.(),s instanceof HTMLElement){t.content=s.innerHTML.trim();for(const e of Object.keys(t.props)){const n=s.getAttribute(e);if(null===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.data[e]=null}else if(""===n){if(t.props[e].required)throw{id:"prop-is-required",component:r,prop:e};t.props[e].isValid(n)&&(t.data[e]=null)}else{let o;switch(t.props[e].type){case"boolean":o=Boolean(n);break;case"number":o=Number(n);break;case"string":o=String(n);break;case"array":try{o=JSON.parse(n)}catch(t){throw{id:"invalid-value-for-property",component:r,prop:e,value:n,attr:n}}break;default:if(t.props[e].required)throw{id:"unsupported-type-for-property",component:r,prop:e,type:t.props[e].type}}if(void 0!==o){if(!t.props[e].isValid(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};if(t.props[e].validate&&!t.props[e].validate(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:n};t.data[e]=o,t.onPropParsed?.(t.props[e],o)}}}t.onPropsParsed?.()}const i=document.createElement("template");let a={strings:Object.assign([],{raw:[]}),values:[]};try{a=t.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:t.name,err:e}}(e=>{if(a instanceof Promise)try{a.then(t=>{i.appendChild(this.templateToElement(t)),e()}).catch(e=>{throw{id:"unknown-from",from:t.name,err:e}})}catch(e){throw{id:"render-async-failed",component:r,err:e}}else"object"==typeof a&&(i.appendChild(this.templateToElement(a)),e())})(()=>{this.render(i.content),t.onRender?.();const o=i.firstElementChild;if(s.hasAttribute("ref")||t.options.generateRefIfNotSpecified){let i=s.getAttribute("ref");if(null===i&&(i=""),n++,""===i&&(i=`r${n}${Math.random().toString(36).substring(2,15)}${n}`),this.refs[i])throw{id:"ref-collision",ref:i,component:r};this.refs[i]=new e(t,o),o.setAttribute("ref",i),t.ref={id:i,contact:(...e)=>this.contactWith(i,...e),update:()=>this.updateRef(i)}}s.replaceWith(i.firstElementChild),console.log(s,i),t.onMount?.(o)})}}}}class o{content;options={generateRefIfNotSpecified:!1};ref;props={};data={};onInit(){}onRender(){}onMount(e){}onContact(...e){}onPropsParsed(){}onPropParsed(e,t){}}class s{type;required=!0;validate;constructor(e,t=!0,n){this.type=e,this.required=t,this.validate=n||(()=>!0)}isValid(e){switch(this.type){case"boolean":return"boolean"==typeof e;case"number":return"number"==typeof e&&!isNaN(e);case"string":return"string"==typeof e;case"array":return Array.isArray(e);case"null":return null===e;default:return!1}}}class i{name;version;author;libs={};components={};constructor(e,t,n){this.name=e,this.author=t,this.version=n}add(e,t){const n="string"==typeof t?t:e.name;this.components[n]&&console.warn(new Error(`Component with name ${n} already exists.`)),this.components[n]=e}register(e,...t){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists in ${this.name}.`));const n=e.list().filter(e=>!(e.name in t));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:n.map(e=>e.name),dependencies:e.libs};for(const t of n)this.add(t.component,`${e.name}-${t.name}`)}get(e){return this.components[e]}lib(e){return this.libs[e]}list(){return Object.entries(this.components).map(([e,t])=>({component:t,name:e}))}}function a(e,...t){return{strings:e,values:t}}function c(){return new r}export{o as Component,i as Library,s as Prop,r as Signature,c as default,a as html};
|
package/dist/Errors.js
CHANGED
|
@@ -7,6 +7,7 @@ const errorMessages = {
|
|
|
7
7
|
"ref-collision": "Ref collision detected for ref '#ref' in component '#component'.",
|
|
8
8
|
"unknown": "An unknown error occurred.",
|
|
9
9
|
"unknown-from": "An unknown error occurred in component '#from'.",
|
|
10
|
-
"stack-overflow": "Stack Overflow detected: possible recursive component rendering."
|
|
10
|
+
"stack-overflow": "Stack Overflow detected: possible recursive component rendering.",
|
|
11
|
+
"render-async-failed": "Error during asynchronous rendering of the component #component.",
|
|
11
12
|
};
|
|
12
13
|
export default errorMessages;
|
package/dist/Signature.js
CHANGED
|
@@ -78,14 +78,6 @@ export default class Signature {
|
|
|
78
78
|
};
|
|
79
79
|
return resolve(this.libs);
|
|
80
80
|
}
|
|
81
|
-
// /**
|
|
82
|
-
// * Returns a reference.
|
|
83
|
-
// * @param {string} name The name of the reference.
|
|
84
|
-
// * @return {Element | undefined} The element associated with the reference, or undefined if it does not exist.
|
|
85
|
-
// */
|
|
86
|
-
// public ref(name: string): Element | undefined {
|
|
87
|
-
// return this.refs[name]?.element;
|
|
88
|
-
// }
|
|
89
81
|
/**
|
|
90
82
|
* Contacts the Component.onContact method through its reference.
|
|
91
83
|
* @param {string} name The name of the reference.
|
|
@@ -109,7 +101,10 @@ export default class Signature {
|
|
|
109
101
|
throw new Error(`Ref with name ${name} does not exist.`);
|
|
110
102
|
}
|
|
111
103
|
const component = ref.instance;
|
|
112
|
-
let fragment =
|
|
104
|
+
let fragment = {
|
|
105
|
+
strings: Object.assign([], { raw: [] }),
|
|
106
|
+
values: []
|
|
107
|
+
};
|
|
113
108
|
try {
|
|
114
109
|
fragment = component.render();
|
|
115
110
|
}
|
|
@@ -120,18 +115,18 @@ export default class Signature {
|
|
|
120
115
|
}
|
|
121
116
|
const template = document.createElement("template");
|
|
122
117
|
((next) => {
|
|
123
|
-
if (
|
|
124
|
-
template.innerHTML = fragment.trim();
|
|
125
|
-
next();
|
|
126
|
-
}
|
|
127
|
-
else if (fragment instanceof Promise) {
|
|
118
|
+
if (fragment instanceof Promise) {
|
|
128
119
|
fragment.then((html) => {
|
|
129
|
-
template.
|
|
120
|
+
template.content.appendChild(this.templateToElement(html));
|
|
130
121
|
next();
|
|
131
122
|
}).catch((err) => {
|
|
132
123
|
throw { id: "unknown-from", from: component.name, err: err };
|
|
133
124
|
});
|
|
134
125
|
}
|
|
126
|
+
else if (typeof fragment === "object") {
|
|
127
|
+
template.content.appendChild(this.templateToElement(fragment));
|
|
128
|
+
next();
|
|
129
|
+
}
|
|
135
130
|
})(() => {
|
|
136
131
|
if (template.content.children.length !== 1) {
|
|
137
132
|
throw new Error(`Component '${component.name}' must render a single root element.`);
|
|
@@ -183,7 +178,7 @@ export default class Signature {
|
|
|
183
178
|
Object.keys(err).filter(key => !(key in ["id", "err"])).forEach((key) => {
|
|
184
179
|
message = message.replace(new RegExp(`#${key}`, "gm"), String(err[key]));
|
|
185
180
|
});
|
|
186
|
-
if (err.id in ["unknown", "unknown-from"]) {
|
|
181
|
+
if (err.id in ["unknown", "unknown-from", "render-async-failed"]) {
|
|
187
182
|
console.error(`[${err.id}] ${message}`, err.err);
|
|
188
183
|
}
|
|
189
184
|
else
|
|
@@ -191,6 +186,59 @@ export default class Signature {
|
|
|
191
186
|
throw "Page rendering was interrupted by Signature due to the above error.";
|
|
192
187
|
});
|
|
193
188
|
}
|
|
189
|
+
templateToString(template) {
|
|
190
|
+
let body = "";
|
|
191
|
+
for (let i = 0; i < template.strings.length; i++) {
|
|
192
|
+
body += template.strings[i];
|
|
193
|
+
if (i < template.values.length) {
|
|
194
|
+
body += `<!--si-mark-${i}-->`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.log(body, template);
|
|
198
|
+
return body;
|
|
199
|
+
}
|
|
200
|
+
fillTemplate(template, markup) {
|
|
201
|
+
let body = document.createElement("template");
|
|
202
|
+
body.innerHTML = markup;
|
|
203
|
+
// Processing si-mark comments
|
|
204
|
+
(() => {
|
|
205
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_COMMENT);
|
|
206
|
+
let node;
|
|
207
|
+
let marks = [];
|
|
208
|
+
while ((node = walker.nextNode())) {
|
|
209
|
+
if (/si-mark-\d+/gm.test(node.nodeValue ?? "")) {
|
|
210
|
+
marks.push(node);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const node of marks) {
|
|
214
|
+
node.replaceWith(document.createTextNode(String(template.values[Number((node.nodeValue ?? "").match(/si-mark-(\d+)/m)[1])])));
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
// Processing si-mark attributes
|
|
218
|
+
(() => {
|
|
219
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_ELEMENT);
|
|
220
|
+
let node;
|
|
221
|
+
while ((node = walker.nextNode())) {
|
|
222
|
+
for (const attr of Array.from(node.attributes)) {
|
|
223
|
+
if (/<!--si-mark-\d+-->/gm.test(attr.value)) {
|
|
224
|
+
const match = attr.value.match(/si-mark-(\d+)/m);
|
|
225
|
+
if (match) {
|
|
226
|
+
node.setAttribute(attr.name, String(template.values[Number(match[1])]));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
return body;
|
|
233
|
+
}
|
|
234
|
+
templateToElement(template) {
|
|
235
|
+
const markup = this.templateToString(template);
|
|
236
|
+
const body = this.fillTemplate(template, markup);
|
|
237
|
+
if (body.content.children.length !== 1) {
|
|
238
|
+
throw { id: "multiple-root-elements", elements: body.innerHTML };
|
|
239
|
+
}
|
|
240
|
+
return body.content.firstElementChild;
|
|
241
|
+
}
|
|
194
242
|
render(frame) {
|
|
195
243
|
for (const com of Object.keys(this.components)) {
|
|
196
244
|
const component = this.components[com];
|
|
@@ -288,7 +336,10 @@ export default class Signature {
|
|
|
288
336
|
}
|
|
289
337
|
// Create a template for rendering
|
|
290
338
|
const body = document.createElement("template");
|
|
291
|
-
let fragment =
|
|
339
|
+
let fragment = {
|
|
340
|
+
strings: Object.assign([], { raw: [] }),
|
|
341
|
+
values: []
|
|
342
|
+
};
|
|
292
343
|
try {
|
|
293
344
|
fragment = renderer.render();
|
|
294
345
|
}
|
|
@@ -298,30 +349,27 @@ export default class Signature {
|
|
|
298
349
|
}
|
|
299
350
|
}
|
|
300
351
|
((next) => {
|
|
301
|
-
if (
|
|
302
|
-
body.innerHTML = fragment.trim();
|
|
303
|
-
next();
|
|
304
|
-
}
|
|
305
|
-
else if (fragment instanceof Promise) {
|
|
352
|
+
if (fragment instanceof Promise) {
|
|
306
353
|
try {
|
|
307
354
|
fragment.then((html) => {
|
|
308
|
-
body.
|
|
355
|
+
body.appendChild(this.templateToElement(html));
|
|
309
356
|
next();
|
|
310
357
|
}).catch((err) => {
|
|
311
358
|
throw { id: "unknown-from", from: renderer.name, err: err };
|
|
312
359
|
});
|
|
313
360
|
}
|
|
314
361
|
catch (err) {
|
|
315
|
-
|
|
362
|
+
throw { id: "render-async-failed", component: com, err: err };
|
|
316
363
|
}
|
|
317
364
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
365
|
+
else if (typeof fragment === "object") {
|
|
366
|
+
body.appendChild(this.templateToElement(fragment));
|
|
367
|
+
next();
|
|
321
368
|
}
|
|
369
|
+
})(() => {
|
|
322
370
|
this.render(body.content);
|
|
323
371
|
renderer.onRender?.(); // lifecycle hook
|
|
324
|
-
const mountEl = body.
|
|
372
|
+
const mountEl = body.firstElementChild;
|
|
325
373
|
// Processing ref
|
|
326
374
|
if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
|
|
327
375
|
let refName = el.getAttribute("ref");
|
|
@@ -344,7 +392,8 @@ export default class Signature {
|
|
|
344
392
|
update: () => this.updateRef(refName)
|
|
345
393
|
};
|
|
346
394
|
}
|
|
347
|
-
el.replaceWith(body.
|
|
395
|
+
el.replaceWith(body.firstElementChild);
|
|
396
|
+
console.log(el, body);
|
|
348
397
|
renderer.onMount?.(mountEl); // lifecycle hook
|
|
349
398
|
});
|
|
350
399
|
}
|
package/dist/d/Component.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import component from "./types/Component.js";
|
|
1
|
+
import component, { html } from "./types/Component.js";
|
|
2
2
|
import Prop from "./types/Prop.js";
|
|
3
3
|
import type { Options } from "./types/Component.js";
|
|
4
4
|
import type { Ref } from "./types/Component.js";
|
|
@@ -9,7 +9,7 @@ export default abstract class Component implements component {
|
|
|
9
9
|
ref?: Ref;
|
|
10
10
|
readonly props: Record<string, Prop>;
|
|
11
11
|
readonly data: Record<string, string | number | boolean | null>;
|
|
12
|
-
abstract render():
|
|
12
|
+
abstract render(): html | Promise<html>;
|
|
13
13
|
onInit?(): void;
|
|
14
14
|
onRender?(): void;
|
|
15
15
|
onMount?(el: Element): void;
|
package/dist/d/Signature.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export default class Signature {
|
|
|
50
50
|
* @param {() => void} [callback] Optional callback that will be called after rendering is complete.
|
|
51
51
|
*/
|
|
52
52
|
contact(selector: string, callback?: () => void): void;
|
|
53
|
+
private templateToString;
|
|
54
|
+
private fillTemplate;
|
|
55
|
+
private templateToElement;
|
|
53
56
|
private render;
|
|
54
57
|
}
|
|
55
58
|
export {};
|
package/dist/d/html.d.ts
ADDED
package/dist/d/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export type Ref = {
|
|
|
21
21
|
*/
|
|
22
22
|
update: () => void;
|
|
23
23
|
};
|
|
24
|
+
export type html = {
|
|
25
|
+
strings: TemplateStringsArray;
|
|
26
|
+
values: any[];
|
|
27
|
+
};
|
|
24
28
|
interface Component {
|
|
25
29
|
name: string;
|
|
26
30
|
options: Options;
|
|
@@ -42,10 +46,10 @@ interface Component {
|
|
|
42
46
|
*/
|
|
43
47
|
data: Record<string, string | number | boolean | null>;
|
|
44
48
|
/**
|
|
45
|
-
* Returns the component as a string.
|
|
46
|
-
* @returns {
|
|
49
|
+
* Returns the component as a string (template).
|
|
50
|
+
* @returns {html | Promise<html>} The rendered component as a string (template).
|
|
47
51
|
*/
|
|
48
|
-
render():
|
|
52
|
+
render(): html | Promise<html>;
|
|
49
53
|
/**
|
|
50
54
|
* Lifecycle hook that is called when the component is initialized.
|
|
51
55
|
*/
|
package/dist/d/types/Errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type Errors = "unknown" | "unknown-from" | "element-not-found" | "prop-is-required" | "unsupported-type-for-property" | "invalid-value-for-property" | "multiple-root-elements" | "ref-collision" | "stack-overflow";
|
|
1
|
+
type Errors = "unknown" | "unknown-from" | "element-not-found" | "prop-is-required" | "unsupported-type-for-property" | "invalid-value-for-property" | "multiple-root-elements" | "ref-collision" | "stack-overflow" | "render-async-failed";
|
|
2
2
|
export { Errors };
|
|
3
3
|
export interface error {
|
|
4
4
|
id: Errors;
|
|
@@ -48,5 +48,10 @@ export interface StackOverflowError extends error {
|
|
|
48
48
|
id: "stack-overflow";
|
|
49
49
|
err?: Error;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
export interface RenderAsyncFailedError extends error {
|
|
52
|
+
id: "render-async-failed";
|
|
53
|
+
component: string;
|
|
54
|
+
err?: Error;
|
|
55
|
+
}
|
|
56
|
+
type ErrorUnion = UnknownError | UnknownFromError | ElementNotFoundError | PropIsRequiredError | UnsupportedTypeForPropertyError | InvalidValueForPropertyError | MultipleRootElementsError | RefCollisionError | StackOverflowError | RenderAsyncFailedError;
|
|
52
57
|
export default ErrorUnion;
|
package/dist/html.js
ADDED
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/Component.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import component from "./types/Component.js";
|
|
1
|
+
import component, {html} from "./types/Component.js";
|
|
2
2
|
import Prop from "./types/Prop.js";
|
|
3
3
|
|
|
4
4
|
import type {Options} from "./types/Component.js";
|
|
@@ -17,7 +17,7 @@ export default abstract class Component implements component {
|
|
|
17
17
|
readonly props: Record<string, Prop> = {};
|
|
18
18
|
readonly data: Record<string, string | number | boolean | null> = {};
|
|
19
19
|
|
|
20
|
-
abstract render():
|
|
20
|
+
abstract render(): html | Promise<html>;
|
|
21
21
|
|
|
22
22
|
onInit?(): void {
|
|
23
23
|
};
|
package/src/Errors.ts
CHANGED
|
@@ -9,7 +9,8 @@ const errorMessages: Record<Errors, string> = {
|
|
|
9
9
|
"ref-collision": "Ref collision detected for ref '#ref' in component '#component'.",
|
|
10
10
|
"unknown": "An unknown error occurred.",
|
|
11
11
|
"unknown-from": "An unknown error occurred in component '#from'.",
|
|
12
|
-
"stack-overflow": "Stack Overflow detected: possible recursive component rendering."
|
|
12
|
+
"stack-overflow": "Stack Overflow detected: possible recursive component rendering.",
|
|
13
|
+
"render-async-failed": "Error during asynchronous rendering of the component #component.",
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export default errorMessages;
|
package/src/Signature.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Ref from "./Ref.js";
|
|
|
3
3
|
import ErrorUnion from "./types/Errors.js";
|
|
4
4
|
import Errors from "./Errors.js";
|
|
5
5
|
import Library, {LibMeta} from "./Library.js";
|
|
6
|
+
import {html} from "./types/Component";
|
|
6
7
|
|
|
7
8
|
let _counter = 0;
|
|
8
9
|
|
|
@@ -107,15 +108,6 @@ export default class Signature {
|
|
|
107
108
|
return resolve(this.libs);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
// /**
|
|
111
|
-
// * Returns a reference.
|
|
112
|
-
// * @param {string} name The name of the reference.
|
|
113
|
-
// * @return {Element | undefined} The element associated with the reference, or undefined if it does not exist.
|
|
114
|
-
// */
|
|
115
|
-
// public ref(name: string): Element | undefined {
|
|
116
|
-
// return this.refs[name]?.element;
|
|
117
|
-
// }
|
|
118
|
-
|
|
119
111
|
/**
|
|
120
112
|
* Contacts the Component.onContact method through its reference.
|
|
121
113
|
* @param {string} name The name of the reference.
|
|
@@ -145,7 +137,10 @@ export default class Signature {
|
|
|
145
137
|
|
|
146
138
|
const component = ref.instance;
|
|
147
139
|
|
|
148
|
-
let fragment:
|
|
140
|
+
let fragment: html | Promise<html> = {
|
|
141
|
+
strings: Object.assign([], {raw: []}) as TemplateStringsArray,
|
|
142
|
+
values: []
|
|
143
|
+
};
|
|
149
144
|
|
|
150
145
|
try {
|
|
151
146
|
fragment = component.render();
|
|
@@ -158,16 +153,16 @@ export default class Signature {
|
|
|
158
153
|
const template = document.createElement("template");
|
|
159
154
|
|
|
160
155
|
((next: () => void) => {
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} else if (fragment instanceof Promise) {
|
|
165
|
-
fragment.then((html: string) => {
|
|
166
|
-
template.innerHTML = html.trim();
|
|
156
|
+
if (fragment instanceof Promise) {
|
|
157
|
+
fragment.then((html: html) => {
|
|
158
|
+
template.content.appendChild(this.templateToElement(html))
|
|
167
159
|
next();
|
|
168
160
|
}).catch((err: Error) => {
|
|
169
161
|
throw {id: "unknown-from", from: component.name, err: err} as ErrorUnion;
|
|
170
162
|
});
|
|
163
|
+
} else if (typeof fragment === "object") {
|
|
164
|
+
template.content.appendChild(this.templateToElement(fragment));
|
|
165
|
+
next();
|
|
171
166
|
}
|
|
172
167
|
})(() => {
|
|
173
168
|
if (template.content.children.length !== 1) {
|
|
@@ -230,7 +225,7 @@ export default class Signature {
|
|
|
230
225
|
message = message.replace(new RegExp(`#${key}`, "gm"), String(err[key as keyof typeof err]));
|
|
231
226
|
});
|
|
232
227
|
|
|
233
|
-
if (err.id in ["unknown", "unknown-from"]) {
|
|
228
|
+
if (err.id in ["unknown", "unknown-from", "render-async-failed"]) {
|
|
234
229
|
console.error(`[${err.id}] ${message}`, (err as {
|
|
235
230
|
err: Error
|
|
236
231
|
}).err);
|
|
@@ -240,6 +235,86 @@ export default class Signature {
|
|
|
240
235
|
});
|
|
241
236
|
}
|
|
242
237
|
|
|
238
|
+
private templateToString(template: html): string {
|
|
239
|
+
let body = "";
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < template.strings.length; i++) {
|
|
242
|
+
body += template.strings[i];
|
|
243
|
+
|
|
244
|
+
if (i < template.values.length) {
|
|
245
|
+
body += `<!--si-mark-${i}-->`
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
console.log(body, template)
|
|
249
|
+
return body;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fillTemplate(template: html, markup: string): HTMLTemplateElement {
|
|
253
|
+
let body = document.createElement("template");
|
|
254
|
+
body.innerHTML = markup;
|
|
255
|
+
|
|
256
|
+
// Processing si-mark comments
|
|
257
|
+
(() => {
|
|
258
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_COMMENT);
|
|
259
|
+
|
|
260
|
+
let node: ChildNode;
|
|
261
|
+
|
|
262
|
+
let marks: ChildNode[] = [];
|
|
263
|
+
|
|
264
|
+
while ((node = walker.nextNode() as ChildNode)) {
|
|
265
|
+
if (/si-mark-\d+/gm.test(node.nodeValue ?? "")) {
|
|
266
|
+
marks.push(node);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const node of marks) {
|
|
271
|
+
node.replaceWith(
|
|
272
|
+
document.createTextNode(
|
|
273
|
+
String(
|
|
274
|
+
template.values[Number(
|
|
275
|
+
(
|
|
276
|
+
(node.nodeValue ?? "").match(/si-mark-(\d+)/m) as string[]
|
|
277
|
+
)[1]
|
|
278
|
+
)]
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
|
|
285
|
+
// Processing si-mark attributes
|
|
286
|
+
(() => {
|
|
287
|
+
let walker = document.createTreeWalker(body.content, NodeFilter.SHOW_ELEMENT);
|
|
288
|
+
|
|
289
|
+
let node: Element;
|
|
290
|
+
|
|
291
|
+
while ((node = walker.nextNode() as Element)) {
|
|
292
|
+
for (const attr of Array.from(node.attributes)) {
|
|
293
|
+
if (/<!--si-mark-\d+-->/gm.test(attr.value)) {
|
|
294
|
+
const match: RegExpMatchArray = attr.value.match(/si-mark-(\d+)/m) as RegExpMatchArray;
|
|
295
|
+
|
|
296
|
+
if (match) {
|
|
297
|
+
node.setAttribute(attr.name, String(template.values[Number(match[1])]))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
|
|
304
|
+
return body;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private templateToElement(template: html): HTMLElement {
|
|
308
|
+
const markup: string = this.templateToString(template);
|
|
309
|
+
const body: HTMLTemplateElement = this.fillTemplate(template, markup);
|
|
310
|
+
|
|
311
|
+
if (body.content.children.length !== 1) {
|
|
312
|
+
throw {id: "multiple-root-elements", elements: body.innerHTML} as ErrorUnion;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return (body.content.firstElementChild as HTMLElement);
|
|
316
|
+
}
|
|
317
|
+
|
|
243
318
|
private render(frame: Element | DocumentFragment): void {
|
|
244
319
|
for (const com of Object.keys(this.components)) {
|
|
245
320
|
const component: ComponentConstructor = this.components[com];
|
|
@@ -346,7 +421,10 @@ export default class Signature {
|
|
|
346
421
|
// Create a template for rendering
|
|
347
422
|
const body = document.createElement("template");
|
|
348
423
|
|
|
349
|
-
let fragment:
|
|
424
|
+
let fragment: html | Promise<html> = {
|
|
425
|
+
strings: Object.assign([], {raw: []}) as TemplateStringsArray,
|
|
426
|
+
values: []
|
|
427
|
+
};
|
|
350
428
|
|
|
351
429
|
try {
|
|
352
430
|
fragment = renderer.render();
|
|
@@ -357,30 +435,27 @@ export default class Signature {
|
|
|
357
435
|
}
|
|
358
436
|
|
|
359
437
|
((next: () => void) => {
|
|
360
|
-
if (
|
|
361
|
-
body.innerHTML = fragment.trim();
|
|
362
|
-
next();
|
|
363
|
-
} else if (fragment instanceof Promise) {
|
|
438
|
+
if (fragment instanceof Promise) {
|
|
364
439
|
try {
|
|
365
|
-
fragment.then((html:
|
|
366
|
-
body.
|
|
440
|
+
fragment.then((html: html) => {
|
|
441
|
+
body.appendChild(this.templateToElement(html));
|
|
367
442
|
next();
|
|
368
443
|
}).catch((err: Error) => {
|
|
369
444
|
throw {id: "unknown-from", from: renderer.name, err: err} as ErrorUnion;
|
|
370
445
|
});
|
|
371
446
|
} catch (err) {
|
|
372
|
-
|
|
447
|
+
throw {id: "render-async-failed", component: com, err: err} as ErrorUnion;
|
|
373
448
|
}
|
|
449
|
+
} else if (typeof fragment === "object") {
|
|
450
|
+
body.appendChild(this.templateToElement(fragment));
|
|
451
|
+
next();
|
|
374
452
|
}
|
|
375
453
|
})(() => {
|
|
376
|
-
if (body.content.children.length > 1) {
|
|
377
|
-
throw {id: "multiple-root-elements", component: com, elements: body.innerHTML} as ErrorUnion;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
454
|
this.render(body.content);
|
|
455
|
+
|
|
381
456
|
renderer.onRender?.(); // lifecycle hook
|
|
382
457
|
|
|
383
|
-
const mountEl: Element = body.
|
|
458
|
+
const mountEl: Element = body.firstElementChild as Element;
|
|
384
459
|
|
|
385
460
|
// Processing ref
|
|
386
461
|
if (el.hasAttribute("ref") || renderer.options.generateRefIfNotSpecified) {
|
|
@@ -412,8 +487,8 @@ export default class Signature {
|
|
|
412
487
|
};
|
|
413
488
|
}
|
|
414
489
|
|
|
415
|
-
el.replaceWith(body.
|
|
416
|
-
|
|
490
|
+
el.replaceWith(body.firstElementChild as Element);
|
|
491
|
+
console.log(el, body)
|
|
417
492
|
renderer.onMount?.(mountEl); // lifecycle hook
|
|
418
493
|
});
|
|
419
494
|
}
|
package/src/html.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -7,4 +7,5 @@ export default function (): Signature {
|
|
|
7
7
|
export {default as Signature} from './Signature.js';
|
|
8
8
|
export {default as Component} from './Component.js';
|
|
9
9
|
export {default as Prop} from './Prop.js';
|
|
10
|
-
export {default as Library} from './Library.js';
|
|
10
|
+
export {default as Library} from './Library.js';
|
|
11
|
+
export {default as html} from './html.js';
|
package/src/types/Component.ts
CHANGED
|
@@ -26,6 +26,11 @@ export type Ref = {
|
|
|
26
26
|
update: () => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export type html = {
|
|
30
|
+
strings: TemplateStringsArray;
|
|
31
|
+
values: any[];
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
interface Component {
|
|
30
35
|
name: string;
|
|
31
36
|
|
|
@@ -54,10 +59,10 @@ interface Component {
|
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
|
-
* Returns the component as a string.
|
|
58
|
-
* @returns {
|
|
62
|
+
* Returns the component as a string (template).
|
|
63
|
+
* @returns {html | Promise<html>} The rendered component as a string (template).
|
|
59
64
|
*/
|
|
60
|
-
render():
|
|
65
|
+
render(): html | Promise<html>;
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
68
|
* Lifecycle hook that is called when the component is initialized.
|
package/src/types/Errors.ts
CHANGED
|
@@ -7,7 +7,9 @@ type Errors =
|
|
|
7
7
|
| "invalid-value-for-property"
|
|
8
8
|
| "multiple-root-elements"
|
|
9
9
|
| "ref-collision"
|
|
10
|
-
| "stack-overflow"
|
|
10
|
+
| "stack-overflow"
|
|
11
|
+
| "render-async-failed";
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
export {Errors};
|
|
13
15
|
|
|
@@ -69,6 +71,12 @@ export interface StackOverflowError extends error {
|
|
|
69
71
|
err?: Error;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
export interface RenderAsyncFailedError extends error {
|
|
75
|
+
id: "render-async-failed";
|
|
76
|
+
component: string;
|
|
77
|
+
err?: Error;
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
type ErrorUnion =
|
|
73
81
|
UnknownError
|
|
74
82
|
| UnknownFromError
|
|
@@ -78,6 +86,7 @@ type ErrorUnion =
|
|
|
78
86
|
| InvalidValueForPropertyError
|
|
79
87
|
| MultipleRootElementsError
|
|
80
88
|
| RefCollisionError
|
|
81
|
-
| StackOverflowError
|
|
89
|
+
| StackOverflowError
|
|
90
|
+
| RenderAsyncFailedError;
|
|
82
91
|
|
|
83
92
|
export default ErrorUnion;
|