structured-fw 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -19
- package/build/system/EventEmitter.d.ts +4 -1
- package/build/system/EventEmitter.js +30 -12
- package/build/system/client/Client.d.ts +1 -6
- package/build/system/client/Client.js +2 -9
- package/build/system/client/ClientApplication.d.ts +13 -0
- package/build/system/client/ClientApplication.js +43 -0
- package/build/system/client/ClientComponent.d.ts +10 -6
- package/build/system/client/ClientComponent.js +106 -85
- package/build/system/client/DataStoreView.d.ts +1 -0
- package/build/system/client/DataStoreView.js +4 -1
- package/build/system/server/Component.js +1 -0
- package/build/system/server/Components.js +3 -0
- package/build/system/server/Document.d.ts +1 -1
- package/build/system/server/Request.d.ts +2 -1
- package/build/system/server/Request.js +4 -1
- package/build/system/types/component.types.d.ts +11 -10
- package/build/system/types/eventEmitter.types.d.ts +1 -1
- package/build/system/types/general.types.d.ts +1 -0
- package/package.json +2 -2
- package/build/system/client/App.d.ts +0 -7
- package/build/system/client/App.js +0 -8
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ export default function(app: Application) {
|
|
|
51
51
|
|
|
52
52
|
### Compile
|
|
53
53
|
`tsc`\
|
|
54
|
-
This will create a directory `build` (
|
|
54
|
+
This will create a directory `build` (which can be changed in tsconfig.json, compilerOptions.outputDir)
|
|
55
55
|
|
|
56
56
|
### Run
|
|
57
57
|
```
|
|
@@ -115,11 +115,11 @@ new Application(config);
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
### Properties
|
|
118
|
-
- `cookies` - Instance of Cookies
|
|
119
|
-
- [`session`](#session) - Instance of Session
|
|
120
|
-
- `request` - Instance of Request
|
|
121
|
-
- `handlebars` - Instance of Handlebars (wrapper around Handlebars templating engine)
|
|
122
|
-
- `components` - Instance of Components
|
|
118
|
+
- `cookies` - Instance of `Cookies`, allows you to set a cookie
|
|
119
|
+
- [`session`](#session) - Instance of `Session`, utilities to manage sessions and data
|
|
120
|
+
- `request` - Instance of `Request`, you will use this to add routes, which is usually done in routes files, more on that in [routes](#route) section
|
|
121
|
+
- `handlebars` - Instance of `Handlebars` (wrapper around Handlebars templating engine)
|
|
122
|
+
- `components` - Instance of `Components`, this is the components registry, you should never need to use this directly
|
|
123
123
|
|
|
124
124
|
### Methods
|
|
125
125
|
- `init(): Promise<void>` - initializes application, you only need to run this if you set `autoInit = false` in config, otherwise this will be ran when you create the Application instance
|
|
@@ -188,7 +188,7 @@ app.exportContextFields('user');
|
|
|
188
188
|
### Session
|
|
189
189
|
Session allows you to store temporary data for the users of your web application. You don't need to create an instance of Session, you will always use the instance `Application.session`.
|
|
190
190
|
|
|
191
|
-
Session data is tied to a visitor via sessionId, which is always available on `RequestContext`, which means you can interact with session data from routes and server side
|
|
191
|
+
Session data is tied to a visitor via sessionId, which is always available on `RequestContext`, which means you can interact with session data from routes and server side part of your components.
|
|
192
192
|
|
|
193
193
|
**Configuration**\
|
|
194
194
|
`StructuredConfig`.`session`:
|
|
@@ -247,8 +247,16 @@ export default function(app: Application) {
|
|
|
247
247
|
|
|
248
248
|
Route file name has no effect on how the route (request handler) behaves, the only purpose of splitting your routes in separate files is making your code more maintainable.
|
|
249
249
|
|
|
250
|
+
> [!TIP]
|
|
251
|
+
> You can, and should, define the output/input types for your routes.
|
|
252
|
+
> ```
|
|
253
|
+
> app.request.on<OutputType, InputType>(...)
|
|
254
|
+
> ```
|
|
255
|
+
> This allows TypeScript to validate that you are returning the desired type in all return paths and makes interaction with received (POST, PUT) data easier.
|
|
256
|
+
|
|
250
257
|
### RequestContext
|
|
251
|
-
|
|
258
|
+
RequestContext is created for every received request. It contains all the data related to the request, as well as data you include in `RequestContextData`.
|
|
259
|
+
All request handlers receive a `RequestContext` as the first argument. Your component server side part also receives `RequestContext` as the second argument.
|
|
252
260
|
```
|
|
253
261
|
type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
|
|
254
262
|
request: IncomingMessage,
|
|
@@ -306,12 +314,13 @@ Capture group in URL pattern is (name) in above example. It makes data available
|
|
|
306
314
|
```
|
|
307
315
|
app.request.on('GET', '/greet/(userId:num)', async (ctx) => {
|
|
308
316
|
const userId = ctx.args.userId as number;
|
|
317
|
+
|
|
309
318
|
// fetch user from DB
|
|
310
319
|
const user = await userModel.get(userId);
|
|
311
320
|
return `Hello, ${user.name}!`;
|
|
312
321
|
});
|
|
313
322
|
```
|
|
314
|
-
It is safe to cast `ctx.args.userId` as `number` in above example because the route would not get executed if the second segment of the URL is not a numeric value, and in case :num modifier is used, URL-provided value is parsed to a number
|
|
323
|
+
It is safe to cast `ctx.args.userId` as `number` in above example because the route would not get executed if the second segment of the URL is not a numeric value, and in case :num modifier is used, URL-provided value is parsed to a number.
|
|
315
324
|
|
|
316
325
|
|
|
317
326
|
**Doing more with less code**\
|
|
@@ -346,13 +355,15 @@ In some edge cases you may need more control of when a route is executed, in whi
|
|
|
346
355
|
> ```
|
|
347
356
|
|
|
348
357
|
## Document
|
|
349
|
-
Document does not differ much from a component, in fact, it extends Component. It has a more user-friendly API than Component. Each Document represents a web page. It has a head and body. Structured intentionally does not differentiate between a page and a Component - page is just a component that loads many other components in a desired layout. DocumentHead (each document has one at Document.head) allows adding content to `<head>` section of the output HTML page.
|
|
358
|
+
Document does not differ much from a component, in fact, it extends Component. It has a more user-friendly API than Component. Each Document represents a web page. It has a head and body. Structured intentionally does not differentiate between a page and a Component - page is just a component that loads many other components in a desired layout. `DocumentHead` (each document has one at Document.head) allows adding content to `<head>` section of the output HTML page.
|
|
350
359
|
|
|
351
360
|
Creating a document:
|
|
352
361
|
`const doc = new Document(app, 'HelloWorld page', ctx);`
|
|
353
362
|
|
|
354
363
|
Send document as a response:
|
|
355
364
|
```
|
|
365
|
+
import { Document } from 'structured-fw/Document';
|
|
366
|
+
|
|
356
367
|
app.request.on('GET', '/home', async (ctx) => {
|
|
357
368
|
const doc = new Document(app, 'Home', ctx);
|
|
358
369
|
await doc.loadComponent('Home');
|
|
@@ -373,7 +384,7 @@ app.request.on('GET', '/home', async (ctx) => {
|
|
|
373
384
|
|
|
374
385
|
## Component
|
|
375
386
|
A component is comprised of [1-3 files](#component-parts). It always must include one HTML file, while server side and client side files are optional.
|
|
376
|
-
* HTML file
|
|
387
|
+
* HTML file requires no explanation
|
|
377
388
|
* server side file, code that runs on the server and makes data available to HTML and client side code
|
|
378
389
|
* client side file, code that runs on the client (in the browser)
|
|
379
390
|
|
|
@@ -404,11 +415,13 @@ It is recommended, but not necessary, that you contain each component in it's ow
|
|
|
404
415
|
- [Component client-side code](#component-client-side-code) (_ComponentName.client.ts_)
|
|
405
416
|
|
|
406
417
|
### Component HTML
|
|
407
|
-
Let's create a HelloWorld Component `/app/views/HelloWorld/HelloWorld.html
|
|
418
|
+
Let's create a HelloWorld Component `/app/views/HelloWorld/HelloWorld.html` with contents:\
|
|
408
419
|
`Hello, World!`
|
|
409
420
|
|
|
410
421
|
Let's load this Component into a Document and send it as a response `/app/routes/HelloWorld.ts`:
|
|
411
422
|
```
|
|
423
|
+
import { Document } from 'structured-fw/Document';
|
|
424
|
+
|
|
412
425
|
export default function(app: Application) {
|
|
413
426
|
app.request.on('GET', '/hello/world', async (ctx) => {
|
|
414
427
|
const doc = new Document(app, 'Hello, World! From a Component', ctx);
|
|
@@ -466,7 +479,7 @@ We just generated a random number, but the data could be anything and will more
|
|
|
466
479
|
> Server side `getData` will receive the following arguments:
|
|
467
480
|
> - `data: LooseObject` any data passed in (either by attributes, ClientComponent.add or ClientComponent.redraw)
|
|
468
481
|
> - `ctx: RequestContext` - current `RequestContext`, you will often use this to access for example ctx.data (`RequestContextData`) or ctx.sessionId to interact with session
|
|
469
|
-
> - `app: Application` -
|
|
482
|
+
> - `app: Application` - `Application` instance. You can use it to, for example, access the session in combination with ctx.sessionId
|
|
470
483
|
|
|
471
484
|
Let's make it even more interesting by adding some client side code to it.
|
|
472
485
|
|
|
@@ -551,7 +564,7 @@ which is now available in `AnotherComponent` HTML, we assigned the received numb
|
|
|
551
564
|
What about client side? **By default, data returned by server side code is not available in client side code** for obvious reasons, let's assume your server side code returns sensitive data such as user's password, you would not like that exposed on the client side, hence exporting data needs to be explicitly requested in the server side code. There are two ways to achieve this, setting `exportData = true` (exports all data), or `exportFields: Array<string> = [...keysToExport]` (export only given fields).
|
|
552
565
|
|
|
553
566
|
> [!NOTE]
|
|
554
|
-
> Whenever a component with server-side code is rendered, `getData` is automatically called and anything it returns is available in HTML. You can export all returned data to client-side code by setting `exportData = true` or you can export some of the fields by setting `exportFields = ["field1", "field2", ...]` as a direct property of the class. To access the exported data from client-side use `ClientComponent`.`getData(key: string)` which will be `this.getData(key:string)` within client side
|
|
567
|
+
> Whenever a component with server-side code is rendered, `getData` is automatically called and anything it returns is available in HTML. You can export all returned data to client-side code by setting `exportData = true` or you can export some of the fields by setting `exportFields = ["field1", "field2", ...] as const` as a direct property of the class. To access the exported data from client-side use `ClientComponent`.`getData(key: string)` which will be `this.getData(key:string)` within client side part.
|
|
555
568
|
|
|
556
569
|
Let's create a client side code for `AnotherComponent` and export the `betterNumber` to it, create `/app/views/AnotherComponent/AnotherComponent.client.ts`:
|
|
557
570
|
```
|
|
@@ -567,7 +580,7 @@ And let's update `AnotherComponent.ts` to export `betterNumber`:
|
|
|
567
580
|
```
|
|
568
581
|
import { ComponentScaffold } from 'structured-fw/Types';
|
|
569
582
|
export default class AnotherComponent implements ComponentScaffold {
|
|
570
|
-
exportFields = ['betterNumber'];
|
|
583
|
+
exportFields = ['betterNumber'] as const;
|
|
571
584
|
async getData(data: { number: number }): Promise<{
|
|
572
585
|
parentSuggests: number,
|
|
573
586
|
betterNumber: number
|
|
@@ -580,7 +593,7 @@ export default class AnotherComponent implements ComponentScaffold {
|
|
|
580
593
|
}
|
|
581
594
|
```
|
|
582
595
|
|
|
583
|
-
The only change is we added `exportFields = ['betterNumber'];`, that's all there is to it, better number is now available to component's client side
|
|
596
|
+
The only change is we added `exportFields = ['betterNumber'] as const;`, that's all there is to it, better number is now available to component's client side part, again, any type of data can be exported and type of data is preserved in the process.
|
|
584
597
|
|
|
585
598
|
**What about passing data from children to parent?**\
|
|
586
599
|
This concept is wrong to start with, if we want a component to be independent, it should not assume it's parent to exist, or behave in any specific way. That being said, components can access each other, and communicate, even from child to parent (only in client side code).
|
|
@@ -628,7 +641,11 @@ export const init: InitializerFunction = async function() {
|
|
|
628
641
|
|
|
629
642
|
That's it. If there is `AnotherComponent` found within `HelloWorld` (which there is in our case) we are subscribing to "truth" event and capturing the payload. Payload is optional, sometimes we just want to inform anyone interested that a certain event has occurred, without the need to pass any extra data with it. We used `this.find(componentName: string)`, this will recursively find the first instance of a component with `componentName`, optionally you can make it non-recursive by passing `false` as the second argument to `find` method in which case it will look for a direct child with given name.
|
|
630
643
|
|
|
631
|
-
|
|
644
|
+
> [!IMPORTANT]
|
|
645
|
+
> To bind an event listener, in example above, we used `child.on`, while this works, direct use of `child.on` is discouraged, it is highly recommended to use `this.bind<PayloadType>(child, eventName, callback)` _(possible since v1.1.5)_. Using `.bind` makes sure event listener is re-bound on component redraw, using `.on` will bind the same event listener every time the listener component is redrawn.
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
We have only scratched the surface of what client-side part of a component is capable of. Which brings us to `this`. In client-side code of a component, `this` is the instance of a `ClientComponent`.
|
|
632
649
|
|
|
633
650
|
I won't list all of it's properties here, but a few notable mentions are:
|
|
634
651
|
|
|
@@ -647,7 +664,11 @@ Methods:
|
|
|
647
664
|
- `find(componentName: string, recursive: boolean = true): ClientComponent | null` - find a child component
|
|
648
665
|
- `findParent(componentName: string): ClientComponent | null` - find the first parent with given name
|
|
649
666
|
- `query(componentName: string, recursive: boolean = true): Array<ClientComponent>` - return all components with given name found within this component, if `recursive = false`, only direct children are considered
|
|
650
|
-
- `bind<T extends LooseObject | undefined = undefined
|
|
667
|
+
- `bind<T extends LooseObject | undefined = undefined>`(\
|
|
668
|
+
`element: HTMLElement | Window | Array<HTMLElement | Window> | ClientComponent`,\
|
|
669
|
+
`event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | string`,\
|
|
670
|
+
`callback: ClientComponentEventCallback<T> | EventEmitterCallback<T>`\
|
|
671
|
+
): `void` - adds event listener(s) to given HTML element(s) / components. This is preferred over `HTMLElement`.`.addEventListener` / `ClientComponent`.`on` because when the component is redrawn/removed, the event listeners added using bind method are automatically restored/removed. Callback receives event as the first argument. Any "data-" prefixed attributes found on `element` are parsed into an object and provided as second argument to callback (you can specify data using attr helper if you want to pass in something other than a string). Third argument provided to callback is the `element`. The method is generic, allowing you to specify expected data type received as the second argument.
|
|
651
672
|
- `ref<T>(refName: string): T` - get a HTMLElement or ClientComponent that has attribute `ref="[refName]"`
|
|
652
673
|
- `arrayRef<T>(refName: string): Array<T>` - get an array of HTMLElement or ClientComponent that have attribute `array:ref="[refName]"`
|
|
653
674
|
- `add(appendTo: HTMLElement, componentName: string, data?: LooseObject): Promise<ClientComponent | null>` - add `componentName` component to `appendTo` element, optionally passing `data` to the component when it's being rendered. Returns a promise that resolves with added ClientComponent or null if something went wrong
|
|
@@ -730,7 +751,7 @@ You can use two modifier attributes with `data-model`:
|
|
|
730
751
|
- `data-nullable`
|
|
731
752
|
|
|
732
753
|
`data-type` - cast value to given type. Can be one of number | boolean | string, string has no effect as HTMLInput values are already a string by default.\
|
|
733
|
-
If number: if input is empty or value casts to `NaN` then `0` (unless `data-nullable` in which case `null`),
|
|
754
|
+
If number: if input is empty or value casts to `NaN` then `0` (unless `data-nullable` in which case `null`), otherwise the casted number (uses parseFloat so it works with decimal numbers)\
|
|
734
755
|
If boolean: `"1"` and `"true"` casted to `true`, otherwise `false`\
|
|
735
756
|
If string no type casting is attempted.
|
|
736
757
|
|
|
@@ -2,9 +2,12 @@ import { EventEmitterCallback } from './types/eventEmitter.types.js';
|
|
|
2
2
|
export declare class EventEmitter<T extends Record<string, any> = Record<string, any>> {
|
|
3
3
|
protected listeners: Partial<Record<Extract<keyof T, string>, Array<EventEmitterCallback<any>>>>;
|
|
4
4
|
protected destroyed: boolean;
|
|
5
|
+
protected ready: boolean;
|
|
6
|
+
private eventQueue;
|
|
5
7
|
on<K extends Extract<keyof T, string>>(eventName: K, callback: EventEmitterCallback<T[K]>): void;
|
|
6
|
-
emit(eventName: Extract<keyof T, string>, payload?: any): void
|
|
8
|
+
emit(eventName: Extract<keyof T, string>, payload?: any): Promise<void>;
|
|
7
9
|
off(eventName: keyof T, callback: EventEmitterCallback<any>): void;
|
|
10
|
+
emitterReady(): Promise<void>;
|
|
8
11
|
unbindAllListeners(): void;
|
|
9
12
|
emitterDestroy(): void;
|
|
10
13
|
}
|
|
@@ -2,6 +2,8 @@ export class EventEmitter {
|
|
|
2
2
|
constructor() {
|
|
3
3
|
this.listeners = {};
|
|
4
4
|
this.destroyed = false;
|
|
5
|
+
this.ready = false;
|
|
6
|
+
this.eventQueue = [];
|
|
5
7
|
}
|
|
6
8
|
on(eventName, callback) {
|
|
7
9
|
if (this.destroyed) {
|
|
@@ -10,31 +12,47 @@ export class EventEmitter {
|
|
|
10
12
|
if (!Array.isArray(this.listeners[eventName])) {
|
|
11
13
|
this.listeners[eventName] = [];
|
|
12
14
|
}
|
|
15
|
+
if (this.listeners[eventName].indexOf(callback) > -1) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
13
18
|
this.listeners[eventName].push(callback);
|
|
14
19
|
}
|
|
15
|
-
emit(eventName, payload) {
|
|
20
|
+
async emit(eventName, payload) {
|
|
16
21
|
if (this.destroyed) {
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
if (!this.ready) {
|
|
25
|
+
this.eventQueue.push({
|
|
26
|
+
event: eventName,
|
|
27
|
+
payload
|
|
22
28
|
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(this.listeners[eventName]) || Array.isArray(this.listeners['*'])) {
|
|
32
|
+
const listeners = (this.listeners[eventName] || []).concat(this.listeners['*'] || []);
|
|
33
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
34
|
+
await listeners[i](payload, eventName);
|
|
35
|
+
}
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
off(eventName, callback) {
|
|
26
39
|
if (Array.isArray(this.listeners[eventName])) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.listeners[eventName].splice(index, 1);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
40
|
+
const index = this.listeners[eventName].indexOf(callback);
|
|
41
|
+
if (index > -1) {
|
|
42
|
+
this.listeners[eventName].splice(index, 1);
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
async emitterReady() {
|
|
47
|
+
if (this.ready) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.ready = true;
|
|
51
|
+
for (let i = 0; i < this.eventQueue.length; i++) {
|
|
52
|
+
await this.emit(this.eventQueue[i].event, this.eventQueue[i].payload);
|
|
53
|
+
}
|
|
54
|
+
this.eventQueue.length = 0;
|
|
55
|
+
}
|
|
38
56
|
unbindAllListeners() {
|
|
39
57
|
this.listeners = {};
|
|
40
58
|
}
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export class Client {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.Components = new App();
|
|
6
|
-
this.Net = new Net();
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
new App();
|
|
1
|
+
import { ClientApplication } from './ClientApplication.js';
|
|
2
|
+
new ClientApplication();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DataStore } from './DataStore.js';
|
|
2
|
+
import { ClientComponent } from './ClientComponent.js';
|
|
3
|
+
import { InitializerFunction } from '../types/component.types.js';
|
|
4
|
+
export declare class ClientApplication {
|
|
5
|
+
root: ClientComponent;
|
|
6
|
+
store: DataStore;
|
|
7
|
+
initializers: Record<string, InitializerFunction>;
|
|
8
|
+
constructor();
|
|
9
|
+
getInitializer(componentName: string): InitializerFunction | null;
|
|
10
|
+
private loadInitializers;
|
|
11
|
+
hasInitializer(componentName: string): boolean;
|
|
12
|
+
registerInitializer(componentName: string, initializerFunctionString: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DataStore } from './DataStore.js';
|
|
2
|
+
import { ClientComponent } from './ClientComponent.js';
|
|
3
|
+
export class ClientApplication {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.store = new DataStore();
|
|
6
|
+
this.initializers = {};
|
|
7
|
+
this.loadInitializers();
|
|
8
|
+
this.root = new ClientComponent(null, 'root', document.body, this);
|
|
9
|
+
}
|
|
10
|
+
getInitializer(componentName) {
|
|
11
|
+
if (!this.hasInitializer(componentName)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return this.initializers[componentName];
|
|
15
|
+
}
|
|
16
|
+
loadInitializers() {
|
|
17
|
+
if (!!window.initializers) {
|
|
18
|
+
for (const componentName in window.initializers) {
|
|
19
|
+
this.registerInitializer(componentName, window.initializers[componentName]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
hasInitializer(componentName) {
|
|
24
|
+
return componentName in this.initializers;
|
|
25
|
+
}
|
|
26
|
+
registerInitializer(componentName, initializerFunctionString) {
|
|
27
|
+
if (this.hasInitializer(componentName)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const AsyncFunction = async function () { }.constructor;
|
|
31
|
+
const initializerFunction = new AsyncFunction(`
|
|
32
|
+
if (!this.destroyed) {
|
|
33
|
+
const init = ${initializerFunctionString};
|
|
34
|
+
try {
|
|
35
|
+
await init.apply(this, [...arguments]);
|
|
36
|
+
} catch(e) {
|
|
37
|
+
console.error('Error in component ${componentName}: ' + e.message, this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`);
|
|
41
|
+
this.initializers[componentName] = initializerFunction;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { LooseObject } from '../types/general.types.js';
|
|
2
2
|
import { ClientComponentEventCallback } from '../types/component.types.js';
|
|
3
3
|
import { DataStoreView } from './DataStoreView.js';
|
|
4
|
-
import { DataStore } from './DataStore.js';
|
|
5
4
|
import { Net } from './Net.js';
|
|
6
5
|
import { EventEmitter } from '../EventEmitter.js';
|
|
6
|
+
import { ClientApplication } from './ClientApplication.js';
|
|
7
|
+
import { EventEmitterCallback } from '../types/eventEmitter.types.js';
|
|
7
8
|
export declare class ClientComponent extends EventEmitter {
|
|
8
9
|
readonly name: string;
|
|
9
10
|
children: Array<ClientComponent>;
|
|
@@ -12,9 +13,10 @@ export declare class ClientComponent extends EventEmitter {
|
|
|
12
13
|
readonly isRoot: boolean;
|
|
13
14
|
readonly root: ClientComponent;
|
|
14
15
|
store: DataStoreView;
|
|
15
|
-
private
|
|
16
|
+
private app;
|
|
16
17
|
readonly net: Net;
|
|
17
18
|
private initializerExecuted;
|
|
19
|
+
readonly fn: Record<string, (...args: Array<any>) => any | undefined>;
|
|
18
20
|
destroyed: boolean;
|
|
19
21
|
private redrawRequest;
|
|
20
22
|
private bound;
|
|
@@ -25,7 +27,7 @@ export declare class ClientComponent extends EventEmitter {
|
|
|
25
27
|
private refsArray;
|
|
26
28
|
isReady: boolean;
|
|
27
29
|
private data;
|
|
28
|
-
constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement,
|
|
30
|
+
constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement, app: ClientApplication);
|
|
29
31
|
private init;
|
|
30
32
|
private reset;
|
|
31
33
|
private runInitializer;
|
|
@@ -46,7 +48,6 @@ export declare class ClientComponent extends EventEmitter {
|
|
|
46
48
|
private execCondition;
|
|
47
49
|
conditionalCallback(name: string, callback: (args?: any) => boolean, updateConditionals?: boolean): void;
|
|
48
50
|
private updateConditionals;
|
|
49
|
-
remove(): Promise<void>;
|
|
50
51
|
parentFind(parentName: string): ClientComponent | null;
|
|
51
52
|
find(componentName: string, recursive?: boolean): null | ClientComponent;
|
|
52
53
|
query(componentName: string, recursive?: boolean, results?: Array<ClientComponent>): Array<ClientComponent>;
|
|
@@ -57,8 +58,11 @@ export declare class ClientComponent extends EventEmitter {
|
|
|
57
58
|
private transitionAttributes;
|
|
58
59
|
private transitionAxis;
|
|
59
60
|
private destroy;
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
remove(): Promise<void>;
|
|
62
|
+
bind<T extends any>(element: ClientComponent, event: string, callback: EventEmitterCallback<T>): void;
|
|
63
|
+
bind<T extends LooseObject | undefined>(element: HTMLElement | Window | Array<HTMLElement | Window>, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
|
|
64
|
+
unbind<T extends LooseObject | undefined = undefined>(element: HTMLElement | Window | ClientComponent, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T> | EventEmitterCallback<T>): void;
|
|
65
|
+
private unbindOwn;
|
|
62
66
|
private unbindAll;
|
|
63
67
|
log(msg: any): void;
|
|
64
68
|
warn(msg: any): void;
|
|
@@ -4,7 +4,7 @@ import { Net } from './Net.js';
|
|
|
4
4
|
import { NetRequest } from './NetRequest.js';
|
|
5
5
|
import { EventEmitter } from '../EventEmitter.js';
|
|
6
6
|
export class ClientComponent extends EventEmitter {
|
|
7
|
-
constructor(parent, name, domNode,
|
|
7
|
+
constructor(parent, name, domNode, app) {
|
|
8
8
|
super();
|
|
9
9
|
this.children = [];
|
|
10
10
|
this.net = new Net();
|
|
@@ -31,15 +31,35 @@ export class ClientComponent extends EventEmitter {
|
|
|
31
31
|
this.root = parent.root;
|
|
32
32
|
this.parent = parent;
|
|
33
33
|
}
|
|
34
|
-
this.
|
|
35
|
-
this.store = new DataStoreView(this.
|
|
34
|
+
this.app = app;
|
|
35
|
+
this.store = new DataStoreView(this.app.store, this);
|
|
36
|
+
const self = this;
|
|
37
|
+
this.fn = new Proxy(this.store, {
|
|
38
|
+
set(target, key, val) {
|
|
39
|
+
const fnKey = `fn_${key}`;
|
|
40
|
+
if (target.has(fnKey)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
target.set(fnKey, val);
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
get(target, key) {
|
|
47
|
+
return target.get(`fn_${key}`) || (() => {
|
|
48
|
+
self.warn(`Function ${key} not defined`);
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|
|
36
52
|
if (this.isRoot) {
|
|
37
53
|
this.init(false);
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
|
-
async init(isRedraw) {
|
|
41
|
-
const initializerExists =
|
|
56
|
+
async init(isRedraw, data = {}, isRedrawRoot = false) {
|
|
57
|
+
const initializerExists = this.app.hasInitializer(this.name);
|
|
42
58
|
this.reset();
|
|
59
|
+
this.initData();
|
|
60
|
+
objectEach(data, (key, val) => {
|
|
61
|
+
this.setData(key, val, false);
|
|
62
|
+
});
|
|
43
63
|
this.initChildren();
|
|
44
64
|
await Promise.all(this.children.map(async (child) => {
|
|
45
65
|
await child.init(isRedraw);
|
|
@@ -48,12 +68,8 @@ export class ClientComponent extends EventEmitter {
|
|
|
48
68
|
this.store.import(undefined, false, false);
|
|
49
69
|
}
|
|
50
70
|
this.initRefs();
|
|
51
|
-
this.initData();
|
|
52
71
|
this.initModels();
|
|
53
|
-
this.promoteRefs();
|
|
54
72
|
this.initConditionals();
|
|
55
|
-
await this.runInitializer(isRedraw);
|
|
56
|
-
this.updateConditionals(false);
|
|
57
73
|
this.store.onChange('*', () => {
|
|
58
74
|
this.updateConditionals(true);
|
|
59
75
|
});
|
|
@@ -62,6 +78,9 @@ export class ClientComponent extends EventEmitter {
|
|
|
62
78
|
this.redraw();
|
|
63
79
|
}
|
|
64
80
|
this.isReady = true;
|
|
81
|
+
if (this.isRoot || isRedrawRoot) {
|
|
82
|
+
await this.runInitializer(isRedraw);
|
|
83
|
+
}
|
|
65
84
|
this.emit('ready');
|
|
66
85
|
}
|
|
67
86
|
reset() {
|
|
@@ -69,45 +88,32 @@ export class ClientComponent extends EventEmitter {
|
|
|
69
88
|
this.isReady = false;
|
|
70
89
|
this.refs = {};
|
|
71
90
|
this.refsArray = {};
|
|
72
|
-
this.conditionalClassNames =
|
|
91
|
+
this.conditionalClassNames.length = 0;
|
|
73
92
|
this.conditionalCallbacks = {};
|
|
74
|
-
this.conditionals =
|
|
93
|
+
this.conditionals.length = 0;
|
|
75
94
|
this.redrawRequest = null;
|
|
76
95
|
this.initializerExecuted = false;
|
|
77
|
-
this.bound =
|
|
78
|
-
this.children =
|
|
96
|
+
this.bound.length = 0;
|
|
97
|
+
this.children.length = 0;
|
|
79
98
|
}
|
|
80
99
|
async runInitializer(isRedraw = false) {
|
|
81
|
-
|
|
82
|
-
if (!initializer) {
|
|
100
|
+
if (this.destroyed || this.initializerExecuted) {
|
|
83
101
|
return;
|
|
84
102
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
`);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
initializerFunction = initializer;
|
|
102
|
-
}
|
|
103
|
-
if (initializerFunction) {
|
|
104
|
-
await initializerFunction.apply(this, [{
|
|
105
|
-
net: this.net,
|
|
106
|
-
isRedraw
|
|
107
|
-
}]);
|
|
108
|
-
}
|
|
103
|
+
const initializer = this.app.getInitializer(this.name);
|
|
104
|
+
if (initializer !== null) {
|
|
105
|
+
await initializer.apply(this, [{
|
|
106
|
+
net: this.net,
|
|
107
|
+
isRedraw
|
|
108
|
+
}]);
|
|
109
|
+
this.updateConditionals(false);
|
|
110
|
+
}
|
|
111
|
+
await this.emitterReady();
|
|
112
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
113
|
+
await this.children[i].runInitializer(isRedraw);
|
|
109
114
|
}
|
|
110
115
|
this.initializerExecuted = true;
|
|
116
|
+
this.emit('initializerExecuted');
|
|
111
117
|
}
|
|
112
118
|
initData() {
|
|
113
119
|
objectEach(this.attributeData(this.domNode), (key, val) => {
|
|
@@ -159,7 +165,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
159
165
|
const childNode = scope.childNodes[i];
|
|
160
166
|
if (childNode.nodeType == 1) {
|
|
161
167
|
if (childNode.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`)) {
|
|
162
|
-
const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.
|
|
168
|
+
const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.app);
|
|
163
169
|
this.children.push(component);
|
|
164
170
|
if (typeof callback === 'function') {
|
|
165
171
|
callback(component);
|
|
@@ -179,11 +185,12 @@ export class ClientComponent extends EventEmitter {
|
|
|
179
185
|
if (this.destroyed) {
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
182
|
-
this.emit('beforeRedraw');
|
|
188
|
+
await this.emit('beforeRedraw');
|
|
183
189
|
if (this.redrawRequest !== null) {
|
|
184
190
|
this.redrawRequest.abort();
|
|
185
191
|
this.redrawRequest = null;
|
|
186
192
|
}
|
|
193
|
+
this.unbindOwn();
|
|
187
194
|
const redrawRequest = new NetRequest('POST', window.structuredClientConfig.componentRender, {
|
|
188
195
|
'content-type': 'application/json'
|
|
189
196
|
});
|
|
@@ -204,19 +211,14 @@ export class ClientComponent extends EventEmitter {
|
|
|
204
211
|
for (let i = 0; i < childrenOld.length; i++) {
|
|
205
212
|
const child = childrenOld[i];
|
|
206
213
|
childStoreChangeCallbacks[child.getData('componentId')] = child.store.onChangeCallbacks();
|
|
207
|
-
await child.
|
|
214
|
+
await child.destroy();
|
|
208
215
|
}
|
|
209
216
|
const componentData = JSON.parse(componentDataJSON);
|
|
210
217
|
this.domNode.innerHTML = componentData.html;
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
});
|
|
214
|
-
for (const key in componentData.initializers) {
|
|
215
|
-
if (!window.initializers[key]) {
|
|
216
|
-
window.initializers[key] = componentData.initializers[key];
|
|
217
|
-
}
|
|
218
|
+
for (const componentName in componentData.initializers) {
|
|
219
|
+
this.app.registerInitializer(componentName, componentData.initializers[componentName]);
|
|
218
220
|
}
|
|
219
|
-
await this.init(true);
|
|
221
|
+
await this.init(true, componentData.data, true);
|
|
220
222
|
for (let i = 0; i < this.children.length; i++) {
|
|
221
223
|
const childNew = this.children[i];
|
|
222
224
|
const childNewId = childNew.getData('componentId');
|
|
@@ -229,7 +231,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
229
231
|
});
|
|
230
232
|
}
|
|
231
233
|
}
|
|
232
|
-
this.emit('afterRedraw');
|
|
234
|
+
await this.emit('afterRedraw');
|
|
233
235
|
}
|
|
234
236
|
initConditionals(node) {
|
|
235
237
|
const isSelf = node === undefined;
|
|
@@ -274,6 +276,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
274
276
|
this.initRefs(child);
|
|
275
277
|
}
|
|
276
278
|
});
|
|
279
|
+
this.promoteRefs();
|
|
277
280
|
}
|
|
278
281
|
initModels(node, modelNodes = []) {
|
|
279
282
|
const isSelf = node === undefined;
|
|
@@ -503,19 +506,6 @@ export class ClientComponent extends EventEmitter {
|
|
|
503
506
|
}
|
|
504
507
|
});
|
|
505
508
|
}
|
|
506
|
-
async remove() {
|
|
507
|
-
if (!this.isRoot) {
|
|
508
|
-
const children = Array.from(this.children);
|
|
509
|
-
for (let i = 0; i < children.length; i++) {
|
|
510
|
-
await children[i].remove();
|
|
511
|
-
}
|
|
512
|
-
if (this.parent) {
|
|
513
|
-
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
514
|
-
}
|
|
515
|
-
this.domNode.parentElement?.removeChild(this.domNode);
|
|
516
|
-
await this.destroy();
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
509
|
parentFind(parentName) {
|
|
520
510
|
let parent = this.parent;
|
|
521
511
|
while (true) {
|
|
@@ -578,17 +568,15 @@ export class ClientComponent extends EventEmitter {
|
|
|
578
568
|
unwrap: false
|
|
579
569
|
}));
|
|
580
570
|
const res = JSON.parse(componentDataJSON);
|
|
581
|
-
for (let
|
|
582
|
-
|
|
583
|
-
window.initializers[key] = res.initializers[key];
|
|
584
|
-
}
|
|
571
|
+
for (let componentName in res.initializers) {
|
|
572
|
+
this.app.registerInitializer(componentName, res.initializers[componentName]);
|
|
585
573
|
}
|
|
586
574
|
const tmpContainer = document.createElement('div');
|
|
587
575
|
tmpContainer.innerHTML = res.html;
|
|
588
576
|
const componentNode = tmpContainer.firstChild;
|
|
589
|
-
const component = new ClientComponent(this, componentName, componentNode, this.
|
|
577
|
+
const component = new ClientComponent(this, componentName, componentNode, this.app);
|
|
590
578
|
this.children.push(component);
|
|
591
|
-
await component.init(false);
|
|
579
|
+
await component.init(false, res.data, true);
|
|
592
580
|
container.appendChild(componentNode);
|
|
593
581
|
return component;
|
|
594
582
|
}
|
|
@@ -775,19 +763,26 @@ export class ClientComponent extends EventEmitter {
|
|
|
775
763
|
this.redrawRequest.abort();
|
|
776
764
|
this.redrawRequest = null;
|
|
777
765
|
}
|
|
778
|
-
this.emit('beforeDestroy');
|
|
766
|
+
await this.emit('beforeDestroy');
|
|
767
|
+
const children = Array.from(this.children);
|
|
768
|
+
for (let i = 0; i < children.length; i++) {
|
|
769
|
+
await children[i].destroy();
|
|
770
|
+
}
|
|
771
|
+
this.domNode.parentElement?.removeChild(this.domNode);
|
|
772
|
+
this.domNode.innerHTML = '';
|
|
773
|
+
if (this.parent) {
|
|
774
|
+
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
775
|
+
}
|
|
779
776
|
this.store.destroy();
|
|
780
777
|
this.unbindAll();
|
|
781
|
-
this.
|
|
782
|
-
this.conditionalClassNames = [];
|
|
783
|
-
this.conditionalCallbacks = {};
|
|
784
|
-
this.refs = {};
|
|
785
|
-
this.refsArray = {};
|
|
786
|
-
this.data = {};
|
|
778
|
+
this.reset();
|
|
787
779
|
this.destroyed = true;
|
|
788
|
-
this.emit('afterDestroy');
|
|
780
|
+
await this.emit('afterDestroy');
|
|
789
781
|
this.emitterDestroy();
|
|
790
782
|
}
|
|
783
|
+
async remove() {
|
|
784
|
+
await this.destroy();
|
|
785
|
+
}
|
|
791
786
|
bind(element, event, callback) {
|
|
792
787
|
if (Array.isArray(element)) {
|
|
793
788
|
element.forEach((el) => {
|
|
@@ -801,19 +796,33 @@ export class ClientComponent extends EventEmitter {
|
|
|
801
796
|
});
|
|
802
797
|
return;
|
|
803
798
|
}
|
|
804
|
-
|
|
805
|
-
|
|
799
|
+
if (element instanceof HTMLElement || element instanceof Window) {
|
|
800
|
+
const cb = callback;
|
|
806
801
|
const callbackWrapper = (e) => {
|
|
807
|
-
|
|
802
|
+
cb.apply(this, [
|
|
803
|
+
e,
|
|
804
|
+
element instanceof Window ? undefined : this.attributeData(element),
|
|
805
|
+
element
|
|
806
|
+
]);
|
|
808
807
|
};
|
|
809
808
|
this.bound.push({
|
|
810
809
|
element,
|
|
811
|
-
event,
|
|
810
|
+
event: event,
|
|
812
811
|
callback: callbackWrapper,
|
|
813
812
|
callbackOriginal: callback
|
|
814
813
|
});
|
|
815
814
|
element.addEventListener(event, callbackWrapper);
|
|
816
815
|
}
|
|
816
|
+
else if (element instanceof ClientComponent) {
|
|
817
|
+
const cb = callback;
|
|
818
|
+
this.bound.push({
|
|
819
|
+
element,
|
|
820
|
+
event: event,
|
|
821
|
+
callback: cb,
|
|
822
|
+
callbackOriginal: cb
|
|
823
|
+
});
|
|
824
|
+
element.on(event, cb);
|
|
825
|
+
}
|
|
817
826
|
}
|
|
818
827
|
unbind(element, event, callback) {
|
|
819
828
|
if (Array.isArray(event)) {
|
|
@@ -827,13 +836,25 @@ export class ClientComponent extends EventEmitter {
|
|
|
827
836
|
});
|
|
828
837
|
if (boundIndex > -1) {
|
|
829
838
|
const bound = this.bound[boundIndex];
|
|
830
|
-
bound.element
|
|
831
|
-
|
|
839
|
+
if (bound.element instanceof ClientComponent) {
|
|
840
|
+
bound.element.off(bound.event, bound.callback);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
bound.element.removeEventListener(bound.event, bound.callback);
|
|
844
|
+
this.bound.splice(boundIndex, 1);
|
|
845
|
+
}
|
|
832
846
|
}
|
|
833
847
|
}
|
|
848
|
+
unbindOwn() {
|
|
849
|
+
this.bound.forEach((bound) => {
|
|
850
|
+
if (bound.element === this) {
|
|
851
|
+
this.unbind(bound.element, bound.event, bound.callback);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
834
855
|
unbindAll() {
|
|
835
856
|
this.bound.forEach((bound) => {
|
|
836
|
-
bound.element
|
|
857
|
+
this.unbind(bound.element, bound.event, bound.callbackOriginal);
|
|
837
858
|
});
|
|
838
859
|
this.bound = [];
|
|
839
860
|
}
|
|
@@ -10,6 +10,7 @@ export declare class DataStoreView {
|
|
|
10
10
|
get<T>(key: string): T | undefined;
|
|
11
11
|
toggle(key: string): void;
|
|
12
12
|
keys(): Array<string>;
|
|
13
|
+
has(key: string): boolean;
|
|
13
14
|
import(fields?: Array<string>, force?: boolean, triggerListeners?: boolean): void;
|
|
14
15
|
clear(): void;
|
|
15
16
|
destroy(): void;
|
|
@@ -23,7 +23,10 @@ export class DataStoreView {
|
|
|
23
23
|
if (this.destroyed) {
|
|
24
24
|
return [];
|
|
25
25
|
}
|
|
26
|
-
return Object.keys(this.store.get(this.componentId()));
|
|
26
|
+
return Object.keys(this.store.get(this.componentId()) || {});
|
|
27
|
+
}
|
|
28
|
+
has(key) {
|
|
29
|
+
return this.keys().includes(key);
|
|
27
30
|
}
|
|
28
31
|
import(fields, force = false, triggerListeners = true) {
|
|
29
32
|
const fieldsImported = Array.isArray(fields) ? fields : Object.keys(this.component.getData());
|
|
@@ -63,6 +63,9 @@ export class Components {
|
|
|
63
63
|
entry.attributes = entry.serverPart?.attributes;
|
|
64
64
|
entry.static = typeof entry.serverPart?.static === 'boolean' ? entry.serverPart.static : false;
|
|
65
65
|
}
|
|
66
|
+
else {
|
|
67
|
+
entry.exportData = true;
|
|
68
|
+
}
|
|
66
69
|
this.components[componentName.toUpperCase()] = entry;
|
|
67
70
|
this.componentNames.push(entry.name);
|
|
68
71
|
}
|
|
@@ -18,7 +18,7 @@ export declare class Document extends Component<{
|
|
|
18
18
|
componentIds: Array<string>;
|
|
19
19
|
ctx: undefined | RequestContext;
|
|
20
20
|
appendHTML: string;
|
|
21
|
-
constructor(app: Application, title: string, ctx?: RequestContext);
|
|
21
|
+
constructor(app: Application, title: string, ctx?: RequestContext<any>);
|
|
22
22
|
push(response: ServerResponse): void;
|
|
23
23
|
body(): string;
|
|
24
24
|
initInitializers(): Record<string, string>;
|
|
@@ -2,10 +2,11 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { LooseObject } from '../types/general.types.js';
|
|
3
3
|
import { RequestMethod, PostedDataDecoded, RequestBodyFile, RequestCallback } from "../types/request.types.js";
|
|
4
4
|
import { Application } from "./Application.js";
|
|
5
|
+
import { Document } from "./Document.js";
|
|
5
6
|
export declare class Request {
|
|
6
7
|
private app;
|
|
7
8
|
constructor(app: Application);
|
|
8
|
-
pageNotFoundCallback: RequestCallback<void
|
|
9
|
+
pageNotFoundCallback: RequestCallback<void | Document, LooseObject>;
|
|
9
10
|
private readonly handlers;
|
|
10
11
|
on<R extends any, Body extends LooseObject | undefined = LooseObject>(methods: RequestMethod | Array<RequestMethod>, pattern: string | RegExp | Array<string | RegExp>, callback: RequestCallback<R, Body>, scope?: any, isStaticAsset?: boolean): void;
|
|
11
12
|
private getHandler;
|
|
@@ -130,8 +130,11 @@ export class Request {
|
|
|
130
130
|
this.redirect(response, to, statusCode);
|
|
131
131
|
},
|
|
132
132
|
show404: async () => {
|
|
133
|
-
await this.pageNotFoundCallback.apply(this.app, [context]);
|
|
134
133
|
this.app.emit('pageNotFound', context);
|
|
134
|
+
const res = await this.pageNotFoundCallback.apply(this.app, [context]);
|
|
135
|
+
if (res instanceof Document) {
|
|
136
|
+
context.respondWith(res);
|
|
137
|
+
}
|
|
135
138
|
}
|
|
136
139
|
};
|
|
137
140
|
if (handler !== null) {
|
|
@@ -2,8 +2,9 @@ import { ClientComponent } from "../client/ClientComponent.js";
|
|
|
2
2
|
import { Net } from "../client/Net.js";
|
|
3
3
|
import { Application } from "../server/Application.js";
|
|
4
4
|
import { Component } from "../server/Component.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { EventEmitterCallback } from "./eventEmitter.types.js";
|
|
6
|
+
import { KeysOfUnion, LooseObject } from './general.types.js';
|
|
7
|
+
import { RequestContext } from "./request.types.js";
|
|
7
8
|
export type ComponentEntry = {
|
|
8
9
|
name: string;
|
|
9
10
|
path: {
|
|
@@ -21,18 +22,18 @@ export type ComponentEntry = {
|
|
|
21
22
|
static: boolean;
|
|
22
23
|
renderTagName?: string;
|
|
23
24
|
exportData: boolean;
|
|
24
|
-
exportFields?:
|
|
25
|
+
exportFields?: ReadonlyArray<string>;
|
|
25
26
|
attributes?: Record<string, string>;
|
|
26
27
|
initializer?: InitializerFunction;
|
|
27
28
|
};
|
|
28
|
-
export interface ComponentScaffold {
|
|
29
|
+
export interface ComponentScaffold<T extends LooseObject = LooseObject, K extends KeysOfUnion<T> = KeysOfUnion<T>> {
|
|
29
30
|
tagName?: string;
|
|
30
31
|
exportData?: boolean;
|
|
31
|
-
exportFields?:
|
|
32
|
+
exportFields?: ReadonlyArray<K>;
|
|
32
33
|
static?: boolean;
|
|
33
34
|
deferred?: (data: Record<string, any>, ctx: RequestContext | undefined, app: Application) => boolean;
|
|
34
35
|
attributes?: Record<string, string>;
|
|
35
|
-
getData: (this: ComponentScaffold, data:
|
|
36
|
+
getData: (this: ComponentScaffold, data: LooseObject, ctx: undefined | RequestContext, app: Application, component: Component) => Promise<T | void>;
|
|
36
37
|
[key: string]: any;
|
|
37
38
|
}
|
|
38
39
|
export type ClientComponentTransition = {
|
|
@@ -42,11 +43,11 @@ export type ClientComponentTransition = {
|
|
|
42
43
|
};
|
|
43
44
|
export type ClientComponentTransitionEvent = 'show' | 'hide';
|
|
44
45
|
export type ClientComponentTransitions = Record<ClientComponentTransitionEvent, ClientComponentTransition>;
|
|
45
|
-
export type ClientComponentBoundEvent<T extends LooseObject | undefined
|
|
46
|
-
element:
|
|
46
|
+
export type ClientComponentBoundEvent<T extends LooseObject | undefined, E extends HTMLElement | Window | ClientComponent> = {
|
|
47
|
+
element: E;
|
|
47
48
|
event: keyof HTMLElementEventMap;
|
|
48
|
-
callback: (e: Event) => void;
|
|
49
|
-
callbackOriginal: ClientComponentEventCallback<T>;
|
|
49
|
+
callback: E extends ClientComponent ? EventEmitterCallback<T> : (e: Event) => void;
|
|
50
|
+
callbackOriginal: E extends ClientComponent ? EventEmitterCallback<T> : ClientComponentEventCallback<T>;
|
|
50
51
|
};
|
|
51
52
|
export type ClientComponentEventCallback<T> = (e: Event, data: T, element: HTMLElement | Window) => void;
|
|
52
53
|
export type InitializerFunction = (this: ClientComponent, ctx: InitializerFunctionContext) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type EventEmitterCallback<T> = (payload: T, eventName: string) => void
|
|
1
|
+
export type EventEmitterCallback<T> = (payload: T, eventName: string) => void | Promise<void>;
|
package/package.json
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "build/index",
|
|
22
|
-
"version": "1.
|
|
22
|
+
"version": "1.2.0",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"develop": "tsc --watch",
|
|
25
25
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,hbs,css index.js",
|
|
26
26
|
"start": "cd build && node index.js",
|
|
27
27
|
"compileAndPack": "tsc && npm pack",
|
|
28
|
-
"compileAndPublish": "tsc && npm publish"
|
|
28
|
+
"compileAndPublish": "rm -r build && tsc && npm publish"
|
|
29
29
|
},
|
|
30
30
|
"bin": {
|
|
31
31
|
"structured": "./build/system/bin/structured.js"
|