structured-fw 1.0.9 → 1.1.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.
@@ -1,7 +1,10 @@
1
1
  import { EventEmitterCallback } from './types/eventEmitter.types.js';
2
2
  export declare class EventEmitter<T extends Record<string, any> = Record<string, any>> {
3
3
  protected listeners: Partial<Record<Extract<keyof T, string>, Array<EventEmitterCallback<any>>>>;
4
+ protected destroyed: boolean;
4
5
  on<K extends Extract<keyof T, string>>(eventName: K, callback: EventEmitterCallback<T[K]>): void;
5
6
  emit(eventName: Extract<keyof T, string>, payload?: any): void;
6
7
  off(eventName: keyof T, callback: EventEmitterCallback<any>): void;
8
+ unbindAllListeners(): void;
9
+ emitterDestroy(): void;
7
10
  }
@@ -1,14 +1,21 @@
1
1
  export class EventEmitter {
2
2
  constructor() {
3
3
  this.listeners = {};
4
+ this.destroyed = false;
4
5
  }
5
6
  on(eventName, callback) {
7
+ if (this.destroyed) {
8
+ return;
9
+ }
6
10
  if (!Array.isArray(this.listeners[eventName])) {
7
11
  this.listeners[eventName] = [];
8
12
  }
9
13
  this.listeners[eventName].push(callback);
10
14
  }
11
15
  emit(eventName, payload) {
16
+ if (this.destroyed) {
17
+ return;
18
+ }
12
19
  if (Array.isArray(this.listeners[eventName]) || Array.isArray(this.listeners['*'])) {
13
20
  (this.listeners[eventName] || []).concat(this.listeners['*'] || []).forEach((callback) => {
14
21
  callback(payload, eventName);
@@ -28,4 +35,11 @@ export class EventEmitter {
28
35
  }
29
36
  }
30
37
  }
38
+ unbindAllListeners() {
39
+ this.listeners = {};
40
+ }
41
+ emitterDestroy() {
42
+ this.unbindAllListeners();
43
+ this.destroyed = true;
44
+ }
31
45
  }
@@ -23,11 +23,11 @@ export declare class ClientComponent extends EventEmitter {
23
23
  private conditionalClassNames;
24
24
  private refs;
25
25
  private refsArray;
26
- loaded: boolean;
27
- private initializer;
26
+ isReady: boolean;
28
27
  private data;
29
28
  constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement, store: DataStore, runInitializer?: boolean);
30
29
  private init;
30
+ private reset;
31
31
  private runInitializer;
32
32
  private initData;
33
33
  private attributeData;
@@ -60,4 +60,7 @@ export declare class ClientComponent extends EventEmitter {
60
60
  bind<T extends LooseObject | undefined = undefined>(element: HTMLElement | Window | Array<HTMLElement | Window>, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
61
61
  unbind<T extends LooseObject | undefined = undefined>(element: HTMLElement, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
62
62
  private unbindAll;
63
+ log(msg: any): void;
64
+ warn(msg: any): void;
65
+ error(err: any): void;
63
66
  }
@@ -17,8 +17,7 @@ export class ClientComponent extends EventEmitter {
17
17
  this.conditionalClassNames = [];
18
18
  this.refs = {};
19
19
  this.refsArray = {};
20
- this.loaded = false;
21
- this.initializer = null;
20
+ this.isReady = false;
22
21
  this.data = {};
23
22
  this.name = name;
24
23
  this.domNode = domNode;
@@ -35,36 +34,49 @@ export class ClientComponent extends EventEmitter {
35
34
  this.storeGlobal = store;
36
35
  this.store = new DataStoreView(this.storeGlobal, this);
37
36
  if (this.isRoot) {
38
- this.init(runInitializer);
37
+ this.init(false);
39
38
  }
40
39
  }
41
- async init(runInitializer) {
40
+ async init(isRedraw) {
42
41
  const initializerExists = window.initializers !== undefined && this.name in window.initializers;
42
+ this.reset();
43
+ this.initChildren();
44
+ await Promise.all(this.children.map(async (child) => {
45
+ await child.init(isRedraw);
46
+ }));
47
+ if (!initializerExists && this.conditionals.length > 0) {
48
+ this.store.import(undefined, false, false);
49
+ }
43
50
  this.initRefs();
44
51
  this.initData();
45
52
  this.initModels();
46
- this.initConditionals();
47
- await this.initChildren();
48
53
  this.promoteRefs();
54
+ this.initConditionals();
55
+ await this.runInitializer(isRedraw);
56
+ this.updateConditionals(false);
49
57
  this.store.onChange('*', () => {
50
58
  this.updateConditionals(true);
51
59
  });
52
- if (runInitializer && initializerExists) {
53
- await this.runInitializer();
54
- }
55
- if (this.conditionals.length > 0) {
56
- if (!initializerExists) {
57
- this.store.import(undefined, false, false);
58
- }
59
- this.updateConditionals(false);
60
- }
61
60
  if (this.data.deferred === true) {
62
61
  this.setData('deferred', false, false);
63
62
  this.redraw();
64
63
  }
65
- this.loaded = true;
64
+ this.isReady = true;
66
65
  this.emit('ready');
67
66
  }
67
+ reset() {
68
+ this.data = {};
69
+ this.isReady = false;
70
+ this.refs = {};
71
+ this.refsArray = {};
72
+ this.conditionalClassNames = [];
73
+ this.conditionalCallbacks = {};
74
+ this.conditionals = [];
75
+ this.redrawRequest = null;
76
+ this.initializerExecuted = false;
77
+ this.bound = [];
78
+ this.children = [];
79
+ }
68
80
  async runInitializer(isRedraw = false) {
69
81
  const initializer = window.initializers[this.name];
70
82
  if (!initializer) {
@@ -74,14 +86,22 @@ export class ClientComponent extends EventEmitter {
74
86
  let initializerFunction = null;
75
87
  if (typeof initializer === 'string') {
76
88
  const AsyncFunction = async function () { }.constructor;
77
- initializerFunction = new AsyncFunction('const init = ' + initializer + '; await init.apply(this, [...arguments]);');
89
+ initializerFunction = new AsyncFunction(`
90
+ const init = ${initializer};
91
+ if (!this.destroyed) {
92
+ try {
93
+ await init.apply(this, [...arguments]);
94
+ } catch(e) {
95
+ console.error('Error in component ${this.name}: ' + e.message, this);
96
+ }
97
+ }
98
+ `);
78
99
  }
79
100
  else {
80
101
  initializerFunction = initializer;
81
102
  }
82
103
  if (initializerFunction) {
83
- this.initializer = initializerFunction;
84
- await this.initializer.apply(this, [{
104
+ await initializerFunction.apply(this, [{
85
105
  net: this.net,
86
106
  isRedraw
87
107
  }]);
@@ -133,41 +153,33 @@ export class ClientComponent extends EventEmitter {
133
153
  }
134
154
  return this;
135
155
  }
136
- async initChildren(scope, callback) {
156
+ initChildren(scope, callback) {
137
157
  scope = scope || this.domNode;
138
- const childInitPromises = [];
139
158
  for (let i = 0; i < scope.childNodes.length; i++) {
140
159
  const childNode = scope.childNodes[i];
141
160
  if (childNode.nodeType == 1) {
142
161
  if (childNode.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`)) {
143
- const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.storeGlobal);
162
+ const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.storeGlobal, false);
144
163
  this.children.push(component);
145
164
  if (typeof callback === 'function') {
146
165
  callback(component);
147
166
  }
148
- childInitPromises.push(component.init(true));
149
167
  }
150
168
  else {
151
- childInitPromises.push(this.initChildren(childNode, callback));
169
+ this.initChildren(childNode, callback);
152
170
  }
153
171
  }
154
172
  }
155
- await Promise.all(childInitPromises);
156
173
  }
157
174
  async redraw(data) {
158
175
  if (window.structuredClientConfig.componentRender === false) {
159
- console.error(`Can't redraw component, component rendering URL disabled`);
176
+ this.error(`Can't redraw component, component rendering URL disabled`);
160
177
  return;
161
178
  }
162
179
  if (this.destroyed) {
163
180
  return;
164
181
  }
165
182
  this.emit('beforeRedraw');
166
- if (data) {
167
- objectEach(data, (key, val) => {
168
- this.setData(key, val, false);
169
- });
170
- }
171
183
  if (this.redrawRequest !== null) {
172
184
  this.redrawRequest.abort();
173
185
  this.redrawRequest = null;
@@ -178,20 +190,22 @@ export class ClientComponent extends EventEmitter {
178
190
  this.redrawRequest = redrawRequest.xhr;
179
191
  const componentDataJSON = await redrawRequest.send(JSON.stringify({
180
192
  component: this.name,
181
- attributes: this.data,
193
+ attributes: Object.assign(this.data, data || {}),
182
194
  unwrap: true
183
195
  }));
184
196
  this.redrawRequest = null;
185
197
  if (componentDataJSON.length === 0) {
186
198
  return;
187
199
  }
188
- this.loaded = false;
200
+ this.isReady = false;
189
201
  this.unbindAll();
190
202
  const childStoreChangeCallbacks = {};
191
- Array.from(this.children).forEach((child) => {
203
+ const childrenOld = Array.from(this.children);
204
+ for (let i = 0; i < childrenOld.length; i++) {
205
+ const child = childrenOld[i];
192
206
  childStoreChangeCallbacks[child.getData('componentId')] = child.store.onChangeCallbacks();
193
- child.remove();
194
- });
207
+ await child.remove();
208
+ }
195
209
  const componentData = JSON.parse(componentDataJSON);
196
210
  this.domNode.innerHTML = componentData.html;
197
211
  objectEach(componentData.data, (key, val) => {
@@ -202,7 +216,9 @@ export class ClientComponent extends EventEmitter {
202
216
  window.initializers[key] = componentData.initializers[key];
203
217
  }
204
218
  }
205
- await this.initChildren(this.domNode, (childNew) => {
219
+ await this.init(true);
220
+ for (let i = 0; i < this.children.length; i++) {
221
+ const childNew = this.children[i];
206
222
  const childNewId = childNew.getData('componentId');
207
223
  const existingChild = childNewId in childStoreChangeCallbacks;
208
224
  if (existingChild) {
@@ -212,20 +228,7 @@ export class ClientComponent extends EventEmitter {
212
228
  });
213
229
  });
214
230
  }
215
- });
216
- this.refs = {};
217
- this.refsArray = {};
218
- this.conditionals = [];
219
- this.initRefs();
220
- this.initModels();
221
- this.initConditionals();
222
- this.promoteRefs();
223
- if (this.initializer) {
224
- this.initializerExecuted = false;
225
- this.runInitializer(true);
226
231
  }
227
- this.updateConditionals(false);
228
- this.loaded = true;
229
232
  this.emit('afterRedraw');
230
233
  }
231
234
  initConditionals(node) {
@@ -337,7 +340,7 @@ export class ClientComponent extends EventEmitter {
337
340
  };
338
341
  let data = {};
339
342
  modelNodes.forEach((modelNode) => {
340
- modelNode.addEventListener('input', () => {
343
+ this.bind(modelNode, 'input', () => {
341
344
  let data = modelData(modelNode);
342
345
  const key = Object.keys(data)[0];
343
346
  if (typeof data[key] === 'object') {
@@ -390,14 +393,14 @@ export class ClientComponent extends EventEmitter {
390
393
  if (isMethod) {
391
394
  const parts = /^(!?)\s*([a-zA-Z]+[a-zA-Z0-9_]*)\(([^)]*)\)$/.exec(condition);
392
395
  if (parts === null) {
393
- console.error(`Could not parse condition ${condition}`);
396
+ this.error(`Could not parse condition ${condition}`);
394
397
  return false;
395
398
  }
396
399
  const negated = parts[1] === '!';
397
400
  const functionName = parts[2];
398
401
  const args = parts[3].trim();
399
402
  if (typeof this.conditionalCallbacks[functionName] !== 'function') {
400
- console.warn(`No registered conditional callback '${functionName}'`);
403
+ this.warn(`No registered conditional callback '${functionName}'`);
401
404
  return false;
402
405
  }
403
406
  const isTrue = this.conditionalCallbacks[functionName](args === '' ? undefined : eval(`(${args})`));
@@ -409,7 +412,7 @@ export class ClientComponent extends EventEmitter {
409
412
  else {
410
413
  const parts = /^(!)?\s*([a-zA-Z0-9_]+)\s*((?:==)|(?:===)|(?:!=)|(?:!==)|<|>|(?:<=)|(?:>=))?\s?([^=]+)?$/.exec(condition);
411
414
  if (parts === null) {
412
- console.error(`Could not parse condition ${condition}`);
415
+ this.error(`Could not parse condition ${condition}`);
413
416
  return false;
414
417
  }
415
418
  const property = parts[2];
@@ -559,7 +562,7 @@ export class ClientComponent extends EventEmitter {
559
562
  }
560
563
  async add(appendTo, componentName, data) {
561
564
  if (window.structuredClientConfig.componentRender === false) {
562
- console.error(`Can't add component, component rendering URL disabled`);
565
+ this.error(`Can't add component, component rendering URL disabled`);
563
566
  return null;
564
567
  }
565
568
  const container = typeof appendTo === 'string' ? this.domNode.querySelector(appendTo) : appendTo;
@@ -585,7 +588,7 @@ export class ClientComponent extends EventEmitter {
585
588
  const componentNode = tmpContainer.firstChild;
586
589
  const component = new ClientComponent(this, componentName, componentNode, this.storeGlobal);
587
590
  this.children.push(component);
588
- await component.init(true);
591
+ await component.init(false);
589
592
  container.appendChild(componentNode);
590
593
  return component;
591
594
  }
@@ -780,10 +783,10 @@ export class ClientComponent extends EventEmitter {
780
783
  this.conditionalCallbacks = {};
781
784
  this.refs = {};
782
785
  this.refsArray = {};
783
- this.initializer = null;
784
786
  this.data = {};
785
787
  this.destroyed = true;
786
788
  this.emit('afterDestroy');
789
+ this.emitterDestroy();
787
790
  }
788
791
  bind(element, event, callback) {
789
792
  if (Array.isArray(element)) {
@@ -834,4 +837,13 @@ export class ClientComponent extends EventEmitter {
834
837
  });
835
838
  this.bound = [];
836
839
  }
840
+ log(msg) {
841
+ console.log(this.name, msg);
842
+ }
843
+ warn(msg) {
844
+ console.warn(this.name, msg);
845
+ }
846
+ error(err) {
847
+ console.error(this.name, err);
848
+ }
837
849
  }
@@ -11,6 +11,8 @@ export declare class Document extends Component<{
11
11
  head: DocumentHead;
12
12
  language: string;
13
13
  application: Application;
14
+ htmlTagAttributes: Record<string, string>;
15
+ bodyTagAttributes: Record<string, string>;
14
16
  initializers: Initializers;
15
17
  initializersInitialized: boolean;
16
18
  componentIds: Array<string>;
@@ -25,4 +27,6 @@ export declare class Document extends Component<{
25
27
  allocateId(): string;
26
28
  loadView(pathRelative: string, data?: LooseObject): Promise<Document>;
27
29
  loadComponent(componentName: string, data?: LooseObject): Promise<Document>;
30
+ private htmlTagAttributesString;
31
+ private bodyTagAttributesString;
28
32
  }
@@ -8,6 +8,8 @@ export class Document extends Component {
8
8
  constructor(app, title, ctx) {
9
9
  super('root');
10
10
  this.language = 'en';
11
+ this.htmlTagAttributes = {};
12
+ this.bodyTagAttributes = {};
11
13
  this.initializers = {};
12
14
  this.initializersInitialized = false;
13
15
  this.componentIds = [];
@@ -55,10 +57,12 @@ export class Document extends Component {
55
57
  this.initInitializers();
56
58
  this.initClientConfig();
57
59
  }
60
+ const htmlTagAttributes = this.htmlTagAttributesString();
61
+ const bodyTagAttributes = this.bodyTagAttributesString();
58
62
  return `<!DOCTYPE html>
59
- <html lang="${this.language}">
63
+ <html lang="${this.language}"${htmlTagAttributes.length > 0 ? ` ${htmlTagAttributes}` : ''}>
60
64
  ${this.head.toString()}
61
- <body>
65
+ <body${bodyTagAttributes.length > 0 ? ` ${bodyTagAttributes}` : ''}>
62
66
  ${this.body()}
63
67
  </body>
64
68
  </html>`;
@@ -88,4 +92,18 @@ export class Document extends Component {
88
92
  }
89
93
  return this;
90
94
  }
95
+ htmlTagAttributesString() {
96
+ const parts = [];
97
+ for (const attributeName in this.htmlTagAttributes) {
98
+ parts.push(`${attributeName}="${this.htmlTagAttributes[attributeName]}"`);
99
+ }
100
+ return parts.join(' ');
101
+ }
102
+ bodyTagAttributesString() {
103
+ const parts = [];
104
+ for (const attributeName in this.bodyTagAttributes) {
105
+ parts.push(`${attributeName}="${this.bodyTagAttributes[attributeName]}"`);
106
+ }
107
+ return parts.join(' ');
108
+ }
91
109
  }
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "type": "module",
21
21
  "main": "build/index",
22
- "version": "1.0.9",
22
+ "version": "1.1.0",
23
23
  "scripts": {
24
24
  "develop": "tsc --watch",
25
25
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,hbs,css index.js",
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- import { EventEmitterCallback } from "../Types.js";
2
- export declare class EventEmitter {
3
- protected listeners: Record<string, Array<EventEmitterCallback>>;
4
- on(eventName: string, callback: EventEmitterCallback): void;
5
- emit(eventName: string, payload?: any): void;
6
- unbind(eventName: string, callback: EventEmitterCallback): void;
7
- }
@@ -1,31 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -1 +0,0 @@
1
- export {};
@@ -1,57 +0,0 @@
1
- import { IncomingMessage, ServerResponse } from "http";
2
- import { LooseObject } from "../Types.js";
3
- import { symbolArrays } from "../Symbols.js";
4
- export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
- export type RequestCallback<R extends any, Body extends LooseObject | undefined> = (ctx: RequestContext<Body>) => Promise<R>;
6
- export type RequestHandler = {
7
- match: Array<URISegmentPattern> | RegExp;
8
- methods: Array<RequestMethod>;
9
- callback: RequestCallback<any, LooseObject | undefined>;
10
- scope: any;
11
- staticAsset: boolean;
12
- };
13
- export type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
14
- request: IncomingMessage;
15
- response: ServerResponse;
16
- args: URIArguments;
17
- handler: null | RequestHandler;
18
- cookies: Record<string, string>;
19
- body: Body;
20
- bodyRaw?: Buffer;
21
- files?: Record<string, RequestBodyRecordValue>;
22
- data: RequestContextData;
23
- sessionId?: string;
24
- isAjax: boolean;
25
- getArgs: PostedDataDecoded;
26
- respondWith: (data: any) => void;
27
- redirect: (to: string, statusCode?: number) => void;
28
- show404: () => Promise<void>;
29
- };
30
- export type PostedDataDecoded = Record<string, string | boolean | Array<string | boolean | PostedDataDecoded> | Record<string, string | boolean | Array<string | boolean | PostedDataDecoded>> | Record<string, string | boolean | Array<string | boolean>>>;
31
- export type RequestBodyRecordValue = string | Array<RequestBodyRecordValue> | {
32
- [key: string]: RequestBodyRecordValue;
33
- } | {
34
- [key: string]: RequestBodyFile;
35
- } | Array<RequestBodyFile> | RequestBodyFile;
36
- export interface RequestBodyArguments {
37
- [key: string]: RequestBodyRecordValue;
38
- [symbolArrays]?: {
39
- [key: string]: Array<string>;
40
- };
41
- }
42
- export type RequestBodyFile = {
43
- fileName: string;
44
- data: Buffer;
45
- type: string;
46
- };
47
- export type RequestBodyFiles = {
48
- [key: string]: RequestBodyFile;
49
- };
50
- export type URISegmentPattern = {
51
- pattern: string | RegExp;
52
- name?: string;
53
- type?: 'string' | 'number';
54
- };
55
- export type URIArguments = {
56
- [key: string]: string | number | RegExpExecArray;
57
- };
@@ -1 +0,0 @@
1
- import { symbolArrays } from "../Symbols.js";