structured-fw 0.8.3 → 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 +49 -11
- 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 +1 -1
- package/build/system/client/ClientComponent.js +1 -1
- package/build/system/server/Component.d.ts +6 -3
- package/build/system/server/Component.js +8 -6
- package/build/system/server/Document.d.ts +3 -1
- package/index.ts +8 -2
- 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 +1 -1
- package/system/server/Application.ts +1 -1
- package/system/server/Component.ts +13 -11
- package/system/server/Document.ts +1 -1
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ new Application(config);
|
|
|
110
110
|
|
|
111
111
|
### Methods
|
|
112
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
|
|
113
|
-
- `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `
|
|
113
|
+
- `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `ApplicationEvents`:
|
|
114
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
|
|
115
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)
|
|
116
116
|
- `afterRequestHandler` - runs after any request handler (route) is executed. Callback receives `RequestContext` as the first argument
|
|
@@ -173,7 +173,7 @@ app.exportContextFields('user');
|
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
### Session
|
|
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
|
|
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`.
|
|
177
177
|
|
|
178
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.
|
|
179
179
|
|
|
@@ -321,7 +321,7 @@ In some edge cases you may need more control of when a route is executed, in whi
|
|
|
321
321
|
> email: string,
|
|
322
322
|
> password: string,
|
|
323
323
|
> age: number
|
|
324
|
-
>}>('POST', '/users/create',
|
|
324
|
+
>}>('POST', '/users/create', async (ctx) => {
|
|
325
325
|
> ctx.body.email // string
|
|
326
326
|
> ctx.body.age // number
|
|
327
327
|
> const doc = new Document(app, 'User', ctx);
|
|
@@ -330,7 +330,7 @@ In some edge cases you may need more control of when a route is executed, in whi
|
|
|
330
330
|
> ```
|
|
331
331
|
|
|
332
332
|
## Document
|
|
333
|
-
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.
|
|
334
334
|
|
|
335
335
|
Creating a document:
|
|
336
336
|
`const doc = new Document(app, 'HelloWorld page', ctx);`
|
|
@@ -346,7 +346,7 @@ app.request.on('GET', '/home', async (ctx) => {
|
|
|
346
346
|
|
|
347
347
|
## Component
|
|
348
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.
|
|
349
|
-
* HTML file
|
|
349
|
+
* HTML file probably requires no explanation
|
|
350
350
|
* server side file, code that runs on the server and makes data available to HTML and client side code
|
|
351
351
|
* client side file, code that runs on the client (in the browser)
|
|
352
352
|
|
|
@@ -367,7 +367,7 @@ It is recommended, but not necessary, that you contain each component in it's ow
|
|
|
367
367
|
\
|
|
368
368
|
**Component rules:**
|
|
369
369
|
- **Component names must be unique**
|
|
370
|
-
- 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)
|
|
371
371
|
- Components can reside at any depth in the file structure
|
|
372
372
|
|
|
373
373
|
Let's create a HelloWorld Component `/app/views/HelloWorld/HelloWorld.html`:\
|
|
@@ -499,7 +499,7 @@ What we did is, we accepted the number provided by parent component, and returne
|
|
|
499
499
|
betterNumber: number
|
|
500
500
|
}
|
|
501
501
|
```
|
|
502
|
-
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.
|
|
503
503
|
|
|
504
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).
|
|
505
505
|
|
|
@@ -555,7 +555,7 @@ export const init: InitializerFunction = async function() {
|
|
|
555
555
|
```
|
|
556
556
|
Here we accessed the `parent` and obtained it's `name`.
|
|
557
557
|
|
|
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
|
|
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:
|
|
559
559
|
```
|
|
560
560
|
import { InitializerFunction } from 'system/Types.js';
|
|
561
561
|
export const init: InitializerFunction = async function() {
|
|
@@ -585,7 +585,7 @@ export const init: InitializerFunction = async function() {
|
|
|
585
585
|
}
|
|
586
586
|
```
|
|
587
587
|
|
|
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
|
|
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.
|
|
589
589
|
|
|
590
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`.
|
|
591
591
|
|
|
@@ -605,15 +605,16 @@ Methods:
|
|
|
605
605
|
- `store.set(key: string, value: any)` - set data in client side data store
|
|
606
606
|
- `find(componentName: string, recursive: boolean = true): ClientComponent | null` - find a child component
|
|
607
607
|
- `findParent(componentName: string): ClientComponent | null` - find the first parent with given name
|
|
608
|
-
- `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
|
|
609
609
|
- `ref<T>(refName: string): T` - get a HTMLElement or ClientComponent that has attribute `ref="[refName]"`
|
|
610
610
|
- `arrayRef<T>(refName: string): Array<T>` - get an array of HTMLElement or ClientComponent that have attribute `array:ref="[refName]"`
|
|
611
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
|
|
612
612
|
|
|
613
613
|
## Good to know
|
|
614
|
-
- [
|
|
614
|
+
- [Using CSS frameworks](#css-frameworks)
|
|
615
615
|
- [Using JS runtimes other than Node.js](#runtimes)
|
|
616
616
|
- [Why not JSR](#jsr)
|
|
617
|
+
- [Best practices](#best-practices)
|
|
617
618
|
|
|
618
619
|
### CSS frameworks
|
|
619
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.
|
|
@@ -721,6 +722,43 @@ Run application using `deno main.ts`
|
|
|
721
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).\
|
|
722
723
|
This does not stop the framework from working with Deno, but for the time being, we have to stick with good old npm.
|
|
723
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
|
+
|
|
724
762
|
## Why Structured
|
|
725
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.
|
|
726
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' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | '
|
|
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;
|
|
@@ -2,7 +2,7 @@ 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 '
|
|
5
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
6
6
|
export declare class ClientComponent extends EventEmitter {
|
|
7
7
|
readonly name: string;
|
|
8
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();
|
|
@@ -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,8 +1,10 @@
|
|
|
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 = {};
|
|
@@ -44,7 +46,7 @@ export class Component {
|
|
|
44
46
|
this.entry = null;
|
|
45
47
|
}
|
|
46
48
|
if (!isDocument) {
|
|
47
|
-
this.document.
|
|
49
|
+
this.document.emit('componentCreated', this);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
async init(html, data) {
|
|
@@ -184,7 +186,7 @@ export class Component {
|
|
|
184
186
|
}
|
|
185
187
|
for (let i = 0; i < domNode.attributes.length; i++) {
|
|
186
188
|
const attrNameRaw = domNode.attributes[i].name;
|
|
187
|
-
const attrNameUnprefixed = this.
|
|
189
|
+
const attrNameUnprefixed = this.attributeUnprefixed(attrNameRaw);
|
|
188
190
|
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
189
191
|
const attrDataType = this.attributeDataType(attrNameRaw);
|
|
190
192
|
const dataDecoded = attributeValueFromString(domNode.attributes[i].value.toString());
|
|
@@ -218,7 +220,7 @@ export class Component {
|
|
|
218
220
|
this.attributesRaw[attrNameRaw] = domNode.attributes[i].value;
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
|
-
|
|
223
|
+
attributePrefix(attrName) {
|
|
222
224
|
const index = attrName.indexOf(':');
|
|
223
225
|
if (index < 0) {
|
|
224
226
|
return null;
|
|
@@ -226,7 +228,7 @@ export class Component {
|
|
|
226
228
|
return attrName.substring(0, index);
|
|
227
229
|
}
|
|
228
230
|
attributeDataType(attrName) {
|
|
229
|
-
const prefix = this.
|
|
231
|
+
const prefix = this.attributePrefix(attrName);
|
|
230
232
|
if (prefix === 'string' ||
|
|
231
233
|
prefix === 'number' ||
|
|
232
234
|
prefix === 'object' ||
|
|
@@ -235,7 +237,7 @@ export class Component {
|
|
|
235
237
|
}
|
|
236
238
|
return 'any';
|
|
237
239
|
}
|
|
238
|
-
|
|
240
|
+
attributeUnprefixed(attrName) {
|
|
239
241
|
const index = attrName.indexOf(':');
|
|
240
242
|
if (index < 0) {
|
|
241
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,7 +1,7 @@
|
|
|
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
5
|
|
|
6
6
|
// app.on('afterComponentsLoaded', (components) => {
|
|
7
7
|
// components.componentNames.forEach((componentName) => {
|
|
@@ -12,4 +12,10 @@ new Application(config);
|
|
|
12
12
|
|
|
13
13
|
// app.on('componentCreated', (component) => {
|
|
14
14
|
// console.log(component.document.id);
|
|
15
|
-
// })
|
|
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/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'|'beforeComponentsLoad'|'afterComponentsLoaded'|'
|
|
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
|
|
@@ -4,7 +4,7 @@ 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 '
|
|
7
|
+
import { EventEmitter } from '../EventEmitter.js';
|
|
8
8
|
|
|
9
9
|
export class ClientComponent extends EventEmitter {
|
|
10
10
|
readonly name: string;
|
|
@@ -146,7 +146,7 @@ export class Application {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// load
|
|
149
|
+
// load environment variables
|
|
150
150
|
// if this.config.envPrefix is a string, load all ENV variables starting with [envPrefix]_
|
|
151
151
|
// the method is generic, so user can define the expected return type
|
|
152
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,7 @@ export class Component {
|
|
|
29
30
|
isRoot: boolean;
|
|
30
31
|
|
|
31
32
|
constructor(name: string, node?: DOMNode, parent?: Document|Component, autoInit: boolean = true) {
|
|
33
|
+
super();
|
|
32
34
|
const isDocument = this instanceof Document;
|
|
33
35
|
this.name = name;
|
|
34
36
|
|
|
@@ -74,7 +76,7 @@ export class Component {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
if (! isDocument) {
|
|
77
|
-
this.document.
|
|
79
|
+
this.document.emit('componentCreated', this);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -82,10 +84,10 @@ export class Component {
|
|
|
82
84
|
// load any nested components recursively
|
|
83
85
|
public async init(html: string, data?: LooseObject): Promise<void> {
|
|
84
86
|
|
|
85
|
-
// extract data-
|
|
87
|
+
// extract data-attributes and encode non-encoded attributes
|
|
86
88
|
this.initAttributesData();
|
|
87
89
|
|
|
88
|
-
// create component container
|
|
90
|
+
// create component container replacing the original tag name with a div
|
|
89
91
|
// (or whatever is set as renderTagName on ComponentEntry)
|
|
90
92
|
this.dom.tagName = this.entry?.renderTagName || 'div';
|
|
91
93
|
|
|
@@ -93,7 +95,7 @@ export class Component {
|
|
|
93
95
|
this.dom.innerHTML = html;
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
// re-apply attributes the
|
|
98
|
+
// re-apply attributes the original tag had
|
|
97
99
|
// no need to encode values at this point
|
|
98
100
|
// any non-encoded attributes got encoded earlier by initAttributesData
|
|
99
101
|
this.setAttributes(this.attributesRaw, '', false);
|
|
@@ -115,7 +117,7 @@ export class Component {
|
|
|
115
117
|
this.id = this.attributes.componentId;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
// export RequestContext.data fields specified in Application.
|
|
120
|
+
// export RequestContext.data fields specified in Application.exportedRequestContextData
|
|
119
121
|
const exportedContextData = this.document.application.exportedRequestContextData.reduce((prev, field) => {
|
|
120
122
|
if (! this.document.ctx) {return prev;}
|
|
121
123
|
if (field in this.document.ctx.data) {
|
|
@@ -300,7 +302,7 @@ export class Component {
|
|
|
300
302
|
|
|
301
303
|
// attributes can have a data prefix eg. number:data-num="3"
|
|
302
304
|
// return unprefixed attribute name
|
|
303
|
-
const attrNameUnprefixed = this.
|
|
305
|
+
const attrNameUnprefixed = this.attributeUnprefixed(attrNameRaw);
|
|
304
306
|
|
|
305
307
|
if (attrNameUnprefixed.indexOf('data-') === 0) {
|
|
306
308
|
// only attributes starting with data- are stored to this.attributes
|
|
@@ -358,7 +360,7 @@ export class Component {
|
|
|
358
360
|
|
|
359
361
|
// component attributes can have a data type prefix [prefix]:data-[name]="[val]"
|
|
360
362
|
// returns the prefix
|
|
361
|
-
private
|
|
363
|
+
private attributePrefix(attrName: string): string|null {
|
|
362
364
|
const index = attrName.indexOf(':');
|
|
363
365
|
if (index < 0) {
|
|
364
366
|
return null;
|
|
@@ -369,7 +371,7 @@ export class Component {
|
|
|
369
371
|
// returns the user defined data type of given attribute
|
|
370
372
|
// for example number:data-total returns 'number'
|
|
371
373
|
private attributeDataType(attrName: string): 'string'|'number'|'object'|'boolean'|'any' {
|
|
372
|
-
const prefix = this.
|
|
374
|
+
const prefix = this.attributePrefix(attrName);
|
|
373
375
|
|
|
374
376
|
if (
|
|
375
377
|
prefix === 'string' ||
|
|
@@ -380,13 +382,13 @@ export class Component {
|
|
|
380
382
|
return prefix;
|
|
381
383
|
}
|
|
382
384
|
|
|
383
|
-
// unrecognized attribute
|
|
385
|
+
// unrecognized attribute prefix
|
|
384
386
|
return 'any';
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
// removes the data-type prefix from given attribute name
|
|
388
390
|
// for example number:data-total returns data-total
|
|
389
|
-
private
|
|
391
|
+
private attributeUnprefixed(attrName: string): string {
|
|
390
392
|
const index = attrName.indexOf(':');
|
|
391
393
|
if (index < 0) {
|
|
392
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,
|