structured-fw 1.1.5 → 1.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 +40 -19
- package/build/system/EventEmitter.d.ts +3 -0
- package/build/system/EventEmitter.js +19 -0
- package/build/system/client/ClientComponent.js +30 -23
- package/build/system/server/Component.d.ts +2 -4
- package/build/system/server/Component.js +9 -1
- package/build/system/server/Components.js +3 -0
- package/build/system/server/Document.d.ts +1 -1
- package/build/system/server/Document.js +7 -0
- package/build/system/server/Layout.js +7 -1
- package/build/system/server/Request.d.ts +2 -1
- package/build/system/server/Request.js +5 -1
- package/build/system/server/dom/DOMNode.d.ts +1 -0
- package/build/system/server/dom/DOMNode.js +6 -0
- package/build/system/types/component.types.d.ts +4 -0
- package/package.json +1 -1
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
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) {
|
|
@@ -19,6 +21,13 @@ export class EventEmitter {
|
|
|
19
21
|
if (this.destroyed) {
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
24
|
+
if (!this.ready) {
|
|
25
|
+
this.eventQueue.push({
|
|
26
|
+
event: eventName,
|
|
27
|
+
payload
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
22
31
|
if (Array.isArray(this.listeners[eventName]) || Array.isArray(this.listeners['*'])) {
|
|
23
32
|
const listeners = (this.listeners[eventName] || []).concat(this.listeners['*'] || []);
|
|
24
33
|
for (let i = 0; i < listeners.length; i++) {
|
|
@@ -34,6 +43,16 @@ export class EventEmitter {
|
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
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
|
+
}
|
|
37
56
|
unbindAllListeners() {
|
|
38
57
|
this.listeners = {};
|
|
39
58
|
}
|
|
@@ -53,7 +53,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
53
53
|
this.init(false);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
async init(isRedraw, data = {}) {
|
|
56
|
+
async init(isRedraw, data = {}, isRedrawRoot = false) {
|
|
57
57
|
const initializerExists = this.app.hasInitializer(this.name);
|
|
58
58
|
this.reset();
|
|
59
59
|
this.initData();
|
|
@@ -70,8 +70,6 @@ export class ClientComponent extends EventEmitter {
|
|
|
70
70
|
this.initRefs();
|
|
71
71
|
this.initModels();
|
|
72
72
|
this.initConditionals();
|
|
73
|
-
await this.runInitializer(isRedraw);
|
|
74
|
-
this.updateConditionals(false);
|
|
75
73
|
this.store.onChange('*', () => {
|
|
76
74
|
this.updateConditionals(true);
|
|
77
75
|
});
|
|
@@ -80,6 +78,9 @@ export class ClientComponent extends EventEmitter {
|
|
|
80
78
|
this.redraw();
|
|
81
79
|
}
|
|
82
80
|
this.isReady = true;
|
|
81
|
+
if (this.isRoot || isRedrawRoot) {
|
|
82
|
+
await this.runInitializer(isRedraw);
|
|
83
|
+
}
|
|
83
84
|
this.emit('ready');
|
|
84
85
|
}
|
|
85
86
|
reset() {
|
|
@@ -87,26 +88,32 @@ export class ClientComponent extends EventEmitter {
|
|
|
87
88
|
this.isReady = false;
|
|
88
89
|
this.refs = {};
|
|
89
90
|
this.refsArray = {};
|
|
90
|
-
this.conditionalClassNames =
|
|
91
|
+
this.conditionalClassNames.length = 0;
|
|
91
92
|
this.conditionalCallbacks = {};
|
|
92
|
-
this.conditionals =
|
|
93
|
+
this.conditionals.length = 0;
|
|
93
94
|
this.redrawRequest = null;
|
|
94
95
|
this.initializerExecuted = false;
|
|
95
|
-
this.bound =
|
|
96
|
-
this.children =
|
|
96
|
+
this.bound.length = 0;
|
|
97
|
+
this.children.length = 0;
|
|
97
98
|
}
|
|
98
99
|
async runInitializer(isRedraw = false) {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
if (this.destroyed || this.initializerExecuted) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const initializer = this.app.getInitializer(this.name);
|
|
104
|
+
if (initializer !== null) {
|
|
104
105
|
await initializer.apply(this, [{
|
|
105
106
|
net: this.net,
|
|
106
107
|
isRedraw
|
|
107
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);
|
|
108
114
|
}
|
|
109
115
|
this.initializerExecuted = true;
|
|
116
|
+
this.emit('initializerExecuted');
|
|
110
117
|
}
|
|
111
118
|
initData() {
|
|
112
119
|
objectEach(this.attributeData(this.domNode), (key, val) => {
|
|
@@ -211,7 +218,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
211
218
|
for (const componentName in componentData.initializers) {
|
|
212
219
|
this.app.registerInitializer(componentName, componentData.initializers[componentName]);
|
|
213
220
|
}
|
|
214
|
-
await this.init(true, componentData.data);
|
|
221
|
+
await this.init(true, componentData.data, true);
|
|
215
222
|
for (let i = 0; i < this.children.length; i++) {
|
|
216
223
|
const childNew = this.children[i];
|
|
217
224
|
const childNewId = childNew.getData('componentId');
|
|
@@ -569,7 +576,7 @@ export class ClientComponent extends EventEmitter {
|
|
|
569
576
|
const componentNode = tmpContainer.firstChild;
|
|
570
577
|
const component = new ClientComponent(this, componentName, componentNode, this.app);
|
|
571
578
|
this.children.push(component);
|
|
572
|
-
await component.init(false, res.data);
|
|
579
|
+
await component.init(false, res.data, true);
|
|
573
580
|
container.appendChild(componentNode);
|
|
574
581
|
return component;
|
|
575
582
|
}
|
|
@@ -581,10 +588,10 @@ export class ClientComponent extends EventEmitter {
|
|
|
581
588
|
}
|
|
582
589
|
show(domNode, enableTransition = true) {
|
|
583
590
|
if (!enableTransition) {
|
|
584
|
-
domNode.
|
|
591
|
+
domNode.classList.remove('structured-hidden');
|
|
585
592
|
return;
|
|
586
593
|
}
|
|
587
|
-
if (domNode.
|
|
594
|
+
if (!domNode.classList.contains('structured-hidden')) {
|
|
588
595
|
return;
|
|
589
596
|
}
|
|
590
597
|
const transitions = this.transitionAttributes(domNode).show;
|
|
@@ -595,11 +602,10 @@ export class ClientComponent extends EventEmitter {
|
|
|
595
602
|
prev[key] = transitions[key];
|
|
596
603
|
return prev;
|
|
597
604
|
}, {});
|
|
605
|
+
domNode.classList.remove('structured-hidden');
|
|
598
606
|
if (Object.keys(transitionsActive).length === 0) {
|
|
599
|
-
domNode.style.display = '';
|
|
600
607
|
return;
|
|
601
608
|
}
|
|
602
|
-
domNode.style.display = '';
|
|
603
609
|
const onTransitionEnd = (e) => {
|
|
604
610
|
domNode.style.opacity = '1';
|
|
605
611
|
domNode.style.transition = '';
|
|
@@ -645,10 +651,10 @@ export class ClientComponent extends EventEmitter {
|
|
|
645
651
|
}
|
|
646
652
|
hide(domNode, enableTransition = true) {
|
|
647
653
|
if (!enableTransition) {
|
|
648
|
-
domNode.
|
|
654
|
+
domNode.classList.add('structured-hidden');
|
|
649
655
|
return;
|
|
650
656
|
}
|
|
651
|
-
if (domNode.
|
|
657
|
+
if (domNode.classList.contains('structured-hidden')) {
|
|
652
658
|
return;
|
|
653
659
|
}
|
|
654
660
|
const transitions = this.transitionAttributes(domNode).hide;
|
|
@@ -660,11 +666,11 @@ export class ClientComponent extends EventEmitter {
|
|
|
660
666
|
return prev;
|
|
661
667
|
}, {});
|
|
662
668
|
if (Object.keys(transitionsActive).length === 0) {
|
|
663
|
-
domNode.
|
|
669
|
+
domNode.classList.add('structured-hidden');
|
|
664
670
|
}
|
|
665
671
|
else {
|
|
666
672
|
const onTransitionEnd = (e) => {
|
|
667
|
-
domNode.
|
|
673
|
+
domNode.classList.add('structured-hidden');
|
|
668
674
|
domNode.style.opacity = '1';
|
|
669
675
|
domNode.style.transition = '';
|
|
670
676
|
domNode.style.transformOrigin = 'unset';
|
|
@@ -757,11 +763,12 @@ export class ClientComponent extends EventEmitter {
|
|
|
757
763
|
this.redrawRequest = null;
|
|
758
764
|
}
|
|
759
765
|
await this.emit('beforeDestroy');
|
|
760
|
-
this.domNode.parentElement?.removeChild(this.domNode);
|
|
761
766
|
const children = Array.from(this.children);
|
|
762
767
|
for (let i = 0; i < children.length; i++) {
|
|
763
768
|
await children[i].destroy();
|
|
764
769
|
}
|
|
770
|
+
this.domNode.parentElement?.removeChild(this.domNode);
|
|
771
|
+
this.domNode.innerHTML = '';
|
|
765
772
|
if (this.parent) {
|
|
766
773
|
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
767
774
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Document } from './Document.js';
|
|
2
2
|
import { LooseObject } from '../types/general.types.js';
|
|
3
|
-
import { ComponentEntry } from "../types/component.types.js";
|
|
3
|
+
import { ComponentEntry, ComponentEvents } from "../types/component.types.js";
|
|
4
4
|
import { DOMNode } from './dom/DOMNode.js';
|
|
5
5
|
import { EventEmitter } from '../EventEmitter.js';
|
|
6
|
-
export declare class Component<Events extends Record<string, any> = {
|
|
7
|
-
'componentCreated': Component;
|
|
8
|
-
}> extends EventEmitter<Events> {
|
|
6
|
+
export declare class Component<Events extends Record<string, any> = ComponentEvents> extends EventEmitter<Events> {
|
|
9
7
|
id: string;
|
|
10
8
|
name: string;
|
|
11
9
|
document: Document;
|
|
@@ -12,6 +12,7 @@ export class Component extends EventEmitter {
|
|
|
12
12
|
this.data = {};
|
|
13
13
|
const isDocument = this instanceof Document;
|
|
14
14
|
this.name = name;
|
|
15
|
+
this.emitterReady();
|
|
15
16
|
if (name === 'root') {
|
|
16
17
|
this.dom = new DOMFragment();
|
|
17
18
|
this.path.push('');
|
|
@@ -133,9 +134,16 @@ export class Component extends EventEmitter {
|
|
|
133
134
|
if (this.isRoot) {
|
|
134
135
|
const dataIf = this.dom.queryByHasAttribute('data-if');
|
|
135
136
|
for (let i = 0; i < dataIf.length; i++) {
|
|
136
|
-
dataIf[i].
|
|
137
|
+
const className = dataIf[i].getAttribute('class');
|
|
138
|
+
if (typeof className === 'string') {
|
|
139
|
+
dataIf[i].setAttribute('class', `${className} structured-hidden`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
dataIf[i].setAttribute('class', 'structured-hidden');
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
}
|
|
146
|
+
await this.emit('ready');
|
|
139
147
|
}
|
|
140
148
|
setAttributes(attributes, prefix = '', encode = true) {
|
|
141
149
|
if (typeof attributes === 'object' && attributes !== null) {
|
|
@@ -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>;
|
|
@@ -19,6 +19,13 @@ export class Document extends Component {
|
|
|
19
19
|
this.document = this;
|
|
20
20
|
this.head = new DocumentHead(title);
|
|
21
21
|
this.head.addJS('/assets/client-js/client/Client.js', 0, { type: 'module' });
|
|
22
|
+
this.head.add(`
|
|
23
|
+
<style>
|
|
24
|
+
.structured-hidden {
|
|
25
|
+
display: none !important;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
28
|
+
`);
|
|
22
29
|
this.application.emit('documentCreated', this);
|
|
23
30
|
}
|
|
24
31
|
push(response) {
|
|
@@ -29,7 +29,13 @@ export class Layout {
|
|
|
29
29
|
await component.init(`<${componentName}></${componentName}>`, data);
|
|
30
30
|
const conditionals = component.dom.queryByHasAttribute('data-if');
|
|
31
31
|
for (let i = 0; i < conditionals.length; i++) {
|
|
32
|
-
conditionals[i].
|
|
32
|
+
const className = conditionals[i].getAttribute('class');
|
|
33
|
+
if (typeof className === 'string') {
|
|
34
|
+
conditionals[i].setAttribute('class', `${className} structured-hidden`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
conditionals[i].setAttribute('class', 'structured-hidden');
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
return doc;
|
|
35
41
|
}
|
|
@@ -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,12 @@ 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
|
+
response.statusCode = 404;
|
|
135
|
+
const res = await this.pageNotFoundCallback.apply(this.app, [context]);
|
|
136
|
+
if (res instanceof Document) {
|
|
137
|
+
context.respondWith(res);
|
|
138
|
+
}
|
|
135
139
|
}
|
|
136
140
|
};
|
|
137
141
|
if (handler !== null) {
|
|
@@ -25,6 +25,7 @@ export declare class DOMNode {
|
|
|
25
25
|
potentialComponentChildren: Array<DOMNode>;
|
|
26
26
|
constructor(root: DOMFragment | null, parentNode: DOMNode | null, tagName: string);
|
|
27
27
|
appendChild(node: DOMNode | string): void;
|
|
28
|
+
getAttribute(attrName: string): string | true | null;
|
|
28
29
|
setAttribute(attributeName: string, attributeValue: string | true): void;
|
|
29
30
|
hasAttribute(attributeName: string): boolean;
|
|
30
31
|
queryByTagName(...tagNames: Array<string>): Array<DOMNode>;
|
|
@@ -30,6 +30,12 @@ export class DOMNode {
|
|
|
30
30
|
}
|
|
31
31
|
this.children.push(node);
|
|
32
32
|
}
|
|
33
|
+
getAttribute(attrName) {
|
|
34
|
+
if (!this.hasAttribute(attrName)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return this.attributeMap[attrName].value;
|
|
38
|
+
}
|
|
33
39
|
setAttribute(attributeName, attributeValue) {
|
|
34
40
|
const attributeExisting = this.attributeMap[attributeName];
|
|
35
41
|
if (!attributeExisting) {
|
|
@@ -36,6 +36,10 @@ export interface ComponentScaffold<T extends LooseObject = LooseObject, K extend
|
|
|
36
36
|
getData: (this: ComponentScaffold, data: LooseObject, ctx: undefined | RequestContext, app: Application, component: Component) => Promise<T | void>;
|
|
37
37
|
[key: string]: any;
|
|
38
38
|
}
|
|
39
|
+
export type ComponentEvents = {
|
|
40
|
+
componentCreated: Component;
|
|
41
|
+
ready: undefined;
|
|
42
|
+
};
|
|
39
43
|
export type ClientComponentTransition = {
|
|
40
44
|
fade: false | number;
|
|
41
45
|
slide: false | number;
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "build/index",
|
|
22
|
-
"version": "1.1
|
|
22
|
+
"version": "1.2.1",
|
|
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",
|