structured-fw 1.0.9 → 1.1.5

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
- emit(eventName: Extract<keyof T, string>, payload?: any): void;
6
+ emit(eventName: Extract<keyof T, string>, payload?: any): Promise<void>;
6
7
  off(eventName: keyof T, callback: EventEmitterCallback<any>): void;
8
+ unbindAllListeners(): void;
9
+ emitterDestroy(): void;
7
10
  }
@@ -1,31 +1,44 @@
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
  }
13
+ if (this.listeners[eventName].indexOf(callback) > -1) {
14
+ return;
15
+ }
9
16
  this.listeners[eventName].push(callback);
10
17
  }
11
- emit(eventName, payload) {
18
+ async emit(eventName, payload) {
19
+ if (this.destroyed) {
20
+ return;
21
+ }
12
22
  if (Array.isArray(this.listeners[eventName]) || Array.isArray(this.listeners['*'])) {
13
- (this.listeners[eventName] || []).concat(this.listeners['*'] || []).forEach((callback) => {
14
- callback(payload, eventName);
15
- });
23
+ const listeners = (this.listeners[eventName] || []).concat(this.listeners['*'] || []);
24
+ for (let i = 0; i < listeners.length; i++) {
25
+ await listeners[i](payload, eventName);
26
+ }
16
27
  }
17
28
  }
18
29
  off(eventName, callback) {
19
30
  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
- }
31
+ const index = this.listeners[eventName].indexOf(callback);
32
+ if (index > -1) {
33
+ this.listeners[eventName].splice(index, 1);
28
34
  }
29
35
  }
30
36
  }
37
+ unbindAllListeners() {
38
+ this.listeners = {};
39
+ }
40
+ emitterDestroy() {
41
+ this.unbindAllListeners();
42
+ this.destroyed = true;
43
+ }
31
44
  }
@@ -1,6 +1 @@
1
- import { App } from './App.js';
2
- import { Net } from './Net.js';
3
- export declare class Client {
4
- Components: App;
5
- Net: Net;
6
- }
1
+ export {};
@@ -1,9 +1,2 @@
1
- import { App } from './App.js';
2
- import { Net } from './Net.js';
3
- export class Client {
4
- constructor() {
5
- this.Components = new App();
6
- this.Net = new Net();
7
- }
8
- }
9
- new App();
1
+ import { ClientApplication } from './ClientApplication.js';
2
+ new ClientApplication();
@@ -0,0 +1,13 @@
1
+ import { DataStore } from './DataStore.js';
2
+ import { ClientComponent } from './ClientComponent.js';
3
+ import { InitializerFunction } from '../types/component.types.js';
4
+ export declare class ClientApplication {
5
+ root: ClientComponent;
6
+ store: DataStore;
7
+ initializers: Record<string, InitializerFunction>;
8
+ constructor();
9
+ getInitializer(componentName: string): InitializerFunction | null;
10
+ private loadInitializers;
11
+ hasInitializer(componentName: string): boolean;
12
+ registerInitializer(componentName: string, initializerFunctionString: string): void;
13
+ }
@@ -0,0 +1,43 @@
1
+ import { DataStore } from './DataStore.js';
2
+ import { ClientComponent } from './ClientComponent.js';
3
+ export class ClientApplication {
4
+ constructor() {
5
+ this.store = new DataStore();
6
+ this.initializers = {};
7
+ this.loadInitializers();
8
+ this.root = new ClientComponent(null, 'root', document.body, this);
9
+ }
10
+ getInitializer(componentName) {
11
+ if (!this.hasInitializer(componentName)) {
12
+ return null;
13
+ }
14
+ return this.initializers[componentName];
15
+ }
16
+ loadInitializers() {
17
+ if (!!window.initializers) {
18
+ for (const componentName in window.initializers) {
19
+ this.registerInitializer(componentName, window.initializers[componentName]);
20
+ }
21
+ }
22
+ }
23
+ hasInitializer(componentName) {
24
+ return componentName in this.initializers;
25
+ }
26
+ registerInitializer(componentName, initializerFunctionString) {
27
+ if (this.hasInitializer(componentName)) {
28
+ return;
29
+ }
30
+ const AsyncFunction = async function () { }.constructor;
31
+ const initializerFunction = new AsyncFunction(`
32
+ if (!this.destroyed) {
33
+ const init = ${initializerFunctionString};
34
+ try {
35
+ await init.apply(this, [...arguments]);
36
+ } catch(e) {
37
+ console.error('Error in component ${componentName}: ' + e.message, this);
38
+ }
39
+ }
40
+ `);
41
+ this.initializers[componentName] = initializerFunction;
42
+ }
43
+ }
@@ -1,9 +1,10 @@
1
1
  import { LooseObject } from '../types/general.types.js';
