structured-fw 0.8.1 → 0.8.4
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 +169 -15
- package/build/index.js +6 -1
- package/build/system/EventEmitter.d.ts +7 -0
- package/build/system/EventEmitter.js +31 -0
- package/build/system/Types.d.ts +2 -2
- package/build/system/client/ClientComponent.d.ts +2 -8
- package/build/system/client/ClientComponent.js +1 -1
- package/build/system/server/Application.d.ts +1 -1
- package/build/system/server/Application.js +10 -8
- package/build/system/server/Component.d.ts +6 -3
- package/build/system/server/Component.js +12 -6
- package/build/system/server/Document.d.ts +3 -1
- package/index.ts +18 -1
- package/jsr.json +35 -0
- package/package.json +3 -2
- package/system/{client/EventEmitter.ts → EventEmitter.ts} +6 -6
- package/system/Types.ts +2 -2
- package/system/client/ClientComponent.ts +2 -12
- package/system/global.d.ts +12 -0
- package/system/server/Application.ts +11 -9
- package/system/server/Component.ts +18 -11
- package/system/server/Document.ts +1 -1
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -8,15 +8,20 @@ It works with Node.js and Deno runtimes. Other runtimes are not tested.
|
|
|
8
8
|
- [Why Structured](#why-structured)
|
|
9
9
|
- [Audience](#audience)
|
|
10
10
|
- [Getting started](#getting-started)
|
|
11
|
+
- [Key concepts](#key-concepts)
|
|
12
|
+
- [Good to know](#good-to-know)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
### Key concepts:
|
|
13
16
|
* [Application](#application)
|
|
14
17
|
* [Route](#route)
|
|
15
18
|
* [Document](#document)
|
|
16
|
-
* [
|
|
19
|
+
* [Component](#component)
|
|
17
20
|
|
|
18
21
|
## Getting started
|
|
19
22
|
|
|
23
|
+
_Following getting started instructions are relevant for Node.js runtime, if you are using Deno skip to [runtimes](#runtimes) section._
|
|
24
|
+
|
|
20
25
|
### Initialize a Node.js project
|
|
21
26
|
```
|
|
22
27
|
cd /path/to/project
|
|
@@ -105,13 +110,13 @@ new Application(config);
|
|
|
105
110
|
|
|
106
111
|
### Methods
|
|
107
112
|
- `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
|
|
108
|
-
- `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `
|
|
109
|
-
- `serverStarted` - executed once the built-in http server is started and running. Callback receives
|
|
113
|
+
- `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `ApplicationEvents`:
|
|
114
|
+
- `serverStarted` - executed once the built-in http server is started and running. Callback receives Server (exported from node:http) instance as the first argument
|
|
110
115
|
- `beforeRequestHandler` - runs before any request handler (route) is executed. Callback receives `RequestContext` as the first argument. Useful for example to set `RequestContext.data: RequestContextData` (user defined data, to make it available to routes and components)
|
|
111
116
|
- `afterRequestHandler` - runs after any request handler (route) is executed. Callback receives `RequestContext` as the first argument
|
|
112
117
|
- `afterRoutes` - runs after all routes are loaded from `StructuredConfig.routes.path`. Callback receives no arguments
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
118
|
+
- `beforeComponentsLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
|
|
119
|
+
- `afterComponentsLoaded` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives instance of Components as the first argument
|
|
115
120
|
- `documentCreated` - runs whenever an instance of a [Document](#document) is created. Callback receives the Document instance as the first argument. You will often use this, for example if you want to include a CSS file to all pages `Document.head.addCSS(...)`
|
|
116
121
|
- `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
|
|
117
122
|
- `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
|
|
@@ -168,7 +173,7 @@ app.exportContextFields('user');
|
|
|
168
173
|
```
|
|
169
174
|
|
|
170
175
|
### Session
|
|
171
|
-
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
|
|
176
|
+
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`.
|
|
172
177
|
|
|
173
178
|
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 code of your components.
|
|
174
179
|
|
|
@@ -316,16 +321,16 @@ In some edge cases you may need more control of when a route is executed, in whi
|
|
|
316
321
|
> email: string,
|
|
317
322
|
> password: string,
|
|
318
323
|
> age: number
|
|
319
|
-
>}>('POST', '/users/create',
|
|
324
|
+
>}>('POST', '/users/create', async (ctx) => {
|
|
320
325
|
> ctx.body.email // string
|
|
321
326
|
> ctx.body.age // number
|
|
322
|
-
> const doc = new Document(
|
|
327
|
+
> const doc = new Document(app, 'User', ctx);
|
|
323
328
|
> return doc; // error if we return anything but Document
|
|
324
329
|
> });
|
|
325
330
|
> ```
|
|
326
331
|
|
|
327
332
|
## Document
|
|
328
|
-
Document does not differ much from a component, in fact, it extends Component. It has a more user-
|
|
333
|
+
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.
|
|
329
334
|
|
|
330
335
|
Creating a document:
|
|
331
336
|
`const doc = new Document(app, 'HelloWorld page', ctx);`
|
|
@@ -341,7 +346,7 @@ app.request.on('GET', '/home', async (ctx) => {
|
|
|
341
346
|
|
|
342
347
|
## Component
|
|
343
348
|
A component is comprised of 1-3 files. It always must include one HTML file, while server side and client side files are optional.
|
|
344
|
-
* HTML file
|
|
349
|
+
* HTML file probably requires no explanation
|
|
345
350
|
* server side file, code that runs on the server and makes data available to HTML and client side code
|
|
346
351
|
* client side file, code that runs on the client (in the browser)
|
|
347
352
|
|
|
@@ -362,7 +367,7 @@ It is recommended, but not necessary, that you contain each component in it's ow
|
|
|
362
367
|
\
|
|
363
368
|
**Component rules:**
|
|
364
369
|
- **Component names must be unique**
|
|
365
|
-
- Components HTML file can have a `.hbs` extension (which allows for better Handlebars
|
|
370
|
+
- Components HTML file can have a `.hbs` extension (which allows for better Handlebars syntax highlighting)
|
|
366
371
|
- Components can reside at any depth in the file structure
|
|
367
372
|
|
|
368
373
|
Let's create a HelloWorld Component `/app/views/HelloWorld/HelloWorld.html`:\
|
|
@@ -494,7 +499,7 @@ What we did is, we accepted the number provided by parent component, and returne
|
|
|
494
499
|
betterNumber: number
|
|
495
500
|
}
|
|
496
501
|
```
|
|
497
|
-
which is now
|
|
502
|
+
which is now available in `AnotherComponent` HTML, we assigned the received number to `parentSuggests`, while `betterNumber` is `parentSuggests + 5`, we now have these 2 available and ready to use in our HTML template.
|
|
498
503
|
|
|
499
504
|
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).
|
|
500
505
|
|
|
@@ -550,7 +555,7 @@ export const init: InitializerFunction = async function() {
|
|
|
550
555
|
```
|
|
551
556
|
Here we accessed the `parent` and obtained it's `name`.
|
|
552
557
|
|
|
553
|
-
*"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
|
|
558
|
+
*"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:
|
|
554
559
|
```
|
|
555
560
|
import { InitializerFunction } from 'system/Types.js';
|
|
556
561
|
export const init: InitializerFunction = async function() {
|
|
@@ -580,7 +585,7 @@ export const init: InitializerFunction = async function() {
|
|
|
580
585
|
}
|
|
581
586
|
```
|
|
582
587
|
|
|
583
|
-
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
|
|
588
|
+
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.
|
|
584
589
|
|
|
585
590
|
We have only scratched the surface of what client-side code 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`.
|
|
586
591
|
|
|
@@ -600,11 +605,160 @@ Methods:
|
|
|
600
605
|
- `store.set(key: string, value: any)` - set data in client side data store
|
|
601
606
|
- `find(componentName: string, recursive: boolean = true): ClientComponent | null` - find a child component
|
|
602
607
|
- `findParent(componentName: string): ClientComponent | null` - find the first parent with given name
|
|
603
|
-
- `query(componentName: string, recursive: boolean = true): Array<ClientComponent>` - return all components with given name found within this component, if `
|
|
608
|
+
- `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
|
|
604
609
|
- `ref<T>(refName: string): T` - get a HTMLElement or ClientComponent that has attribute `ref="[refName]"`
|
|
605
610
|
- `arrayRef<T>(refName: string): Array<T>` - get an array of HTMLElement or ClientComponent that have attribute `array:ref="[refName]"`
|
|
606
611
|
- `add(appendTo: HTMLElement, componentName: string, data?: LooseObject)` - add `componentName` component to `appendTo` element, optionally passing `data` to the component when it's being rendered
|
|
607
612
|
|
|
613
|
+
## Good to know
|
|
614
|
+
- [Using CSS frameworks](#css-frameworks)
|
|
615
|
+
- [Using JS runtimes other than Node.js](#runtimes)
|
|
616
|
+
- [Why not JSR](#jsr)
|
|
617
|
+
- [Best practices](#best-practices)
|
|
618
|
+
|
|
619
|
+
### CSS frameworks
|
|
620
|
+
We rarely write all CSS from scratch, usually we use a CSS framework to speed us up. Structured allows you to work with any CSS frameworks such as Tailwind, PostCSS or Bootstrap.
|
|
621
|
+
|
|
622
|
+
Your Tailwind configuration may look something like:
|
|
623
|
+
```
|
|
624
|
+
/** @type {import('tailwindcss').Config} */
|
|
625
|
+
module.exports = {
|
|
626
|
+
content: ["./app/views/**/*.html", "./app/views/**/*.hbs"],
|
|
627
|
+
...
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Above we just defined where all our HTML resides, which is within /app/views. That is all there is to it. From there, you can generate the CSS, for example:\
|
|
632
|
+
`npx tailwindcss -i ./assets/css/src/style.css -o ./assets/css/dist.css`
|
|
633
|
+
|
|
634
|
+
**Including the output CSS**\
|
|
635
|
+
To include the output CSS in all pages, you can add the following to `index.ts`:
|
|
636
|
+
```
|
|
637
|
+
const app = new Application(config);
|
|
638
|
+
|
|
639
|
+
app.on('documentCreated', (doc) => {
|
|
640
|
+
doc.head.addCSS('/assets/css/dist.css');
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Runtimes
|
|
645
|
+
Structured is tested with Node.js and Deno. Other runtimes would likely work as well.
|
|
646
|
+
|
|
647
|
+
To use Structured with Deno, you can:
|
|
648
|
+
```
|
|
649
|
+
cd /path/to/project
|
|
650
|
+
deno init
|
|
651
|
+
deno add npm:structured-fw
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
With Deno, we can't use the cli to create the boilerplate, so you will need to create it yourself.
|
|
655
|
+
```
|
|
656
|
+
mkdir app
|
|
657
|
+
mkdir app/views
|
|
658
|
+
mkdir app/routes
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Create `Config.ts`:
|
|
662
|
+
```
|
|
663
|
+
import { StructuredConfig } from "structured-fw/Types";
|
|
664
|
+
|
|
665
|
+
export const config: StructuredConfig = {
|
|
666
|
+
// Application.importEnv will load all env variables starting with [envPrefix]_
|
|
667
|
+
envPrefix: 'STRUCTURED',
|
|
668
|
+
|
|
669
|
+
// whether to call Application.init when an instance of Application is created
|
|
670
|
+
autoInit: true,
|
|
671
|
+
|
|
672
|
+
url: {
|
|
673
|
+
removeTrailingSlash: true,
|
|
674
|
+
|
|
675
|
+
// if you want to enable individual component rendering set this to URI (string)
|
|
676
|
+
// to disable component rendering set it to false
|
|
677
|
+
// setting this to false disallows the use of ClientComponent.redraw and ClientComponent.add
|
|
678
|
+
componentRender: '/componentRender',
|
|
679
|
+
|
|
680
|
+
// function that receives the requested URL and returns boolean, if true, treat as static asset
|
|
681
|
+
// if there is a registered request handler that matches this same URL, it takes precedence over this
|
|
682
|
+
isAsset: function(uri: string) {
|
|
683
|
+
return uri.indexOf('/assets/') === 0;
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
routes: {
|
|
687
|
+
path: '/app/routes'
|
|
688
|
+
},
|
|
689
|
+
components : {
|
|
690
|
+
// relative to index.ts
|
|
691
|
+
path: '/app/views',
|
|
692
|
+
|
|
693
|
+
componentNameAttribute: 'structured-component'
|
|
694
|
+
},
|
|
695
|
+
session: {
|
|
696
|
+
cookieName: 'session',
|
|
697
|
+
keyLength: 24,
|
|
698
|
+
durationSeconds: 60 * 60,
|
|
699
|
+
garbageCollectIntervalSeconds: 60
|
|
700
|
+
},
|
|
701
|
+
http: {
|
|
702
|
+
port: 9191,
|
|
703
|
+
host: '0.0.0.0',
|
|
704
|
+
// used by Document.push, can be preload or preconnect
|
|
705
|
+
linkHeaderRel : 'preload'
|
|
706
|
+
},
|
|
707
|
+
runtime: 'Deno'
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Import `Config.ts` in `main.ts` and create the Application instance:
|
|
712
|
+
```
|
|
713
|
+
import { Application } from 'structured-fw/Application';
|
|
714
|
+
import { config } from './Config.ts';
|
|
715
|
+
|
|
716
|
+
new Application(config);
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Run application using `deno main.ts`
|
|
720
|
+
|
|
721
|
+
### JSR
|
|
722
|
+
It would make a lot of sense to have Structured hosted on JSR (JavaScript Registry) given Structured is a TypeScript framework, and JSR is a TypeScript-first registry, however, the issue is that Deno imposes [limitations with dynamic imports](https://docs.deno.com/deploy/api/dynamic-import/) with JSR-imported dependencies, which are required for the framework (to dynamically import your routes and components).\
|
|
723
|
+
This does not stop the framework from working with Deno, but for the time being, we have to stick with good old npm.
|
|
724
|
+
|
|
725
|
+
### Best practices
|
|
726
|
+
|
|
727
|
+
**Entry point:**\
|
|
728
|
+
I suggest the following setup for your entry point:
|
|
729
|
+
1) Set `autoInit = false` in your `/Config.ts`
|
|
730
|
+
2) If you are using ENV variables, define a type `EnvConf` in `/app/Types.ts`
|
|
731
|
+
3) In `/index.ts`, only create the Application instance and import ENV using `importEnv`, exporting both, as follows:
|
|
732
|
+
```
|
|
733
|
+
import { EnvConf } from './app/Types.js';
|
|
734
|
+
import { Application } from 'structured-fw/Application';
|
|
735
|
+
import { config } from './Config.js';
|
|
736
|
+
|
|
737
|
+
export const app = new Application(config);
|
|
738
|
+
export const env = app.importEnv<EnvConf>();
|
|
739
|
+
```
|
|
740
|
+
4) Create `/main.ts` and import `app` and `env` from `/index.ts`, add `main.ts` to tsconfig.json include array, add any event listeners, and load helpers from within `/main.ts`. This makes sure you can use env in any imported modules in main.ts without having to use dynamic imports. You can later import `env` and `app` from `index.ts` wherever you want to use them.
|
|
741
|
+
|
|
742
|
+
\
|
|
743
|
+
**Component directories**\
|
|
744
|
+
You should always place your components in a directory named same as your component. While this is not required, it will keep things organized. You might think your component will only have a HTML part, but at some point you may decide you want to add client/server code to it, so it's better to start out with a directory.\
|
|
745
|
+
Feel free to group your components in directories and subdirectories. Structured loads all components recursively when Application is initialized, and allows you to load any existing component from any component/Document. You can even move your components to other directory later without having to worry about updating the imports.
|
|
746
|
+
|
|
747
|
+
**Type definitions**\
|
|
748
|
+
I suggest keeping your general type definitions in /app/Types.ts, but for more specific types you should probably create /app/types/[entity].types.ts to keep things clean easy to maintain.\
|
|
749
|
+
For example:\
|
|
750
|
+
`export type BooleanInt = 0 | 1;` - this is fine
|
|
751
|
+
in /app/Types.ts\
|
|
752
|
+
`export type User = {email: string, password: string}` - you should probably create /app/types/users.types.ts for this one
|
|
753
|
+
|
|
754
|
+
**Models**\
|
|
755
|
+
If you ran `npx structured init`, it has created /app/models for you. Structured does not use this directory, but I suggest keeping your models interfacing the DB/APIs there. While Structured framework is not an MVC in a traditional sense, it's a good idea to keep your models in one place, as you will want to import the same model from many routes and components.
|
|
756
|
+
|
|
757
|
+
> [!IMPORTANT]
|
|
758
|
+
> while it's true that with Structured, components take care of their own data, it does not mean that they need to contain the code to fetch said data, instead you are encouraged to keep data logic in your models, and use those models in components/routes.
|
|
759
|
+
|
|
760
|
+
You can create additional code separation, for example, it would make sense to have /app/lib for code that interfaces an API, or have /app/Util.ts where you export utility functions. Structured boilerplate does not include these as not all applications will need them.
|
|
761
|
+
|
|
608
762
|
## Why Structured
|
|
609
763
|
Framework was developed by someone who has been a web developer for almost 20 years (me), and did not like the path web development has taken.
|
|
610
764
|
\
|
package/build/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { Application } from "structured-fw/Application";
|
|
2
2
|
import { config } from './Config.js';
|
|
3
|
-
new Application(config);
|
|
3
|
+
const app = new Application(config);
|
|
4
|
+
app.on('documentCreated', (document) => {
|
|
5
|
+
document.on('componentCreated', (component) => {
|
|
6
|
+
document.head.add(`<script>console.log('${component.name}')</script>`);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EventEmitterCallback } from "./Types.js";
|
|
2
|
+
export declare class EventEmitter<T extends Record<string, any> = Record<string, any>> {
|
|
3
|
+
protected listeners: Partial<Record<keyof T, Array<EventEmitterCallback<any>>>>;
|
|
4
|
+
on<K extends keyof T>(eventName: K, callback: EventEmitterCallback<T[K]>): void;
|
|
5
|
+
emit(eventName: keyof T, payload?: any): void;
|
|
6
|
+
unbind(eventName: keyof T, callback: EventEmitterCallback<any>): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class EventEmitter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = {};
|
|
4
|
+
}
|
|
5
|
+
on(eventName, callback) {
|
|
6
|
+
if (!Array.isArray(this.listeners[eventName])) {
|
|
7
|
+
this.listeners[eventName] = [];
|
|
8
|
+
}
|
|
9
|
+
this.listeners[eventName].push(callback);
|
|
10
|
+
}
|
|
11
|
+
emit(eventName, payload) {
|
|
12
|
+
if (Array.isArray(this.listeners[eventName])) {
|
|
13
|
+
this.listeners[eventName].forEach((callback) => {
|
|
14
|
+
callback(payload);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
unbind(eventName, callback) {
|
|
19
|
+
if (Array.isArray(this.listeners[eventName])) {
|
|
20
|
+
while (true) {
|
|
21
|
+
const index = this.listeners[eventName].indexOf(callback);
|
|
22
|
+
if (index > -1) {
|
|
23
|
+
this.listeners[eventName].splice(index, 1);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/build/system/Types.d.ts
CHANGED
|
@@ -127,7 +127,7 @@ export interface ComponentScaffold {
|
|
|
127
127
|
[key: string]: any;
|
|
128
128
|
}
|
|
129
129
|
export type LooseObject = Record<string, any>;
|
|
130
|
-
export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeRoutes' | 'afterRoutes' | '
|
|
130
|
+
export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
|
|
131
131
|
export type SessionEntry = {
|
|
132
132
|
sessionId: string;
|
|
133
133
|
lastRequest: number;
|
|
@@ -166,4 +166,4 @@ export type ClientComponentTransition = {
|
|
|
166
166
|
};
|
|
167
167
|
export type ClientComponentTransitionEvent = 'show' | 'hide';
|
|
168
168
|
export type ClientComponentTransitions = Record<ClientComponentTransitionEvent, ClientComponentTransition>;
|
|
169
|
-
export type EventEmitterCallback = (payload:
|
|
169
|
+
export type EventEmitterCallback<T> = (payload: T) => void;
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LooseObject } from '../Types.js';
|
|
2
2
|
import { DataStoreView } from './DataStoreView.js';
|
|
3
3
|
import { DataStore } from './DataStore.js';
|
|
4
4
|
import { Net } from './Net.js';
|
|
5
|
-
import { EventEmitter } from '
|
|
6
|
-
declare global {
|
|
7
|
-
interface Window {
|
|
8
|
-
initializers: Record<string, InitializerFunction | string>;
|
|
9
|
-
structuredClientConfig: StructuredClientConfig;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
5
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
12
6
|
export declare class ClientComponent extends EventEmitter {
|
|
13
7
|
readonly name: string;
|
|
14
8
|
children: Array<ClientComponent>;
|
|
@@ -2,7 +2,7 @@ import { attributeValueFromString, attributeValueToString, mergeDeep, objectEach
|
|
|
2
2
|
import { DataStoreView } from './DataStoreView.js';
|
|
3
3
|
import { Net } from './Net.js';
|
|
4
4
|
import { NetRequest } from './NetRequest.js';
|
|
5
|
-
import { EventEmitter } from '
|
|
5
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
6
6
|
export class ClientComponent extends EventEmitter {
|
|
7
7
|
constructor(parent, name, domNode, store, runInitializer = true) {
|
|
8
8
|
super();
|
|
@@ -20,7 +20,7 @@ export declare class Application {
|
|
|
20
20
|
constructor(config: StructuredConfig);
|
|
21
21
|
init(): Promise<void>;
|
|
22
22
|
private start;
|
|
23
|
-
on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext : E extends 'documentCreated' ? Document : undefined) => void): void;
|
|
23
|
+
on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext : E extends 'documentCreated' ? Document : E extends 'afterComponentsLoaded' ? Components : E extends 'serverStarted' ? Server : undefined) => void): void;
|
|
24
24
|
emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>>;
|
|
25
25
|
importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
|
|
26
26
|
exportContextFields(...fields: Array<keyof RequestContextData>): void;
|
|
@@ -35,12 +35,12 @@ export class Application {
|
|
|
35
35
|
catch (e) {
|
|
36
36
|
console.error(e.message);
|
|
37
37
|
}
|
|
38
|
-
await this.emit('
|
|
38
|
+
await this.emit('beforeComponentsLoad');
|
|
39
39
|
this.components.loadComponents();
|
|
40
|
-
await this.emit('
|
|
40
|
+
await this.emit('afterComponentsLoaded', this.components);
|
|
41
41
|
await this.emit('beforeRoutes');
|
|
42
42
|
await this.request.loadHandlers();
|
|
43
|
-
await this.emit('afterRoutes');
|
|
43
|
+
await this.emit('afterRoutes', this.request);
|
|
44
44
|
if (this.config.url.componentRender !== false) {
|
|
45
45
|
this.request.on('POST', `${this.config.url.componentRender}`, async (ctx) => {
|
|
46
46
|
const input = ctx.body;
|
|
@@ -49,16 +49,18 @@ export class Application {
|
|
|
49
49
|
}
|
|
50
50
|
this.request.on('GET', /^\/assets\/client-js/, async ({ request, response }) => {
|
|
51
51
|
const uri = request.url?.substring(18);
|
|
52
|
-
|
|
52
|
+
if (uri.includes('..')) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
const filePath = path.resolve(import.meta.dirname, '..', uri);
|
|
53
56
|
if (existsSync(filePath)) {
|
|
54
57
|
response.setHeader('Content-Type', 'application/javascript');
|
|
55
|
-
|
|
56
|
-
response.end();
|
|
58
|
+
return readFileSync(filePath);
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
59
61
|
response.statusCode = 404;
|
|
60
62
|
}
|
|
61
|
-
return;
|
|
63
|
+
return '';
|
|
62
64
|
}, this, true);
|
|
63
65
|
await this.start();
|
|
64
66
|
}
|
|
@@ -69,7 +71,7 @@ export class Application {
|
|
|
69
71
|
});
|
|
70
72
|
this.server.listen(this.config.http.port, this.config.http.host || '127.0.0.1', async () => {
|
|
71
73
|
const address = (this.config.http.host !== undefined ? this.config.http.host : '') + ':' + this.config.http.port;
|
|
72
|
-
await this.emit('serverStarted');
|
|
74
|
+
await this.emit('serverStarted', this.server);
|
|
73
75
|
console.log(`Server started on ${address}`);
|
|
74
76
|
resolve();
|
|
75
77
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Document } from './Document.js';
|
|
2
2
|
import { ComponentEntry, LooseObject } from '../Types.js';
|
|
3
3
|
import { DOMNode } from './dom/DOMNode.js';
|
|
4
|
-
|
|
4
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
5
|
+
export declare class Component<Events extends Record<string, any> = {
|
|
6
|
+
'componentCreated': Component;
|
|
7
|
+
}> extends EventEmitter<Events> {
|
|
5
8
|
id: string;
|
|
6
9
|
name: string;
|
|
7
10
|
document: Document;
|
|
@@ -20,8 +23,8 @@ export declare class Component {
|
|
|
20
23
|
private initChildren;
|
|
21
24
|
protected importedParentData(parentData: LooseObject): LooseObject;
|
|
22
25
|
protected initAttributesData(domNode?: DOMNode): void;
|
|
23
|
-
private
|
|
26
|
+
private attributePrefix;
|
|
24
27
|
private attributeDataType;
|
|
25
|
-
private
|
|
28
|
+
private attributeUnprefixed;
|
|
26
29
|
protected fillData(data: LooseObject): void;
|
|
27
30
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Document } from './Document.js';
|
|
2
2
|
import { attributeValueFromString, attributeValueToString, objectEach, toCamelCase } from '../Util.js';
|
|
3
3
|
import { DOMFragment } from './dom/DOMFragment.js';
|
|
4
|
-
|
|
4
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
5
|
+
export class Component extends EventEmitter {
|
|
5
6
|
constructor(name, node, parent, autoInit = true) {
|
|
7
|
+
super();
|
|
6
8
|
this.children = [];
|
|
7
9
|
this.path = [];
|
|
8
10
|
this.attributesRaw = {};
|
|
9
11
|
this.attributes = {};
|
|
10
12
|
this.data = {};
|
|
13
|
+
const isDocument = this instanceof Document;
|
|
11
14
|
this.name = name;
|
|
12
15
|
if (name === 'root') {
|
|
13
16
|
this.dom = new DOMFragment();
|
|
@@ -21,7 +24,7 @@ export class Component {
|
|
|
21
24
|
}
|
|
22
25
|
this.isRoot = false;
|
|
23
26
|
}
|
|
24
|
-
if (
|
|
27
|
+
if (isDocument) {
|
|
25
28
|
this.document = this;
|
|
26
29
|
}
|
|
27
30
|
else {
|
|
@@ -42,6 +45,9 @@ export class Component {
|
|
|
42
45
|
else {
|
|
43
46
|
this.entry = null;
|
|
44
47
|
}
|
|
48
|
+
if (!isDocument) {
|
|
49
|
+
this.document.emit('componentCreated', this);
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
async init(html, data) {
|
|
47
53
|
this.initAttributesData();
|
|
@@ -180,7 +186,7 @@ export class Component {
|
|
|
180
186
|
}
|
|
181
187
|
for (let i = 0; i < domNode.attributes.length; i++) {
|
|
182
188
|
const attrNameRaw = domNode.attributes[i].name;
|
|
183
|
-
const attrNameUnprefixed = this.
|
|
189
|
+
const attrNameUnprefixed = this.attributeUnprefixed(attrNameRaw);
|
|
184
190
|
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
185
191
|
const attrDataType = this.attributeDataType(attrNameRaw);
|
|
186
192
|
const dataDecoded = attributeValueFromString(domNode.attributes[i].value.toString());
|
|
@@ -214,7 +220,7 @@ export class Component {
|
|
|
214
220
|
this.attributesRaw[attrNameRaw] = domNode.attributes[i].value;
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
|
-
|
|
223
|
+
attributePrefix(attrName) {
|
|
218
224
|
const index = attrName.indexOf(':');
|
|
219
225
|
if (index < 0) {
|
|
220
226
|
return null;
|
|
@@ -222,7 +228,7 @@ export class Component {
|
|
|
222
228
|
return attrName.substring(0, index);
|
|
223
229
|
}
|
|
224
230
|
attributeDataType(attrName) {
|
|
225
|
-
const prefix = this.
|
|
231
|
+
const prefix = this.attributePrefix(attrName);
|
|
226
232
|
if (prefix === 'string' ||
|
|
227
233
|
prefix === 'number' ||
|
|
228
234
|
prefix === 'object' ||
|
|
@@ -231,7 +237,7 @@ export class Component {
|
|
|
231
237
|
}
|
|
232
238
|
return 'any';
|
|
233
239
|
}
|
|
234
|
-
|
|
240
|
+
attributeUnprefixed(attrName) {
|
|
235
241
|
const index = attrName.indexOf(':');
|
|
236
242
|
if (index < 0) {
|
|
237
243
|
return attrName;
|
|
@@ -3,7 +3,9 @@ import { Initializers, LooseObject, RequestContext } from '../../system/Types.js
|
|
|
3
3
|
import { Application } from './Application.js';
|
|
4
4
|
import { DocumentHead } from './DocumentHead.js';
|
|
5
5
|
import { Component } from './Component.js';
|
|
6
|
-
export declare class Document extends Component
|
|
6
|
+
export declare class Document extends Component<{
|
|
7
|
+
'componentCreated': Component;
|
|
8
|
+
}> {
|
|
7
9
|
head: DocumentHead;
|
|
8
10
|
language: string;
|
|
9
11
|
application: Application;
|
package/index.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { Application } from "structured-fw/Application";
|
|
2
2
|
import { config } from './Config.js';
|
|
3
3
|
|
|
4
|
-
new Application(config);
|
|
4
|
+
const app = new Application(config);
|
|
5
|
+
|
|
6
|
+
// app.on('afterComponentsLoaded', (components) => {
|
|
7
|
+
// components.componentNames.forEach((componentName) => {
|
|
8
|
+
// console.log(componentName)
|
|
9
|
+
// console.log(components.getByName(componentName));
|
|
10
|
+
// });
|
|
11
|
+
// })
|
|
12
|
+
|
|
13
|
+
// app.on('componentCreated', (component) => {
|
|
14
|
+
// console.log(component.document.id);
|
|
15
|
+
// })
|
|
16
|
+
|
|
17
|
+
app.on('documentCreated', (document) => {
|
|
18
|
+
document.on('componentCreated', (component) => {
|
|
19
|
+
document.head.add(`<script>console.log('${component.name}')</script>`);
|
|
20
|
+
});
|
|
21
|
+
})
|
package/jsr.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@structured/structured-fw",
|
|
3
|
+
"version": "0.8.21",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"exports" : {
|
|
6
|
+
"./Types": "./system/Types.ts",
|
|
7
|
+
"./Symbols": "./system/Symbols.ts",
|
|
8
|
+
"./Util": "./system/Util.ts",
|
|
9
|
+
"./Application": "./system/server/Application.ts",
|
|
10
|
+
"./Document": "./system/server/Document.ts",
|
|
11
|
+
"./FormValidation": "./system/server/FormValidation.ts",
|
|
12
|
+
"./ClientComponent": "./system/client/ClientComponent.ts"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"handlebars": "^4.7.8",
|
|
16
|
+
"mime-types": "^3.0.0",
|
|
17
|
+
"ts-md5": "^1.3.1",
|
|
18
|
+
"@types/node": "^22.9.3",
|
|
19
|
+
"@types/mime-types": "^2.1.4"
|
|
20
|
+
},
|
|
21
|
+
"exclude": [
|
|
22
|
+
"build",
|
|
23
|
+
"assets",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"tsconfig.json",
|
|
26
|
+
"package.json",
|
|
27
|
+
"package.lock.json",
|
|
28
|
+
"tailwind.config.cjs",
|
|
29
|
+
".vscode",
|
|
30
|
+
"app/Types.ts",
|
|
31
|
+
"app/views/*",
|
|
32
|
+
"app/routes/*",
|
|
33
|
+
"app/models/*"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "build/index",
|
|
17
|
-
"version": "0.8.
|
|
17
|
+
"version": "0.8.4",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"develop": "tsc --watch",
|
|
20
20
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
|
|
21
21
|
"start": "cd build && node index.js",
|
|
22
|
-
"prepublish": "tsc"
|
|
22
|
+
"prepublish": "tsc && mv tsconfig.json _ts.json",
|
|
23
|
+
"postpublish": "mv _ts.json tsconfig.json"
|
|
23
24
|
},
|
|
24
25
|
"bin": {
|
|
25
26
|
"structured": "./build/system/bin/structured.js"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { EventEmitterCallback } from "
|
|
1
|
+
import { EventEmitterCallback } from "./Types.js";
|
|
2
2
|
|
|
3
|
-
export class EventEmitter {
|
|
4
|
-
protected listeners: Record<
|
|
3
|
+
export class EventEmitter<T extends Record<string, any> = Record<string, any>> {
|
|
4
|
+
protected listeners: Partial<Record<keyof T, Array<EventEmitterCallback<any>>>> = {}
|
|
5
5
|
|
|
6
6
|
// add event listener
|
|
7
|
-
public on(eventName:
|
|
7
|
+
public on<K extends keyof T>(eventName: K, callback: EventEmitterCallback<T[K]>): void {
|
|
8
8
|
if (! Array.isArray(this.listeners[eventName])) {
|
|
9
9
|
this.listeners[eventName] = [];
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@ export class EventEmitter {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// emit event with given payload
|
|
16
|
-
public emit(eventName:
|
|
16
|
+
public emit(eventName: keyof T, payload?: any): void {
|
|
17
17
|
if (Array.isArray(this.listeners[eventName])) {
|
|
18
18
|
this.listeners[eventName].forEach((callback) => {
|
|
19
19
|
callback(payload);
|
|
@@ -22,7 +22,7 @@ export class EventEmitter {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// remove event listener
|
|
25
|
-
public unbind(eventName:
|
|
25
|
+
public unbind(eventName: keyof T, callback: EventEmitterCallback<any>): void {
|
|
26
26
|
if (Array.isArray(this.listeners[eventName])) {
|
|
27
27
|
while (true) {
|
|
28
28
|
const index = this.listeners[eventName].indexOf(callback);
|
package/system/Types.ts
CHANGED
|
@@ -177,7 +177,7 @@ export interface ComponentScaffold {
|
|
|
177
177
|
|
|
178
178
|
export type LooseObject = Record<string, any>
|
|
179
179
|
|
|
180
|
-
export type ApplicationEvents = 'serverStarted'|'beforeRequestHandler'|'afterRequestHandler'|'beforeRoutes'|'afterRoutes'|'
|
|
180
|
+
export type ApplicationEvents = 'serverStarted'|'beforeRequestHandler'|'afterRequestHandler'|'beforeRoutes'|'afterRoutes'|'beforeComponentsLoad'|'afterComponentsLoaded'|'documentCreated'|'beforeAssetAccess'|'afterAssetAccess'|'pageNotFound';
|
|
181
181
|
|
|
182
182
|
export type SessionEntry = {
|
|
183
183
|
sessionId : string,
|
|
@@ -229,4 +229,4 @@ export type ClientComponentTransition = {
|
|
|
229
229
|
export type ClientComponentTransitionEvent = 'show' | 'hide';
|
|
230
230
|
export type ClientComponentTransitions = Record<ClientComponentTransitionEvent, ClientComponentTransition>;
|
|
231
231
|
|
|
232
|
-
export type EventEmitterCallback = (payload:
|
|
232
|
+
export type EventEmitterCallback<T> = (payload: T) => void
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import { ClientComponentTransition, ClientComponentTransitions, InitializerFunction, LooseObject, StoreChangeCallback
|
|
1
|
+
import { ClientComponentTransition, ClientComponentTransitions, InitializerFunction, LooseObject, StoreChangeCallback } from '../Types.js';
|
|
2
2
|
import { attributeValueFromString, attributeValueToString, mergeDeep, objectEach, queryStringDecodedSetValue, toCamelCase } from '../Util.js';
|
|
3
3
|
import { DataStoreView } from './DataStoreView.js';
|
|
4
4
|
import { DataStore } from './DataStore.js';
|
|
5
5
|
import { Net } from './Net.js';
|
|
6
6
|
import { NetRequest } from './NetRequest.js';
|
|
7
|
-
import { EventEmitter } from '
|
|
8
|
-
|
|
9
|
-
// window.initializers will always be present
|
|
10
|
-
// each Document has a list of initializers used in components within it
|
|
11
|
-
// and they will be output as initializers = { componentName : initializer }
|
|
12
|
-
declare global {
|
|
13
|
-
interface Window {
|
|
14
|
-
initializers: Record<string, InitializerFunction | string>;
|
|
15
|
-
structuredClientConfig: StructuredClientConfig;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
7
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
18
8
|
|
|
19
9
|
export class ClientComponent extends EventEmitter {
|
|
20
10
|
readonly name: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InitializerFunction, StructuredClientConfig } from "./Types.js";
|
|
2
|
+
|
|
3
|
+
export {}
|
|
4
|
+
// window.initializers will always be present
|
|
5
|
+
// each Document has a list of initializers used in components within it
|
|
6
|
+
// and they will be output as initializers = { componentName : initializer }
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
initializers: Record<string, InitializerFunction | string>;
|
|
10
|
+
structuredClientConfig: StructuredClientConfig;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -59,14 +59,14 @@ export class Application {
|
|
|
59
59
|
console.error(e.message);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
await this.emit('
|
|
62
|
+
await this.emit('beforeComponentsLoad');
|
|
63
63
|
this.components.loadComponents();
|
|
64
|
-
await this.emit('
|
|
64
|
+
await this.emit('afterComponentsLoaded', this.components);
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
await this.emit('beforeRoutes');
|
|
68
68
|
await this.request.loadHandlers();
|
|
69
|
-
await this.emit('afterRoutes');
|
|
69
|
+
await this.emit('afterRoutes', this.request);
|
|
70
70
|
|
|
71
71
|
if (this.config.url.componentRender !== false) {
|
|
72
72
|
// special request handler, executed when ClientComponent.redraw is called
|
|
@@ -84,15 +84,15 @@ export class Application {
|
|
|
84
84
|
// special request handler, serve the client side JS
|
|
85
85
|
this.request.on('GET', /^\/assets\/client-js/, async ({ request, response }) => {
|
|
86
86
|
const uri = request.url?.substring(18) as string;
|
|
87
|
-
|
|
87
|
+
if (uri.includes('..')) {return '';} // disallow having ".." in the URL
|
|
88
|
+
const filePath = path.resolve(import.meta.dirname, '..', uri);
|
|
88
89
|
if (existsSync(filePath)) {
|
|
89
90
|
response.setHeader('Content-Type', 'application/javascript');
|
|
90
|
-
|
|
91
|
-
response.end();
|
|
91
|
+
return readFileSync(filePath);
|
|
92
92
|
} else {
|
|
93
93
|
response.statusCode = 404;
|
|
94
94
|
}
|
|
95
|
-
return;
|
|
95
|
+
return '';
|
|
96
96
|
}, this, true);
|
|
97
97
|
|
|
98
98
|
await this.start();
|
|
@@ -106,7 +106,7 @@ export class Application {
|
|
|
106
106
|
});
|
|
107
107
|
this.server.listen(this.config.http.port, this.config.http.host || '127.0.0.1', async () => {
|
|
108
108
|
const address = (this.config.http.host !== undefined ? this.config.http.host : '') + ':' + this.config.http.port;
|
|
109
|
-
await this.emit('serverStarted');
|
|
109
|
+
await this.emit('serverStarted', this.server);
|
|
110
110
|
console.log(`Server started on ${address}`);
|
|
111
111
|
resolve();
|
|
112
112
|
});
|
|
@@ -120,6 +120,8 @@ export class Application {
|
|
|
120
120
|
payload:
|
|
121
121
|
E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' ? RequestContext :
|
|
122
122
|
E extends 'documentCreated' ? Document :
|
|
123
|
+
E extends 'afterComponentsLoaded' ? Components :
|
|
124
|
+
E extends 'serverStarted' ? Server :
|
|
123
125
|
undefined
|
|
124
126
|
) => void
|
|
125
127
|
): void {
|
|
@@ -144,7 +146,7 @@ export class Application {
|
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
// load
|
|
149
|
+
// load environment variables
|
|
148
150
|
// if this.config.envPrefix is a string, load all ENV variables starting with [envPrefix]_
|
|
149
151
|
// the method is generic, so user can define the expected return type
|
|
150
152
|
public importEnv<T extends LooseObject>(smartPrimitives: boolean = true): T {
|
|
@@ -3,8 +3,9 @@ import { attributeValueFromString, attributeValueToString, objectEach, toCamelCa
|
|
|
3
3
|
import { ComponentEntry, LooseObject } from '../Types.js';
|
|
4
4
|
import { DOMFragment } from './dom/DOMFragment.js';
|
|
5
5
|
import { DOMNode } from './dom/DOMNode.js';
|
|
6
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
6
7
|
|
|
7
|
-
export class Component {
|
|
8
|
+
export class Component<Events extends Record<string, any> = {'componentCreated' : Component}> extends EventEmitter<Events> {
|
|
8
9
|
id: string;
|
|
9
10
|
name: string;
|
|
10
11
|
document: Document;
|
|
@@ -29,6 +30,8 @@ export class Component {
|
|
|
29
30
|
isRoot: boolean;
|
|
30
31
|
|
|
31
32
|
constructor(name: string, node?: DOMNode, parent?: Document|Component, autoInit: boolean = true) {
|
|
33
|
+
super();
|
|
34
|
+
const isDocument = this instanceof Document;
|
|
32
35
|
this.name = name;
|
|
33
36
|
|
|
34
37
|
if (name === 'root') {
|
|
@@ -43,7 +46,7 @@ export class Component {
|
|
|
43
46
|
this.isRoot = false;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
if (
|
|
49
|
+
if (isDocument) {
|
|
47
50
|
// this will only happen if an instance of Document, as it extends component
|
|
48
51
|
this.document = this;
|
|
49
52
|
} else {
|
|
@@ -71,16 +74,20 @@ export class Component {
|
|
|
71
74
|
} else {
|
|
72
75
|
this.entry = null;
|
|
73
76
|
}
|
|
77
|
+
|
|
78
|
+
if (! isDocument) {
|
|
79
|
+
this.document.emit('componentCreated', this);
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
// load component's data and fill it
|
|
77
84
|
// load any nested components recursively
|
|
78
85
|
public async init(html: string, data?: LooseObject): Promise<void> {
|
|
79
86
|
|
|
80
|
-
// extract data-
|
|
87
|
+
// extract data-attributes and encode non-encoded attributes
|
|
81
88
|
this.initAttributesData();
|
|
82
89
|
|
|
83
|
-
// create component container
|
|
90
|
+
// create component container replacing the original tag name with a div
|
|
84
91
|
// (or whatever is set as renderTagName on ComponentEntry)
|
|
85
92
|
this.dom.tagName = this.entry?.renderTagName || 'div';
|
|
86
93
|
|
|
@@ -88,7 +95,7 @@ export class Component {
|
|
|
88
95
|
this.dom.innerHTML = html;
|
|
89
96
|
|
|
90
97
|
|
|
91
|
-
// re-apply attributes the
|
|
98
|
+
// re-apply attributes the original tag had
|
|
92
99
|
// no need to encode values at this point
|
|
93
100
|
// any non-encoded attributes got encoded earlier by initAttributesData
|
|
94
101
|
this.setAttributes(this.attributesRaw, '', false);
|
|
@@ -110,7 +117,7 @@ export class Component {
|
|
|
110
117
|
this.id = this.attributes.componentId;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
// export RequestContext.data fields specified in Application.
|
|
120
|
+
// export RequestContext.data fields specified in Application.exportedRequestContextData
|
|
114
121
|
const exportedContextData = this.document.application.exportedRequestContextData.reduce((prev, field) => {
|
|
115
122
|
if (! this.document.ctx) {return prev;}
|
|
116
123
|
if (field in this.document.ctx.data) {
|
|
@@ -295,7 +302,7 @@ export class Component {
|
|
|
295
302
|
|
|
296
303
|
// attributes can have a data prefix eg. number:data-num="3"
|
|
297
304
|
// return unprefixed attribute name
|
|
298
|
-
const attrNameUnprefixed = this.
|
|
305
|
+
const attrNameUnprefixed = this.attributeUnprefixed(attrNameRaw);
|
|
299
306
|
|
|
300
307
|
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
301
308
|
// only attributes starting with data- are stored to this.attributes
|
|
@@ -353,7 +360,7 @@ export class Component {
|
|
|
353
360
|
|
|
354
361
|
// component attributes can have a data type prefix [prefix]:data-[name]="[val]"
|
|
355
362
|
// returns the prefix
|
|
356
|
-
private
|
|
363
|
+
private attributePrefix(attrName: string): string|null {
|
|
357
364
|
const index = attrName.indexOf(':');
|
|
358
365
|
if (index < 0) {
|
|
359
366
|
return null;
|
|
@@ -364,7 +371,7 @@ export class Component {
|
|
|
364
371
|
// returns the user defined data type of given attribute
|
|
365
372
|
// for example number:data-total returns 'number'
|
|
366
373
|
private attributeDataType(attrName: string): 'string'|'number'|'object'|'boolean'|'any' {
|
|
367
|
-
const prefix = this.
|
|
374
|
+
const prefix = this.attributePrefix(attrName);
|
|
368
375
|
|
|
369
376
|
if (
|
|
370
377
|
prefix === 'string' ||
|
|
@@ -375,13 +382,13 @@ export class Component {
|
|
|
375
382
|
return prefix;
|
|
376
383
|
}
|
|
377
384
|
|
|
378
|
-
// unrecognized attribute
|
|
385
|
+
// unrecognized attribute prefix
|
|
379
386
|
return 'any';
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
// removes the data-type prefix from given attribute name
|
|
383
390
|
// for example number:data-total returns data-total
|
|
384
|
-
private
|
|
391
|
+
private attributeUnprefixed(attrName: string): string {
|
|
385
392
|
const index = attrName.indexOf(':');
|
|
386
393
|
if (index < 0) {
|
|
387
394
|
return attrName;
|
|
@@ -9,7 +9,7 @@ import { attributeValueToString, randomString } from '../Util.js';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
|
|
12
|
-
export class Document extends Component {
|
|
12
|
+
export class Document extends Component<{'componentCreated': Component}> {
|
|
13
13
|
|
|
14
14
|
head: DocumentHead;
|
|
15
15
|
language = 'en';
|
package/tsconfig.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"module": "ES2020",
|
|
11
11
|
"target": "ES2021",
|
|
12
12
|
"alwaysStrict": true,
|
|
13
|
-
"allowSyntheticDefaultImports": true, //
|
|
13
|
+
"allowSyntheticDefaultImports": true, // able to do import { default as varname } from 'module'
|
|
14
14
|
"declaration": true,
|
|
15
15
|
"isolatedDeclarations": true,
|
|
16
16
|
"noImplicitReturns": true,
|