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 CHANGED
@@ -29,4 +29,297 @@ npm install
29
29
  npm run dev
30
30
  ```
31
31
 
32
- # [Documentation](https://github.com/Pinbib/Signature/tree/main/docs)
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)
@@ -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 {string} The rendered component as a string.
78
+ * Returns the component as a string (template).
79
+ * @returns {html | Promise<html>} The rendered component as a string (template).
76
80
  */
77
- render(): string | Promise<string>;
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(): string | Promise<string>;
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 (typeof fragment === "string") {
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.innerHTML = html.trim();
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 (typeof fragment === "string") {
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.innerHTML = html.trim();
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
- console.log(1);
382
+ throw { id: "render-async-failed", component: com, err: err };
335
383
  }
336
384
  }
337
- })(() => {
338
- if (body.content.children.length > 1) {
339
- throw { id: "multiple-root-elements", component: com, elements: body.innerHTML };
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.content.firstElementChild;
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.content);
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 };
@@ -1 +1 @@
1
- class e{element;instance;constructor(e,n){this.instance=e,this.element=n}}const n={"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."};let t=0;class r{components={};refs={};libs={};constructor(){}add(e,n){const t="string"==typeof n?n:e.name;this.components[t]&&console.warn(new Error(`Component with name ${t} already exists.`)),this.components[t]=e}register(e,...n){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists.`));const t=e.list().filter(e=>!(e.name in n));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:t.map(e=>e.name),dependencies:e.libs};for(const n of t)this.add(n.component,`${e.name}-${n.name}`)}lib(e){return this.libs[e]}libraries(){const e=e=>{let n=e.name;return e.version&&(n+=`@${e.version}`),e.author&&(n+=`#${e.author}`),n},n=(t,r=new Set)=>{const o={};for(const[i,s]of Object.entries(t)){const t=e(s);r.has(t)||(r.add(t),o[t]={components:s.components,dependencies:n(s.dependencies,r)})}return o};return n(this.libs)}contactWith(e,...n){const t=this.refs[e];if(!t)throw new Error(`Ref with name ${e} does not exist.`);const r=t.instance;return r.onContact?.(...n)}updateRef(e){const n=this.refs[e];if(!n)throw new Error(`Ref with name ${e} does not exist.`);const t=n.instance;let r="";try{r=t.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:t.name,err:e}}const o=document.createElement("template");var i;i=()=>{if(1!==o.content.children.length)throw new Error(`Component '${t.name}' must render a single root element.`);const e=o.content.firstElementChild;this.render(o.content),t.onRender?.(),n.element.replaceWith(e),n.element=e,t.onMount?.(e)},"string"==typeof r?(o.innerHTML=r.trim(),i()):r instanceof Promise&&r.then(e=>{o.innerHTML=e.trim(),i()}).catch(e=>{throw{id:"unknown-from",from:t.name,err:e}})}contact(e,t){new Promise((n,r)=>{try{const n=document.querySelector(e);if(!n)return void r({id:"element-not-found",selector:e});const o=document.createElement("div");o.innerHTML=n.innerHTML,this.render(o),n.replaceChildren(...Array.from(o.childNodes)),t&&t()}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 t=n[e.id];throw Object.keys(e).filter(e=>!(e in["id","err"])).forEach(n=>{t=t.replace(new RegExp(`#${n}`,"gm"),String(e[n]))}),e.id in["unknown","unknown-from"]?console.error(`[${e.id}] ${t}`,e.err):console.error(`[${e.id}] ${t}`),"Page rendering was interrupted by Signature due to the above error."})}render(n){for(const r of Object.keys(this.components)){const o=this.components[r];for(const i of Array.from(n.querySelectorAll(r)).concat(Array.from(n.querySelectorAll(`[si-component="${r}"]`)))){const n=new o;if(n.onInit?.(),i instanceof HTMLElement){n.content=i.innerHTML.trim();for(const e of Object.keys(n.props)){const t=i.getAttribute(e);if(null===t){if(n.props[e].required)throw{id:"prop-is-required",component:r,prop:e};n.data[e]=null}else if(""===t){if(n.props[e].required)throw{id:"prop-is-required",component:r,prop:e};n.props[e].isValid(t)&&(n.data[e]=null)}else{let o;switch(n.props[e].type){case"boolean":o=Boolean(t);break;case"number":o=Number(t);break;case"string":o=String(t);break;case"array":try{o=JSON.parse(t)}catch(n){throw{id:"invalid-value-for-property",component:r,prop:e,value:t,attr:t}}break;default:if(n.props[e].required)throw{id:"unsupported-type-for-property",component:r,prop:e,type:n.props[e].type}}if(void 0!==o){if(!n.props[e].isValid(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:t};if(n.props[e].validate&&!n.props[e].validate(o))throw{id:"invalid-value-for-property",component:r,prop:e,value:o,attr:t};n.data[e]=o,n.onPropParsed?.(n.props[e],o)}}}n.onPropsParsed?.()}const s=document.createElement("template");let a="";try{a=n.render()}catch(e){if(e instanceof Error)throw{id:"unknown-from",from:n.name,err:e}}(e=>{if("string"==typeof a)s.innerHTML=a.trim(),e();else if(a instanceof Promise)try{a.then(n=>{s.innerHTML=n.trim(),e()}).catch(e=>{throw{id:"unknown-from",from:n.name,err:e}})}catch(e){console.log(1)}})(()=>{if(s.content.children.length>1)throw{id:"multiple-root-elements",component:r,elements:s.innerHTML};this.render(s.content),n.onRender?.();const o=s.content.firstElementChild;if(i.hasAttribute("ref")||n.options.generateRefIfNotSpecified){let s=i.getAttribute("ref");if(null===s&&(s=""),t++,""===s&&(s=`r${t}${Math.random().toString(36).substring(2,15)}${t}`),this.refs[s])throw{id:"ref-collision",ref:s,component:r};this.refs[s]=new e(n,o),o.setAttribute("ref",s),n.ref={id:s,contact:(...e)=>this.contactWith(s,...e),update:()=>this.updateRef(s)}}i.replaceWith(s.content),n.onMount?.(o)})}}}}class o{content;options={generateRefIfNotSpecified:!1};ref;props={};data={};onInit(){}onRender(){}onMount(e){}onContact(...e){}onPropsParsed(){}onPropParsed(e,n){}}class i{type;required=!0;validate;constructor(e,n=!0,t){this.type=e,this.required=n,this.validate=t||(()=>!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 s{name;version;author;libs={};components={};constructor(e,n,t){this.name=e,this.author=n,this.version=t}add(e,n){const t="string"==typeof n?n:e.name;this.components[t]&&console.warn(new Error(`Component with name ${t} already exists.`)),this.components[t]=e}register(e,...n){this.libs[e.name]&&console.warn(new Error(`Library with name ${e.name} already exists in ${this.name}.`));const t=e.list().filter(e=>!(e.name in n));this.libs[e.name]={name:e.name,version:e.version,author:e.author,components:t.map(e=>e.name),dependencies:e.libs};for(const n of t)this.add(n.component,`${e.name}-${n.name}`)}get(e){return this.components[e]}lib(e){return this.libs[e]}list(){return Object.entries(this.components).map(([e,n])=>({component:n,name:e}))}}function a(){return new r}export{o as Component,s as Library,i as Prop,r as Signature,a as default};
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 (typeof fragment === "string") {
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.innerHTML = html.trim();
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 (typeof fragment === "string") {
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.innerHTML = html.trim();
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
- console.log(1);
362
+ throw { id: "render-async-failed", component: com, err: err };
316
363
  }
317
364
  }
318
- })(() => {
319
- if (body.content.children.length > 1) {
320
- throw { id: "multiple-root-elements", component: com, elements: body.innerHTML };
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.content.firstElementChild;
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.content);
395
+ el.replaceWith(body.firstElementChild);
396
+ console.log(el, body);
348
397
  renderer.onMount?.(mountEl); // lifecycle hook
349
398
  });
350
399
  }
