structured-fw 0.8.44 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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)
@@ -691,6 +688,69 @@ then in ComponentName.html:
691
688
  <div data-if="showDiv()"></div>
692
689
  ```
693
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
+
694
754
  **Basic animation/transitions**\
695
755
  If you use conditionals on any DOM node, you may also enable basic animations/transitions using following attributes:
696
756
  - Enable transition:
@@ -61,9 +61,9 @@ function createTsconfig() {
61
61
  "strictNullChecks": true,
62
62
  "strictPropertyInitialization": true,
63
63
  "strictBindCallApply": true,
64
- "moduleResolution": "bundler",
64
+ "moduleResolution": "node16",
65
65
  "outDir": "./build",
66
- "module": "ES2020",
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
- async post(url, data, headers = {}) {
13
+ serializeData(data, headers) {
14
14
  if (typeof data === 'object' && !headers['content-type'] && !(data instanceof FormData)) {
15
15
  headers['content-type'] = 'application/json';
16
- data = JSON.stringify(data);
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
- if (typeof data === 'object' && !headers['content-type']) {
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) {
@@ -137,14 +137,13 @@ export class Component extends EventEmitter {
137
137
  }
138
138
  }
139
139
  async initChildren(passData) {
140
- const componentTags = this.document.application.components.componentNames;
141
- const childNodes = this.dom.queryByTagName(...componentTags);
142
- for (let i = 0; i < childNodes.length; i++) {
143
- const childNode = childNodes[i];
144
- const component = this.document.application.components.getByName(childNode.tagName);
140
+ const potentialComponents = this.dom.components();
141
+ for (let i = 0; i < potentialComponents.length; i++) {
142
+ const potentialComponent = potentialComponents[i];
143
+ const component = this.document.application.components.getByName(potentialComponent.tagName);
145
144
  if (component) {
146
- const child = new Component(component.name, childNode, this, false);
147
- await child.init(childNode.outerHTML, passData);
145
+ const child = new Component(component.name, potentialComponent, this, false);
146
+ await child.init(potentialComponent.outerHTML, passData);
148
147
  this.children.push(child);
149
148
  }
150
149
  }
@@ -1,4 +1,4 @@
1
- import { ComponentEntry, StructuredConfig } from '../Types';
1
+ import { ComponentEntry, StructuredConfig } from '../Types.js';
2
2
  import { Application } from './Application.js';
3
3
  export declare class Components {
4
4
  config: StructuredConfig;
@@ -1,4 +1,4 @@
1
- import { DocumentResource } from '../Types';
1
+ import { DocumentResource } from '../Types.js';
2
2
  export declare class DocumentHead {
3
3
  title: string;
4
4
  js: Array<DocumentResource>;
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  import { DOMNode } from "./DOMNode.js";
2
2
  export class DOMFragment extends DOMNode {
3
3
  constructor() {
4
- super('body');
4
+ super(null, null, 'body');
5
5
  }
6
6
  }
@@ -1,3 +1,4 @@
1
+ import { DOMFragment } from "./DOMFragment.js";
1
2
  type DOMNodeAttribute = {
2
3
  name: string;
3
4
  value: string | true;
@@ -9,20 +10,27 @@ type JSONNode = {
9
10
  strings: Array<string>;
10
11
  };
11
12
  export declare const selfClosingTags: ReadonlyArray<string>;
13
+ export declare const recognizedHTMLTags: ReadonlyArray<string>;
12
14
  export declare class DOMNode {
13
15
  tagName: string;
16
+ root: DOMFragment;
14
17
  parentNode: DOMNode | null;
15
18
  children: Array<DOMNode | string>;
19
+ isRoot: boolean;
16
20
  attributes: Array<DOMNodeAttribute>;
17
21
  attributeMap: Record<string, DOMNodeAttribute>;
18
22
  style: Partial<CSSStyleDeclaration>;
19
23
  selfClosing: boolean;
20
- constructor(tagName: string);
24
+ potentialComponentChildren: Record<string, Array<DOMNode>>;
25
+ constructor(root: DOMFragment | null, parentNode: DOMNode | null, tagName: string);
21
26
  appendChild(node: DOMNode | string): void;
22
27
  setAttribute(attributeName: string, attributeValue: string | true): void;
23
28
  hasAttribute(attributeName: string): boolean;
24
29
  queryByTagName(...tagNames: Array<string>): Array<DOMNode>;
25
30
  queryByHasAttribute(...attributeNames: Array<string>): Array<DOMNode>;
31
+ isPotentialComponent(): boolean;
32
+ registerPotentialComponent(node: DOMNode): void;
33
+ components(): Array<DOMNode>;
26
34
  get innerHTML(): string;
27
35
  set innerHTML(html: string);
28
36
  get outerHTML(): string;
@@ -1,14 +1,22 @@
1
1
  import { HTMLParser } from "./HTMLParser.js";
2
2
  export const selfClosingTags = ['br', 'hr', 'input', 'img', 'link', 'meta', 'source', 'embed', 'path', 'area'];
3
+ export const recognizedHTMLTags = ['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'b', 'i', 'a', 'em', 'strong', 'br', 'hr', 'abbr', 'address', 'bdi', 'bdo', 'blockquote', 'cite', 'code', 'del', 'dfn', 'ins', 'kbd', 'mark', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'sub', 'sup', 'time', 'u', 'var', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'img', 'area', 'map', 'object', 'param', 'picture', 'table', 'tr', 'td', 'th', 'caption', 'colgroup', 'col', 'form', 'input', 'label', 'select', 'option', 'textarea', 'button', 'fieldset', 'legend', 'datalist', 'output', 'iframe', 'audio', 'video', 'source', 'track', 'script', 'noscript', 'div', 'nav', 'aside', 'article', 'section', 'main', 'canvas', 'details', 'dialog', 'embed', 'figure', 'figcaption', 'hgroup', 'meter', 'progress', 'template'];
3
4
  export class DOMNode {
4
- constructor(tagName) {
5
+ constructor(root, parentNode, tagName) {
5
6
  this.parentNode = null;
6
7
  this.children = [];
7
8
  this.attributes = [];
8
9
  this.attributeMap = {};
9
10
  this.style = {};
11
+ this.potentialComponentChildren = {};
12
+ this.root = root === null ? this : root;
13
+ this.isRoot = root === null;
14
+ this.parentNode = parentNode;
10
15
  this.tagName = tagName;
11
16
  this.selfClosing = selfClosingTags.includes(tagName);
17
+ if (this.isPotentialComponent()) {
18
+ this.registerPotentialComponent(this);
19
+ }
12
20
  }
13
21
  appendChild(node) {
14
22
  if (typeof node !== 'string') {
@@ -63,6 +71,27 @@ export class DOMNode {
63
71
  }
64
72
  return nodes;
65
73
  }
74
+ isPotentialComponent() {
75
+ return !recognizedHTMLTags.includes(this.tagName.toLowerCase());
76
+ }
77
+ registerPotentialComponent(node) {
78
+ if (this.parentNode !== null) {
79
+ if (this.parentNode.isRoot || this.parentNode.isPotentialComponent()) {
80
+ if (!(node.tagName in this.parentNode.potentialComponentChildren)) {
81
+ this.parentNode.potentialComponentChildren[node.tagName] = [];
82
+ }
83
+ this.parentNode.potentialComponentChildren[node.tagName].push(node);
84
+ }
85
+ else {
86
+ this.parentNode.registerPotentialComponent(node);
87
+ }
88
+ }
89
+ }
90
+ components() {
91
+ return Object.values(this.potentialComponentChildren).reduce((prev, curr) => {
92
+ return prev.concat(curr);
93
+ }, []);
94
+ }
66
95
  get innerHTML() {
67
96
  return this.children.reduce((html, child) => {
68
97
  if (typeof child === 'string') {
@@ -76,6 +105,7 @@ export class DOMNode {
76
105
  set innerHTML(html) {
77
106
  const fragment = new HTMLParser(html).dom();
78
107
  this.children = fragment.children;
108
+ this.potentialComponentChildren = fragment.potentialComponentChildren;
79
109
  }
80
110
  get outerHTML() {
81
111
  const attributes = this.attributes.reduce((attributes, attribute) => {
@@ -64,7 +64,7 @@ export class HTMLParser {
64
64
  if (this.tokenCurrent.length === 0) {
65
65
  throw this.error(`Found an empty HTML tag <>`);
66
66
  }
67
- const node = new DOMNode(this.tokenCurrent);
67
+ const node = new DOMNode(this.fragment, this.context, this.tokenCurrent);
68
68
  this.context.appendChild(node);
69
69
  this.state = 'idle';
70
70
  this.tokenCurrent = '';
@@ -79,7 +79,7 @@ export class HTMLParser {
79
79
  return true;
80
80
  }
81
81
  this.state = 'attributeName';
82
- const node = new DOMNode(this.tokenCurrent);
82
+ const node = new DOMNode(this.fragment, this.context, this.tokenCurrent);
83
83
  this.context.appendChild(node);
84
84
  this.tokenCurrent = '';
85
85
  if (!node.selfClosing) {
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.44",
17
+ "version": "0.9.0",
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
  }