2
2
  import { ClientComponentEventCallback } from '../types/component.types.js';
3
3
  import { DataStoreView } from './DataStoreView.js';
4
- import { DataStore } from './DataStore.js';
5
4
  import { Net } from './Net.js';
6
5
  import { EventEmitter } from '../EventEmitter.js';
6
+ import { ClientApplication } from './ClientApplication.js';
7
+ import { EventEmitterCallback } from '../types/eventEmitter.types.js';
7
8
  export declare class ClientComponent extends EventEmitter {
8
9
  readonly name: string;
9
10
  children: Array<ClientComponent>;
@@ -12,9 +13,10 @@ export declare class ClientComponent extends EventEmitter {
12
13
  readonly isRoot: boolean;
13
14
  readonly root: ClientComponent;
14
15
  store: DataStoreView;
15
- private storeGlobal;
16
+ private app;
16
17
  readonly net: Net;
17
18
  private initializerExecuted;
19
+ readonly fn: Record<string, (...args: Array<any>) => any | undefined>;
18
20
  destroyed: boolean;
19
21
  private redrawRequest;
20
22
  private bound;
@@ -23,11 +25,11 @@ export declare class ClientComponent extends EventEmitter {
23
25
  private conditionalClassNames;
24
26
  private refs;
25
27
  private refsArray;
26
- loaded: boolean;
27
- private initializer;
28
+ isReady: boolean;
28
29
  private data;
29
- constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement, store: DataStore, runInitializer?: boolean);
30
+ constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement, app: ClientApplication);
30
31
  private init;
32
+ private reset;
31
33
  private runInitializer;
32
34
  private initData;
33
35
  private attributeData;
@@ -46,7 +48,6 @@ export declare class ClientComponent extends EventEmitter {
46
48
  private execCondition;
47
49
  conditionalCallback(name: string, callback: (args?: any) => boolean, updateConditionals?: boolean): void;
48
50
  private updateConditionals;
49
- remove(): Promise<void>;
50
51
  parentFind(parentName: string): ClientComponent | null;
51
52
  find(componentName: string, recursive?: boolean): null | ClientComponent;
52
53
  query(componentName: string, recursive?: boolean, results?: Array<ClientComponent>): Array<ClientComponent>;
@@ -57,7 +58,13 @@ export declare class ClientComponent extends EventEmitter {
57
58
  private transitionAttributes;
58
59
  private transitionAxis;
59
60
  private destroy;
60
- bind<T extends LooseObject | undefined = undefined>(element: HTMLElement | Window | Array<HTMLElement | Window>, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
61
- unbind<T extends LooseObject | undefined = undefined>(element: HTMLElement, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
61
+ remove(): Promise<void>;
62
+ bind<T extends any>(element: ClientComponent, event: string, callback: EventEmitterCallback<T>): void;
63
+ bind<T extends LooseObject | undefined>(element: HTMLElement | Window | Array<HTMLElement | Window>, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T>): void;
64
+ unbind<T extends LooseObject | undefined = undefined>(element: HTMLElement | Window | ClientComponent, event: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>, callback: ClientComponentEventCallback<T> | EventEmitterCallback<T>): void;
65
+ private unbindOwn;
62
66
  private unbindAll;
67
+ log(msg: any): void;
68
+ warn(msg: any): void;
69
+ error(err: any): void;
63
70
  }
@@ -4,7 +4,7 @@ import { Net } from './Net.js';
4
4
  import { NetRequest } from './NetRequest.js';
5
5
  import { EventEmitter } from '../EventEmitter.js';
6
6
  export class ClientComponent extends EventEmitter {
7
- constructor(parent, name, domNode, store, runInitializer = true) {
7
+ constructor(parent, name, domNode, app) {
8
8
  super();
9
9
  this.children = [];
10
10
  this.net = new Net();
@@ -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;
@@ -32,60 +31,80 @@ export class ClientComponent extends EventEmitter {
32
31
  this.root = parent.root;
33
32
  this.parent = parent;
34
33
  }
35
- this.storeGlobal = store;
36
- this.store = new DataStoreView(this.storeGlobal, this);
34
+ this.app = app;
35
+ this.store = new DataStoreView(this.app.store, this);
36
+ const self = this;
37
+ this.fn = new Proxy(this.store, {
38
+ set(target, key, val) {
39
+ const fnKey = `fn_${key}`;
40
+ if (target.has(fnKey)) {
41
+ return true;
42
+ }
43
+ target.set(fnKey, val);
44
+ return true;
45
+ },
46
+ get(target, key) {
47
+ return target.get(`fn_${key}`) || (() => {
48
+ self.warn(`Function ${key} not defined`);
49
+ });
50
+ },
51
+ });
37
52
  if (this.isRoot) {
38
- this.init(runInitializer);
53
+ this.init(false);
39
54
  }
40
55
  }
41
- async init(runInitializer) {
42
- const initializerExists = window.initializers !== undefined && this.name in window.initializers;
43
- this.initRefs();
56
+ async init(isRedraw, data = {}) {
57
+ const initializerExists = this.app.hasInitializer(this.name);
58
+ this.reset();
44
59
  this.initData();
60
+ objectEach(data, (key, val) => {
61
+ this.setData(key, val, false);
62
+ });
63
+ this.initChildren();
64
+ await Promise.all(this.children.map(async (child) => {
65
+ await child.init(isRedraw);
66
+ }));
67
+ if (!initializerExists && this.conditionals.length > 0) {
68
+ this.store.import(undefined, false, false);
69
+ }
70
+ this.initRefs();
45
71
  this.initModels();
46
72
  this.initConditionals();
47
- await this.initChildren();
48
- this.promoteRefs();
73
+ await this.runInitializer(isRedraw);
74
+ this.updateConditionals(false);
49
75
  this.store.onChange('*', () => {
50
76
  this.updateConditionals(true);
51
77
  });
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
78
  if (this.data.deferred === true) {
62
79
  this.setData('deferred', false, false);
63
80
  this.redraw();
64
81
  }
65
- this.loaded = true;
82
+ this.isReady = true;
66
83
  this.emit('ready');
67
84
  }
85
+ reset() {
86
+ this.data = {};
87
+ this.isReady = false;
88
+ this.refs = {};
89
+ this.refsArray = {};
90
+ this.conditionalClassNames = [];
91
+ this.conditionalCallbacks = {};
92
+ this.conditionals = [];
93
+ this.redrawRequest = null;
94
+ this.initializerExecuted = false;
95
+ this.bound = [];
96
+ this.children = [];
97
+ }
68
98
  async runInitializer(isRedraw = false) {
69
- const initializer = window.initializers[this.name];
70
- if (!initializer) {
71
- return;
72
- }
73
99
  if (!this.initializerExecuted && !this.destroyed) {
74
- let initializerFunction = null;
75
- if (typeof initializer === 'string') {
76
- const AsyncFunction = async function () { }.constructor;
77
- initializerFunction = new AsyncFunction('const init = ' + initializer + '; await init.apply(this, [...arguments]);');
78
- }
79
- else {
80
- initializerFunction = initializer;
81
- }
82
- if (initializerFunction) {
83
- this.initializer = initializerFunction;
84
- await this.initializer.apply(this, [{
85
- net: this.net,
86
- isRedraw
87
- }]);
100
+ const initializer = this.app.getInitializer(this.name);
101
+ if (initializer === null) {
102
+ return;
88
103
  }
104
+ await initializer.apply(this, [{
105
+ net: this.net,
106
+ isRedraw
107
+ }]);
89
108
  }
90
109
  this.initializerExecuted = true;
91
110
  }
@@ -133,76 +152,68 @@ export class ClientComponent extends EventEmitter {
133
152
  }
134
153
  return this;
135
154
  }
136
- async initChildren(scope, callback) {
155
+ initChildren(scope, callback) {
137
156
  scope = scope || this.domNode;
138
- const childInitPromises = [];
139
157
  for (let i = 0; i < scope.childNodes.length; i++) {
140
158
  const childNode = scope.childNodes[i];
141
159
  if (childNode.nodeType == 1) {
142
160
  if (childNode.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`)) {
143
- const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.storeGlobal);
161
+ const component = new ClientComponent(this, childNode.getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode, this.app);
144
162
  this.children.push(component);
145
163
  if (typeof callback === 'function') {
146
164
  callback(component);
147
165
  }
148
- childInitPromises.push(component.init(true));
149
166
  }
150
167
  else {
151
- childInitPromises.push(this.initChildren(childNode, callback));
168
+ this.initChildren(childNode, callback);
152
169
  }
153
170
  }
154
171
  }
155
- await Promise.all(childInitPromises);
156
172
  }
157
173
  async redraw(data) {
158
174
  if (window.structuredClientConfig.componentRender === false) {
159
- console.error(`Can't redraw component, component rendering URL disabled`);
175
+ this.error(`Can't redraw component, component rendering URL disabled`);
160
176
  return;
161
177
  }
162
178
  if (this.destroyed) {
163
179
  return;
164
180
  }
165
- this.emit('beforeRedraw');
166
- if (data) {
167
- objectEach(data, (key, val) => {
168
- this.setData(key, val, false);
169
- });
170
- }
181
+ await this.emit('beforeRedraw');
171
182
  if (this.redrawRequest !== null) {
172
183
  this.redrawRequest.abort();
173
184
  this.redrawRequest = null;
174
185
  }
186
+ this.unbindOwn();
175
187
  const redrawRequest = new NetRequest('POST', window.structuredClientConfig.componentRender, {
176
188
  'content-type': 'application/json'
177
189
  });
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.destroy();
208
+ }
195
209
  const componentData = JSON.parse(componentDataJSON);
196
210
  this.domNode.innerHTML = componentData.html;
197
- objectEach(componentData.data, (key, val) => {
198
- this.setData(key, val, false);
199
- });
200
- for (const key in componentData.initializers) {
201
- if (!window.initializers[key]) {
202
- window.initializers[key] = componentData.initializers[key];
203
- }
211
+ for (const componentName in componentData.initializers) {
212
+ this.app.registerInitializer(componentName, componentData.initializers[componentName]);
204
213
  }
205
- await this.initChildren(this.domNode, (childNew) => {
214
+ await this.init(true, componentData.data);
215
+ for (let i = 0; i < this.children.length; i++) {
216
+ const childNew = this.children[i];
206
217
  const childNewId = childNew.getData('componentId');
207
218
  const existingChild = childNewId in childStoreChangeCallbacks;
208
219
  if (existingChild) {
@@ -212,21 +223,8 @@ export class ClientComponent extends EventEmitter {
212
223
  });
213
224
  });
214
225
  }
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
226
  }
227
- this.updateConditionals(false);
228
- this.loaded = true;
229
- this.emit('afterRedraw');
227
+ await this.emit('afterRedraw');
230
228
  }
231
229
  initConditionals(node) {
232
230
  const isSelf = node === undefined;
@@ -271,6 +269,7 @@ export class ClientComponent extends EventEmitter {
271
269
  this.initRefs(child);
272
270
  }
273
271
  });
272
+ this.promoteRefs();
274
273
  }
275
274
  initModels(node, modelNodes = []) {
276
275
  const isSelf = node === undefined;
@@ -337,7 +336,7 @@ export class ClientComponent extends EventEmitter {
337
336
  };
338
337
  let data = {};
339
338
  modelNodes.forEach((modelNode) => {
340
- modelNode.addEventListener('input', () => {
339
+ this.bind(modelNode, 'input', () => {
341
340
  let data = modelData(modelNode);
342
341
  const key = Object.keys(data)[0];
343
342
  if (typeof data[key] === 'object') {
@@ -390,14 +389,14 @@ export class ClientComponent extends EventEmitter {
390
389
  if (isMethod) {
391
390
  const parts = /^(!?)\s*([a-zA-Z]+[a-zA-Z0-9_]*)\(([^)]*)\)$/.exec(condition);
392
391
  if (parts === null) {
393
- console.error(`Could not parse condition ${condition}`);
392
+ this.error(`Could not parse condition ${condition}`);
394
393
  return false;
395
394
  }
396
395
  const negated = parts[1] === '!';
397
396
  const functionName = parts[2];
398
397
  const args = parts[3].trim();
399
398
  if (typeof this.conditionalCallbacks[functionName] !== 'function') {
400
- console.warn(`No registered conditional callback '${functionName}'`);
399
+ this.warn(`No registered conditional callback '${functionName}'`);
401
400
  return false;
402
401
  }
403
402
  const isTrue = this.conditionalCallbacks[functionName](args === '' ? undefined : eval(`(${args})`));
@@ -409,7 +408,7 @@ export class ClientComponent extends EventEmitter {
409
408
  else {
410
409
  const parts = /^(!)?\s*([a-zA-Z0-9_]+)\s*((?:==)|(?:===)|(?:!=)|(?:!==)|<|>|(?:<=)|(?:>=))?\s?([^=]+)?$/.exec(condition);
411
410
  if (parts === null) {
412
- console.error(`Could not parse condition ${condition}`);
411
+ this.error(`Could not parse condition ${condition}`);
413
412
  return false;
414
413
  }
415
414
  const property = parts[2];
@@ -500,19 +499,6 @@ export class ClientComponent extends EventEmitter {
500
499
  }
501
500
  });
502
501
  }
503
- async remove() {
504
- if (!this.isRoot) {
505
- const children = Array.from(this.children);
506
- for (let i = 0; i < children.length; i++) {
507
- await children[i].remove();
508
- }
509
- if (this.parent) {
510
- this.parent.children.splice(this.parent.children.indexOf(this), 1);
511
- }
512
- this.domNode.parentElement?.removeChild(this.domNode);
513
- await this.destroy();
514
- }
515
- }
516
502
  parentFind(parentName) {
517
503
  let parent = this.parent;
518
504
  while (true) {
@@ -559,7 +545,7 @@ export class ClientComponent extends EventEmitter {
559
545
  }
560
546
  async add(appendTo, componentName, data) {
561
547
  if (window.structuredClientConfig.componentRender === false) {
562
- console.error(`Can't add component, component rendering URL disabled`);
548
+ this.error(`Can't add component, component rendering URL disabled`);
563
549
  return null;
564
550
  }
565
551
  const container = typeof appendTo === 'string' ? this.domNode.querySelector(appendTo) : appendTo;
@@ -575,17 +561,15 @@ export class ClientComponent extends EventEmitter {
575
561
  unwrap: false
576
562
  }));
577
563
  const res = JSON.parse(componentDataJSON);
578
- for (let key in res.initializers) {
579
- if (!window.initializers[key]) {
580
- window.initializers[key] = res.initializers[key];
581
- }
564
+ for (let componentName in res.initializers) {
565
+ this.app.registerInitializer(componentName, res.initializers[componentName]);
582
566
  }
583
567
  const tmpContainer = document.createElement('div');
584
568
  tmpContainer.innerHTML = res.html;
585
569
  const componentNode = tmpContainer.firstChild;
586
- const component = new ClientComponent(this, componentName, componentNode, this.storeGlobal);
570
+ const component = new ClientComponent(this, componentName, componentNode, this.app);
587
571
  this.children.push(component);
588
- await component.init(true);
572
+ await component.init(false, res.data);
589
573
  container.appendChild(componentNode);
590
574
  return component;
591
575
  }
@@ -772,18 +756,24 @@ export class ClientComponent extends EventEmitter {
772
756
  this.redrawRequest.abort();
773
757
  this.redrawRequest = null;
774
758
  }
775
- this.emit('beforeDestroy');
759
+ await this.emit('beforeDestroy');
760
+ this.domNode.parentElement?.removeChild(this.domNode);
761
+ const children = Array.from(this.children);
762
+ for (let i = 0; i < children.length; i++) {
763
+ await children[i].destroy();
764
+ }
765
+ if (this.parent) {
766
+ this.parent.children.splice(this.parent.children.indexOf(this), 1);
767
+ }
776
768
  this.store.destroy();
777
769
  this.unbindAll();
778
- this.conditionals = [];
779
- this.conditionalClassNames = [];
780
- this.conditionalCallbacks = {};
781
- this.refs = {};
782
- this.refsArray = {};
783
- this.initializer = null;
784
- this.data = {};
770
+ this.reset();
785
771
  this.destroyed = true;
786
- this.emit('afterDestroy');
772
+ await this.emit('afterDestroy');
773
+ this.emitterDestroy();
774
+ }
775
+ async remove() {
776
+ await this.destroy();
787
777
  }
788
778
  bind(element, event, callback) {
789
779
  if (Array.isArray(element)) {
@@ -798,19 +788,33 @@ export class ClientComponent extends EventEmitter {
798
788
  });
799
789
  return;
800
790
  }
801
- const isWindow = element instanceof Window;
802
- if (element instanceof HTMLElement || isWindow) {
791
+ if (element instanceof HTMLElement || element instanceof Window) {
792
+ const cb = callback;
803
793
  const callbackWrapper = (e) => {
804
- callback.apply(this, [e, isWindow ? undefined : this.attributeData(element), element]);
794
+ cb.apply(this, [
795
+ e,
796
+ element instanceof Window ? undefined : this.attributeData(element),
797
+ element
798
+ ]);
805
799
  };
806
800
  this.bound.push({
807
801
  element,
808
- event,
802
+ event: event,
809
803
  callback: callbackWrapper,
810
804
  callbackOriginal: callback
811
805
  });
812
806
  element.addEventListener(event, callbackWrapper);
813
807
  }
808
+ else if (element instanceof ClientComponent) {
809
+ const cb = callback;
810
+ this.bound.push({
811
+ element,
812
+ event: event,
813
+ callback: cb,
814
+ callbackOriginal: cb
815
+ });
816
+ element.on(event, cb);
817
+ }
814
818
  }
815
819
  unbind(element, event, callback) {
816
820
  if (Array.isArray(event)) {
@@ -824,14 +828,35 @@ export class ClientComponent extends EventEmitter {
824
828
  });
825
829
  if (boundIndex > -1) {
826
830
  const bound = this.bound[boundIndex];
827
- bound.element.removeEventListener(bound.event, bound.callback);
828
- this.bound.splice(boundIndex, 1);
831
+ if (bound.element instanceof ClientComponent) {
832
+ bound.element.off(bound.event, bound.callback);
833
+ }
834
+ else {
835
+ bound.element.removeEventListener(bound.event, bound.callback);
836
+ this.bound.splice(boundIndex, 1);
837
+ }
829
838
  }
830
839
  }
840
+ unbindOwn() {
841
+ this.bound.forEach((bound) => {
842
+ if (bound.element === this) {
843
+ this.unbind(bound.element, bound.event, bound.callback);
844
+ }
845
+ });
846
+ }
831
847
  unbindAll() {
832
848
  this.bound.forEach((bound) => {
833
- bound.element.removeEventListener(bound.event, bound.callback);
849
+ this.unbind(bound.element, bound.event, bound.callbackOriginal);
834
850
  });
835
851
  this.bound = [];
836
852
  }
853
+ log(msg) {
854
+ console.log(this.name, msg);
855
+ }
856
+ warn(msg) {
857
+ console.warn(this.name, msg);
858
+ }
859
+ error(err) {
860
+ console.error(this.name, err);
861
+ }
837
862
  }
@@ -10,6 +10,7 @@ export declare class DataStoreView {
10
10
  get<T>(key: string): T | undefined;
11
11
  toggle(key: string): void;
12
12
  keys(): Array<string>;
13
+ has(key: string): boolean;
13
14
  import(fields?: Array<string>, force?: boolean, triggerListeners?: boolean): void;
14
15
  clear(): void;
15
16
  destroy(): void;
@@ -23,7 +23,10 @@ export class DataStoreView {
23
23
  if (this.destroyed) {
24
24
  return [];
25
25
  }
26
- return Object.keys(this.store.get(this.componentId()));
26
+ return Object.keys(this.store.get(this.componentId()) || {});
27
+ }
28
+ has(key) {
29
+ return this.keys().includes(key);
27
30
  }
28
31
  import(fields, force = false, triggerListeners = true) {
29
32
  const fieldsImported = Array.isArray(fields) ? fields : Object.keys(this.component.getData());
@@ -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
  }
@@ -2,8 +2,9 @@ import { ClientComponent } from "../client/ClientComponent.js";
2
2
  import { Net } from "../client/Net.js";
3
3
  import { Application } from "../server/Application.js";
4
4
  import { Component } from "../server/Component.js";
5
- import { LooseObject } from './general.types.js';
6
- import { RequestContext, RequestBodyArguments } from "./request.types.js";
5
+ import { EventEmitterCallback } from "./eventEmitter.types.js";
6
+ import { KeysOfUnion, LooseObject } from './general.types.js';
7
+ import { RequestContext } from "./request.types.js";
7
8
  export type ComponentEntry = {
8
9
  name: string;
9
10
  path: {
@@ -21,18 +22,18 @@ export type ComponentEntry = {
21
22
  static: boolean;
22
23
  renderTagName?: string;
23
24
  exportData: boolean;
24
- exportFields?: Array<string>;
25
+ exportFields?: ReadonlyArray<string>;
25
26
  attributes?: Record<string, string>;
26
27
  initializer?: InitializerFunction;
27
28
  };
28
- export interface ComponentScaffold {
29
+ export interface ComponentScaffold<T extends LooseObject = LooseObject, K extends KeysOfUnion<T> = KeysOfUnion<T>> {
29
30
  tagName?: string;
30
31
  exportData?: boolean;
31
- exportFields?: Array<string>;
32
+ exportFields?: ReadonlyArray<K>;
32
33
  static?: boolean;
33
34
  deferred?: (data: Record<string, any>, ctx: RequestContext | undefined, app: Application) => boolean;
34
35
  attributes?: Record<string, string>;
35
- getData: (this: ComponentScaffold, data: RequestBodyArguments | LooseObject, ctx: undefined | RequestContext, app: Application, component: Component) => Promise<LooseObject | null>;
36
+ getData: (this: ComponentScaffold, data: LooseObject, ctx: undefined | RequestContext, app: Application, component: Component) => Promise<T | void>;
36
37
  [key: string]: any;
37
38
  }
38
39
  export type ClientComponentTransition = {
@@ -42,11 +43,11 @@ export type ClientComponentTransition = {
42
43
  };
43
44
  export type ClientComponentTransitionEvent = 'show' | 'hide';
44
45
  export type ClientComponentTransitions = Record<ClientComponentTransitionEvent, ClientComponentTransition>;
45
- export type ClientComponentBoundEvent<T extends LooseObject | undefined = undefined> = {
46
- element: HTMLElement | Window;
46
+ export type ClientComponentBoundEvent<T extends LooseObject | undefined, E extends HTMLElement | Window | ClientComponent> = {
47
+ element: E;
47
48
  event: keyof HTMLElementEventMap;
48
- callback: (e: Event) => void;
49
- callbackOriginal: ClientComponentEventCallback<T>;
49
+ callback: E extends ClientComponent ? EventEmitterCallback<T> : (e: Event) => void;
50
+ callbackOriginal: E extends ClientComponent ? EventEmitterCallback<T> : ClientComponentEventCallback<T>;
50
51
  };
51
52
  export type ClientComponentEventCallback<T> = (e: Event, data: T, element: HTMLElement | Window) => void;
52
53
  export type InitializerFunction = (this: ClientComponent, ctx: InitializerFunctionContext) => Promise<void>;
@@ -1 +1 @@
1
- export type EventEmitterCallback<T> = (payload: T, eventName: string) => void;
1
+ export type EventEmitterCallback<T> = (payload: T, eventName: string) => void | Promise<void>;
@@ -1 +1,2 @@
1
1
  export type LooseObject = Record<string, any>;
2
+ export type KeysOfUnion<T> = T extends T ? keyof T : never;
package/package.json CHANGED
@@ -19,13 +19,13 @@
19
19
  "license": "MIT",
20
20
  "type": "module",
21
21
  "main": "build/index",
22
- "version": "1.0.9",
22
+ "version": "1.1.5",
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",
26
26
  "start": "cd build && node index.js",
27
27
  "compileAndPack": "tsc && npm pack",
28
- "compileAndPublish": "tsc && npm publish"
28
+ "compileAndPublish": "rm -r build && tsc && npm publish"
29
29
  },
30
30
  "bin": {
31
31
  "structured": "./build/system/bin/structured.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 { DataStore } from './DataStore.js';
2
- import { ClientComponent } from './ClientComponent.js';
3
- export declare class App {
4
- root: ClientComponent;
5
- store: DataStore;
6
- constructor();
7
- }
@@ -1,8 +0,0 @@
1
- import { DataStore } from './DataStore.js';
2
- import { ClientComponent } from './ClientComponent.js';
3
- export class App {
4
- constructor() {
5
- this.store = new DataStore();
6
- this.root = new ClientComponent(null, 'root', document.body, this.store);
7
- }
8
- }
@@ -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";