@@ -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(): string | Promise<string>;
12
+ abstract render(): html | Promise<html>;
13
13
  onInit?(): void;
14
14
  onRender?(): void;
15
15
  onMount?(el: Element): void;
@@ -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 {};
@@ -0,0 +1,4 @@
1
+ export default function html(strings: TemplateStringsArray, ...values: any[]): {
2
+ strings: TemplateStringsArray;
3
+ values: any[];
4
+ };
package/dist/d/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { default as Signature } from './Signature.js';
4
4
  export { default as Component } from './Component.js';
5
5
  export { default as Prop } from './Prop.js';
6
6
  export { default as Library } from './Library.js';
7
+ export { default as html } from './html.js';
@@ -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 {string} The rendered component as a string.
49
+ * Returns the component as a string (template).
50
+ * @returns {html | Promise<html>} The rendered component as a string (template).
47
51
  */
48
- render(): string | Promise<string>;
52
+ render(): html | Promise<html>;
49
53
  /**
50
54
  * Lifecycle hook that is called when the component is initialized.
51
55
  */
@@ -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
- type ErrorUnion = UnknownError | UnknownFromError | ElementNotFoundError | PropIsRequiredError | UnsupportedTypeForPropertyError | InvalidValueForPropertyError | MultipleRootElementsError | RefCollisionError | StackOverflowError;
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
@@ -0,0 +1,3 @@
1
+ export default function html(strings, ...values) {
2
+ return { strings, values };
3
+ }
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@ export { default as Signature } from './Signature.js';
6
6
  export { default as Component } from './Component.js';
7
7
  export { default as Prop } from './Prop.js';
8
8
  export { default as Library } from './Library.js';
9
+ export { default as html } from './html.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-signature",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Primitive and fast framework for rendering web interfaces",
5
5
  "license": "ISC",
6
6
  "author": "PinBib",
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(): string | Promise<string>;
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: string | Promise<string> = "";
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 (typeof fragment === "string") {
162
- template.innerHTML = fragment.trim();
163
- next();
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: string | Promise<string> = "";
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 (typeof fragment === "string") {
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: string) => {
366
- body.innerHTML = html.trim();
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
- console.log(1)
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.content.firstElementChild as Element;
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.content);
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
@@ -0,0 +1,6 @@
1
+ export default function html(strings: TemplateStringsArray, ...values: any[]): {
2
+ strings: TemplateStringsArray,
3
+ values: any[]
4
+ } {
5
+ return {strings, values};
6
+ }
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';
@@ -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 {string} The rendered component as a string.
62
+ * Returns the component as a string (template).
63
+ * @returns {html | Promise<html>} The rendered component as a string (template).
59
64
  */
60
- render(): string | Promise<string>;
65
+ render(): html | Promise<html>;
61
66
 
62
67
  /**
63
68
  * Lifecycle hook that is called when the component is initialized.
@@ -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;