structured-fw 0.8.43 → 0.8.71
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 +84 -11
- package/build/system/bin/structured.js +2 -2
- package/build/system/client/Net.d.ts +1 -0
- package/build/system/client/Net.js +7 -6
- package/build/system/server/Application.d.ts +3 -1
- package/build/system/server/Application.js +11 -0
- package/build/system/server/Components.d.ts +1 -1
- package/build/system/server/DocumentHead.d.ts +1 -1
- package/build/system/server/FormValidation.d.ts +1 -1
- package/build/system/server/Layout.d.ts +10 -0
- package/build/system/server/Layout.js +30 -0
- package/index.ts +10 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -5,9 +5,6 @@ Framework allows the developer to develop self-contained components which are re
|
|
|
5
5
|
|
|
6
6
|
It works with Node.js and Deno runtimes. Other runtimes are not tested.
|
|
7
7
|
|
|
8
|
-
> [!NOTE]
|
|
9
|
-
> While Structured framework is in development for a couple of years and is production tested, the npm package is introduced recently and there were still issues that came up with it in the past few versions. Since version 0.8.3 all should be functional. Please update to latest version and check for updates regularly until the npm package is as stable as the framework itself. Feel free to open issues on the github page if you have any issues with the npm package or the framework. _Structured followed versioning x.y.z where z was a single digit 0-9, but since there were a couple of versions that introduced no changes to the framework and were just npm package tweaks, I decided to make an exception to the rule and allow myself to use 2 digits for such updates._
|
|
10
|
-
|
|
11
8
|
- [Why Structured](#why-structured)
|
|
12
9
|
- [Audience](#audience)
|
|
13
10
|
- [Getting started](#getting-started)
|
|
@@ -41,6 +38,17 @@ npm install @types/node
|
|
|
41
38
|
### Create boilerplate
|
|
42
39
|
`npx structured init`
|
|
43
40
|
|
|
41
|
+
### Create a test route
|
|
42
|
+
Create a file `/app/routes/Test.ts`:
|
|
43
|
+
```
|
|
44
|
+
import { Application } from 'structured-fw/Application';
|
|
45
|
+
export default function(app: Application) {
|
|
46
|
+
app.request.on('GET', '/test', async()=> {
|
|
47
|
+
return 'Hello, World!';
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
44
52
|
### Compile
|
|
45
53
|
`tsc`\
|
|
46
54
|
This will create a directory `build` (or whatever you have in tsconfig.json as compilerOptions.outputDir)
|
|
@@ -57,6 +65,8 @@ cd build
|
|
|
57
65
|
pm2 start index.js --name="[appName]"
|
|
58
66
|
```
|
|
59
67
|
|
|
68
|
+
If you followed the above steps, you should be able to access `http://localhost:9191/test` in your browser and see the output `Hello, World!`.
|
|
69
|
+
|
|
60
70
|
# Key concepts
|
|
61
71
|
|
|
62
72
|
## Application
|
|
@@ -418,7 +428,7 @@ That was the simplest possible example, let's make it more interesting by adding
|
|
|
418
428
|
### Component server-side code
|
|
419
429
|
Create a new file `/app/views/HelloWorld/HelloWorld.ts` (server side component code):
|
|
420
430
|
```
|
|
421
|
-
import { ComponentScaffold } from '
|
|
431
|
+
import { ComponentScaffold } from 'structured-fw/Types';
|
|
422
432
|
export default class HelloWorld implements ComponentScaffold {
|
|
423
433
|
async getData(): Promise<{
|
|
424
434
|
luckyNumber: number
|
|
@@ -460,7 +470,7 @@ Let's make it even more interesting by adding some client side code to it.
|
|
|
460
470
|
### Component client-side code
|
|
461
471
|
Create `/app/views/HelloWorld/HelloWorld.client.ts`:
|
|
462
472
|
```
|
|
463
|
-
import { InitializerFunction } from '
|
|
473
|
+
import { InitializerFunction } from 'structured-fw/Types';
|
|
464
474
|
export const init: InitializerFunction = async function() {
|
|
465
475
|
const generateNew = this.ref<HTMLButtonElement>('newNumber');
|
|
466
476
|
|
|
@@ -509,7 +519,7 @@ Parent says your lucky number is {{number}}.
|
|
|
509
519
|
That's it. Since `AnotherComponent` has no server side code, all data passed to it is exported to HTML, hence the `number` you passed from `HelloWorld` will be readily available for use. If AnotherComponent had a server side part, the process is a bit different, it will receive it as part of the `data`, but can choose whether to make it available to the HTML, or just make use of it and return other stuff. Let's see how that works.
|
|
510
520
|
Create `/app/views/AnotherComponent/AnotherComponent.ts`:
|
|
511
521
|
```
|
|
512
|
-
import { ComponentScaffold } from '
|
|
522
|
+
import { ComponentScaffold } from 'structured-fw/Types';
|
|
513
523
|
export default class AnotherComponent implements ComponentScaffold {
|
|
514
524
|
async getData(data: { number: number }): Promise<{
|
|
515
525
|
parentSuggests: number,
|
|
@@ -542,7 +552,7 @@ What about client side? **By default, data returned by server side code is not a
|
|
|
542
552
|
|
|
543
553
|
Let's create a client side code for `AnotherComponent` and export the `betterNumber` to it, create `/app/views/AnotherComponent/AnotherComponent.client.ts`:
|
|
544
554
|
```
|
|
545
|
-
import { InitializerFunction } from '
|
|
555
|
+
import { InitializerFunction } from 'structured-fw/Types';
|
|
546
556
|
export const init: InitializerFunction = async function() {
|
|
547
557
|
const betterNumber = this.getData<number>('betterNumber');
|
|
548
558
|
|
|
@@ -552,7 +562,7 @@ export const init: InitializerFunction = async function() {
|
|
|
552
562
|
|
|
553
563
|
And let's update `AnotherComponent.ts` to export `betterNumber`:
|
|
554
564
|
```
|
|
555
|
-
import { ComponentScaffold } from '
|
|
565
|
+
import { ComponentScaffold } from 'structured-fw/Types';
|
|
556
566
|
export default class AnotherComponent implements ComponentScaffold {
|
|
557
567
|
exportFields = ['betterNumber'];
|
|
558
568
|
async getData(data: { number: number }): Promise<{
|
|
@@ -574,7 +584,7 @@ This concept is wrong to start with, if we want a component to be independent, i
|
|
|
574
584
|
|
|
575
585
|
Let's say we wanted to access the `parent` Component from `AnotherComponent`:
|
|
576
586
|
```
|
|
577
|
-
import { InitializerFunction } from '
|
|
587
|
+
import { InitializerFunction } from 'structured-fw/Types';
|
|
578
588
|
export const init: InitializerFunction = async function() {
|
|
579
589
|
const betterNumber = this.getData<number>('betterNumber');
|
|
580
590
|
|
|
@@ -585,7 +595,7 @@ Here we accessed the `parent` and obtained it's `name`.
|
|
|
585
595
|
|
|
586
596
|
*"But we did not send any data to the parent here"* - correct, we did not, and we won't, instead we can inform them we have some data available, or that an event they might be interested in has occurred, and if they care, so be it:
|
|
587
597
|
```
|
|
588
|
-
import { InitializerFunction } from '
|
|
598
|
+
import { InitializerFunction } from 'structured-fw/Types';
|
|
589
599
|
export const init: InitializerFunction = async function() {
|
|
590
600
|
const betterNumber = this.getData<number>('betterNumber');
|
|
591
601
|
|
|
@@ -595,7 +605,7 @@ export const init: InitializerFunction = async function() {
|
|
|
595
605
|
|
|
596
606
|
We emitted an `event` with `eventName` = "`truth`" and a `payload`, which in this case is a string, but can be of any type. If the parent cares about it (or for that matter, not necessarily the parent, but anyone in the component tree), they can subscribe to that event. Let's subscribe to the event from `HelloWorld` (`HelloWorld.client.ts`):
|
|
597
607
|
```
|
|
598
|
-
import { InitializerFunction } from '
|
|
608
|
+
import { InitializerFunction } from 'structured-fw/Types';
|
|
599
609
|
export const init: InitializerFunction = async function() {
|
|
600
610
|
|
|
601
611
|
const child = this.find('AnotherComponent'); // ClientComponent | null
|
|
@@ -678,6 +688,69 @@ then in ComponentName.html:
|
|
|
678
688
|
<div data-if="showDiv()"></div>
|
|
679
689
|
```
|
|
680
690
|
|
|
691
|
+
### Layout
|
|
692
|
+
Prior to version 0.8.7:
|
|
693
|
+
|
|
694
|
+
1) `/app/views/layout.html`
|
|
695
|
+
```
|
|
696
|
+
...
|
|
697
|
+
{{{layoutComponent component data attributes}}}
|
|
698
|
+
...
|
|
699
|
+
```
|
|
700
|
+
2) `/app/routes/Test.ts`
|
|
701
|
+
```
|
|
702
|
+
import Document from 'structured-fw/Document';
|
|
703
|
+
|
|
704
|
+
app.request.on('GET', '/test', async (ctx) => {
|
|
705
|
+
const doc = new Document(app, 'Title', ctx);
|
|
706
|
+
await doc.loadComponent('layout', {
|
|
707
|
+
component: 'ComponentName',
|
|
708
|
+
data: {
|
|
709
|
+
something: 123
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
return doc;
|
|
713
|
+
});
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Version 0.8.7 introduced the `Layout` class, which allows accomplishing the above in a nicer way:
|
|
717
|
+
1) `/app/views/layout.html`
|
|
718
|
+
```
|
|
719
|
+
...
|
|
720
|
+
<template></template>
|
|
721
|
+
...
|
|
722
|
+
```
|
|
723
|
+
2) `/index.ts` (`app` is an instance of `Application`)
|
|
724
|
+
```
|
|
725
|
+
export const layout = new Layout(app, 'layout');
|
|
726
|
+
```
|
|
727
|
+
3) `/app/routes/Test.ts`
|
|
728
|
+
```
|
|
729
|
+
import { layout } from '../../index.js';
|
|
730
|
+
|
|
731
|
+
app.request.on('GET', '/test', async (ctx) => {
|
|
732
|
+
return await layout.document(ctx, 'Test', 'Conditionals', {
|
|
733
|
+
something: 123
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
While with the new approach there is an extra step where we create the instance(s) of `Layout`, it makes the route/template code cleaner (you will create your layout instance(s) only once, while you will likely use it in many routes, so adding an extra step is worth it).
|
|
740
|
+
|
|
741
|
+
```
|
|
742
|
+
Layout.document(
|
|
743
|
+
ctx: RequestContext,
|
|
744
|
+
title: string,
|
|
745
|
+
componentName: string,
|
|
746
|
+
data?: LooseObject
|
|
747
|
+
): Promise<Document>
|
|
748
|
+
```
|
|
749
|
+
`Layout.document` the only method of Layout you will use, it creates an instance of Document, loads template component (provided as second argument to Layout constructor) into it and loads `componentName` component in place of `<template></template>` found within your template.
|
|
750
|
+
|
|
751
|
+
> [!TIP]
|
|
752
|
+
> You will often want to use a few different layouts in your web application. You can achieve that by creating and exporting multiple instances of Layout and use the appropriate one where you need it.
|
|
753
|
+
|
|
681
754
|
**Basic animation/transitions**\
|
|
682
755
|
If you use conditionals on any DOM node, you may also enable basic animations/transitions using following attributes:
|
|
683
756
|
- Enable transition:
|
|
@@ -61,9 +61,9 @@ function createTsconfig() {
|
|
|
61
61
|
"strictNullChecks": true,
|
|
62
62
|
"strictPropertyInitialization": true,
|
|
63
63
|
"strictBindCallApply": true,
|
|
64
|
-
"moduleResolution": "
|
|
64
|
+
"moduleResolution": "node16",
|
|
65
65
|
"outDir": "./build",
|
|
66
|
-
"module": "
|
|
66
|
+
"module": "Node16",
|
|
67
67
|
"target": "ES2021",
|
|
68
68
|
"allowSyntheticDefaultImports": true,
|
|
69
69
|
"preserveSymlinks": true,
|
|
@@ -4,6 +4,7 @@ export declare class Net {
|
|
|
4
4
|
request(method: RequestMethod, url: string, headers?: IncomingHttpHeaders, body?: any, responseType?: XMLHttpRequestResponseType): Promise<string>;
|
|
5
5
|
get(url: string, headers?: IncomingHttpHeaders): Promise<string>;
|
|
6
6
|
delete(url: string, headers?: IncomingHttpHeaders): Promise<string>;
|
|
7
|
+
private serializeData;
|
|
7
8
|
post(url: string, data: any, headers?: IncomingHttpHeaders): Promise<string>;
|
|
8
9
|
put(url: string, data: any, headers?: IncomingHttpHeaders): Promise<string>;
|
|
9
10
|
getJSON<T>(url: string, headers?: IncomingHttpHeaders): Promise<T>;
|
|
@@ -10,18 +10,19 @@ export class Net {
|
|
|
10
10
|
async delete(url, headers = {}) {
|
|
11
11
|
return this.request('DELETE', url, headers);
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
serializeData(data, headers) {
|
|
14
14
|
if (typeof data === 'object' && !headers['content-type'] && !(data instanceof FormData)) {
|
|
15
15
|
headers['content-type'] = 'application/json';
|
|
16
|
-
|
|
16
|
+
return JSON.stringify(data);
|
|
17
17
|
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
async post(url, data, headers = {}) {
|
|
21
|
+
data = this.serializeData(data, headers);
|
|
18
22
|
return await this.request('POST', url, headers, data);
|
|
19
23
|
}
|
|
20
24
|
async put(url, data, headers = {}) {
|
|
21
|
-
|
|
22
|
-
headers['content-type'] = 'application/json';
|
|
23
|
-
data = JSON.stringify(data);
|
|
24
|
-
}
|
|
25
|
+
data = this.serializeData(data, headers);
|
|
25
26
|
return this.request('PUT', url, headers, data);
|
|
26
27
|
}
|
|
27
28
|
async getJSON(url, headers) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from 'node:http';
|
|
2
|
-
import { ApplicationEvents, LooseObject, RequestContext, StructuredConfig } from '../Types';
|
|
2
|
+
import { ApplicationEvents, LooseObject, RequestContext, StructuredConfig } from '../Types.js';
|
|
3
3
|
import { Document } from './Document.js';
|
|
4
4
|
import { Components } from './Components.js';
|
|
5
5
|
import { Session } from './Session.js';
|
|
@@ -8,6 +8,7 @@ import { Handlebars } from './Handlebars.js';
|
|
|
8
8
|
import { Cookies } from './Cookies.js';
|
|
9
9
|
export declare class Application {
|
|
10
10
|
readonly config: StructuredConfig;
|
|
11
|
+
initialized: boolean;
|
|
11
12
|
server: null | Server;
|
|
12
13
|
listening: boolean;
|
|
13
14
|
private readonly eventEmitter;
|
|
@@ -25,6 +26,7 @@ export declare class Application {
|
|
|
25
26
|
importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
|
|
26
27
|
exportContextFields(...fields: Array<keyof RequestContextData>): void;
|
|
27
28
|
contentType(extension: string): string | false;
|
|
29
|
+
registerPlugin<Opt extends Readonly<LooseObject>>(callback: (app: Application, options: Opt) => void | Promise<void>, opts: NoInfer<Opt>): Promise<void>;
|
|
28
30
|
private respondWithComponent;
|
|
29
31
|
memoryUsage(): NodeJS.MemoryUsage;
|
|
30
32
|
printMemoryUsage(): void;
|
|
@@ -12,6 +12,7 @@ import { Handlebars } from './Handlebars.js';
|
|
|
12
12
|
import { Cookies } from './Cookies.js';
|
|
13
13
|
export class Application {
|
|
14
14
|
constructor(config) {
|
|
15
|
+
this.initialized = false;
|
|
15
16
|
this.server = null;
|
|
16
17
|
this.listening = false;
|
|
17
18
|
this.eventEmitter = new EventEmitter();
|
|
@@ -28,6 +29,9 @@ export class Application {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
async init() {
|
|
32
|
+
if (this.initialized) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
31
35
|
this.eventEmitter.setMaxListeners(10);
|
|
32
36
|
try {
|
|
33
37
|
await this.handlebars.loadHelpers('../Helpers.js');
|
|
@@ -63,6 +67,7 @@ export class Application {
|
|
|
63
67
|
return '';
|
|
64
68
|
}, this, true);
|
|
65
69
|
await this.start();
|
|
70
|
+
this.initialized = true;
|
|
66
71
|
}
|
|
67
72
|
start() {
|
|
68
73
|
return new Promise((resolve, reject) => {
|
|
@@ -136,6 +141,12 @@ export class Application {
|
|
|
136
141
|
contentType(extension) {
|
|
137
142
|
return mime.contentType(extension);
|
|
138
143
|
}
|
|
144
|
+
async registerPlugin(callback, opts) {
|
|
145
|
+
if (this.initialized) {
|
|
146
|
+
console.warn('Plugin registered after app is initialized, some plugin features may not work.');
|
|
147
|
+
}
|
|
148
|
+
await callback.apply(this, [this, opts]);
|
|
149
|
+
}
|
|
139
150
|
async respondWithComponent(ctx, componentName, attributes, unwrap = true) {
|
|
140
151
|
const component = this.components.getByName(componentName);
|
|
141
152
|
if (component) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormValidationEntry, PostedDataDecoded, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types';
|
|
1
|
+
import { FormValidationEntry, PostedDataDecoded, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types.js';
|
|
2
2
|
export declare class FormValidation {
|
|
3
3
|
fieldRules: Array<FormValidationEntry>;
|
|
4
4
|
singleError: boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LooseObject, RequestContext } from "../Types.js";
|
|
2
|
+
import { Application } from "./Application.js";
|
|
3
|
+
import { Document } from "./Document.js";
|
|
4
|
+
export declare class Layout {
|
|
5
|
+
layoutComponent: string;
|
|
6
|
+
app: Application;
|
|
7
|
+
constructor(app: Application, layoutComponent: string);
|
|
8
|
+
private layoutComponentExists;
|
|
9
|
+
document(ctx: RequestContext, title: string, componentName: string, data?: LooseObject): Promise<Document>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Component } from "./Component.js";
|
|
2
|
+
import { Document } from "./Document.js";
|
|
3
|
+
export class Layout {
|
|
4
|
+
constructor(app, layoutComponent) {
|
|
5
|
+
this.app = app;
|
|
6
|
+
this.layoutComponent = layoutComponent;
|
|
7
|
+
if (this.app.initialized) {
|
|
8
|
+
this.layoutComponentExists();
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
this.app.on('afterComponentsLoaded', () => { this.layoutComponentExists(); });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
layoutComponentExists() {
|
|
15
|
+
if (this.app.components.getByName(this.layoutComponent) === null) {
|
|
16
|
+
throw new Error(`Layout component "${this.layoutComponent}" not found`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async document(ctx, title, componentName, data) {
|
|
20
|
+
const doc = new Document(this.app, title, ctx);
|
|
21
|
+
await doc.loadComponent(this.layoutComponent, data);
|
|
22
|
+
const layoutComponent = doc.dom.queryByTagName('template');
|
|
23
|
+
if (layoutComponent.length === 0) {
|
|
24
|
+
throw new Error(`<template></template> not found in the layout component ${this.layoutComponent}`);
|
|
25
|
+
}
|
|
26
|
+
const component = new Component(componentName, layoutComponent[0], doc, false);
|
|
27
|
+
await component.init(`<${componentName}></${componentName}>`, data);
|
|
28
|
+
return doc;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { Application } from "structured-fw/Application";
|
|
2
2
|
import { config } from './Config.js';
|
|
3
|
+
import { Layout } from "./system/server/Layout.js";
|
|
3
4
|
|
|
4
5
|
const app = new Application(config);
|
|
5
6
|
|
|
7
|
+
app.registerPlugin(async (app) => {
|
|
8
|
+
|
|
9
|
+
}, {
|
|
10
|
+
poop: 123
|
|
11
|
+
});
|
|
12
|
+
|
|
6
13
|
// app.on('afterComponentsLoaded', (components) => {
|
|
7
14
|
// components.componentNames.forEach((componentName) => {
|
|
8
15
|
// console.log(componentName)
|
|
@@ -18,4 +25,6 @@ app.on('documentCreated', (document) => {
|
|
|
18
25
|
document.on('componentCreated', (component) => {
|
|
19
26
|
document.head.add(`<script>console.log('${component.name}')</script>`);
|
|
20
27
|
});
|
|
21
|
-
})
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const layout: Layout = new Layout(app, 'layout');
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "build/index",
|
|
17
|
-
"version": "0.8.
|
|
17
|
+
"version": "0.8.71",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"develop": "tsc --watch",
|
|
20
20
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"./Util": "./build/system/Util.js",
|
|
49
49
|
"./Application": "./build/system/server/Application.js",
|
|
50
50
|
"./Document": "./build/system/server/Document.js",
|
|
51
|
+
"./Component": "./build/system/server/Component.js",
|
|
52
|
+
"./Layout": "./build/system/server/Layout.js",
|
|
51
53
|
"./FormValidation": "./build/system/server/FormValidation.js",
|
|
52
54
|
"./ClientComponent": "./build/system/client/ClientComponent.js"
|
|
53
55
|
}
|