structured-fw 0.8.4 → 0.8.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.
Files changed (95) hide show
  1. package/README.md +116 -18
  2. package/build/system/server/Application.d.ts +3 -1
  3. package/build/system/server/Application.js +11 -0
  4. package/build/system/server/Components.d.ts +1 -1
  5. package/build/system/server/DocumentHead.d.ts +1 -1
  6. package/build/system/server/FormValidation.d.ts +1 -1
  7. package/package.json +10 -3
  8. package/app/models/README.md +0 -9
  9. package/app/routes/README.md +0 -19
  10. package/app/views/README.md +0 -1
  11. package/app/views/layout.html +0 -1
  12. package/build/Config.d.ts +0 -2
  13. package/build/Config.js +0 -30
  14. package/build/app/Types.d.ts +0 -5
  15. package/build/app/Types.js +0 -1
  16. package/build/app/global.d.ts +0 -3
  17. package/build/app/global.js +0 -1
  18. package/build/app/models/Users.d.ts +0 -0
  19. package/build/app/models/Users.js +0 -1
  20. package/build/app/routes/Auth.d.ts +0 -0
  21. package/build/app/routes/Auth.js +0 -1
  22. package/build/app/routes/Test.d.ts +0 -2
  23. package/build/app/routes/Test.js +0 -101
  24. package/build/app/routes/Todo.d.ts +0 -0
  25. package/build/app/routes/Todo.js +0 -1
  26. package/build/app/routes/Upload.d.ts +0 -0
  27. package/build/app/routes/Upload.js +0 -1
  28. package/build/app/routes/Validation.d.ts +0 -2
  29. package/build/app/routes/Validation.js +0 -34
  30. package/build/app/routess/Auth.d.ts +0 -0
  31. package/build/app/routess/Auth.js +0 -1
  32. package/build/app/routess/Test.d.ts +0 -2
  33. package/build/app/routess/Test.js +0 -101
  34. package/build/app/routess/Todo.d.ts +0 -0
  35. package/build/app/routess/Todo.js +0 -1
  36. package/build/app/routess/Upload.d.ts +0 -0
  37. package/build/app/routess/Upload.js +0 -1
  38. package/build/app/routess/Validation.d.ts +0 -2
  39. package/build/app/routess/Validation.js +0 -34
  40. package/build/app/views/components/ClientImport/ClientImport.client.d.ts +0 -2
  41. package/build/app/views/components/ClientImport/ClientImport.client.js +0 -4
  42. package/build/app/views/components/ClientImport/Export.d.ts +0 -1
  43. package/build/app/views/components/ClientImport/Export.js +0 -1
  44. package/build/app/views/components/Conditionals/Conditionals.client.d.ts +0 -2
  45. package/build/app/views/components/Conditionals/Conditionals.client.js +0 -43
  46. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +0 -8
  47. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +0 -7
  48. package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +0 -2
  49. package/build/app/views/components/ModelsTest/ModelsTest.client.js +0 -5
  50. package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
  51. package/build/app/views/components/MultipartForm/MultipartForm.client.js +0 -1
  52. package/build/app/views/components/PassObject/PassObject.d.ts +0 -10
  53. package/build/app/views/components/PassObject/PassObject.js +0 -10
  54. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +0 -6
  55. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +0 -6
  56. package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +0 -2
  57. package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +0 -6
  58. package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +0 -8
  59. package/build/app/views/components/RedrawAbort/RedrawAbort.js +0 -8
  60. package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +0 -7
  61. package/build/app/views/components/ServerSideContext/ServerSideContext.js +0 -10
  62. package/build/assets/ts/Export.d.ts +0 -1
  63. package/build/assets/ts/Export.js +0 -1
  64. package/build/index.d.ts +0 -1
  65. package/build/index.js +0 -8
  66. package/build/tsconfig.tsbuildinfo +0 -1
  67. package/jsr.json +0 -35
  68. package/system/EventEmitter.ts +0 -38
  69. package/system/Helpers.ts +0 -97
  70. package/system/Symbols.ts +0 -6
  71. package/system/Types.ts +0 -232
  72. package/system/Util.ts +0 -488
  73. package/system/bin/structured.ts +0 -115
  74. package/system/client/App.ts +0 -11
  75. package/system/client/Client.ts +0 -9
  76. package/system/client/ClientComponent.ts +0 -1107
  77. package/system/client/DataStore.ts +0 -101
  78. package/system/client/DataStoreView.ts +0 -82
  79. package/system/client/Net.ts +0 -58
  80. package/system/client/NetRequest.ts +0 -64
  81. package/system/global.d.ts +0 -12
  82. package/system/server/Application.ts +0 -239
  83. package/system/server/Component.ts +0 -409
  84. package/system/server/Components.ts +0 -114
  85. package/system/server/Cookies.ts +0 -29
  86. package/system/server/Document.ts +0 -163
  87. package/system/server/DocumentHead.ts +0 -150
  88. package/system/server/FormValidation.ts +0 -231
  89. package/system/server/Handlebars.ts +0 -51
  90. package/system/server/Request.ts +0 -502
  91. package/system/server/Session.ts +0 -151
  92. package/system/server/dom/DOMFragment.ts +0 -7
  93. package/system/server/dom/DOMNode.ts +0 -140
  94. package/system/server/dom/HTMLParser.ts +0 -238
  95. package/tsconfig.json +0 -31
@@ -1,1107 +0,0 @@
1
- import { ClientComponentTransition, ClientComponentTransitions, InitializerFunction, LooseObject, StoreChangeCallback } from '../Types.js';
2
- import { attributeValueFromString, attributeValueToString, mergeDeep, objectEach, queryStringDecodedSetValue, toCamelCase } from '../Util.js';
3
- import { DataStoreView } from './DataStoreView.js';
4
- import { DataStore } from './DataStore.js';
5
- import { Net } from './Net.js';
6
- import { NetRequest } from './NetRequest.js';
7
- import { EventEmitter } from '../EventEmitter.js';
8
-
9
- export class ClientComponent extends EventEmitter {
10
- readonly name: string;
11
- children: Array<ClientComponent> = [];
12
- readonly parent: ClientComponent;
13
- readonly domNode: HTMLElement;
14
- readonly isRoot: boolean;
15
- readonly root: ClientComponent;
16
- store: DataStoreView;
17
- private storeGlobal: DataStore;
18
- readonly net: Net = new Net();
19
- private initializerExecuted: boolean = false;
20
-
21
- destroyed: boolean = false;
22
-
23
- private redrawRequest: XMLHttpRequest | null = null;
24
-
25
- // optional user defined callbacks
26
- onDestroy?: Function;
27
- onRedraw?: Function;
28
-
29
- // callbacks bound using bind method
30
- private bound: Array<{
31
- element: HTMLElement;
32
- event: string;
33
- callback: (e: Event) => void;
34
- }> = [];
35
-
36
- // DOM elements within the component that have a data-if attribute
37
- private conditionals: Array<HTMLElement> = [];
38
-
39
- // available for use in data-if and data-classname-[className]
40
- private conditionalCallbacks: Record<string, (args?: any) => boolean> = {};
41
-
42
- private conditionalClassNames: Array<{
43
- element: HTMLElement,
44
- className: string
45
- }> = [];
46
-
47
- private refs: {
48
- [key: string]: HTMLElement | ClientComponent;
49
- } = {};
50
- private refsArray: {
51
- [key: string]: Array<HTMLElement | ClientComponent>;
52
- } = {};
53
-
54
- loaded: boolean = false;
55
-
56
- // callback executed each time the component is redrawn
57
- // this is the ideal place for binding any event listeners within component
58
- private initializer: InitializerFunction | null = null;
59
-
60
- // data-attr are parsed into an object
61
- private data: {
62
- [key: string]: any;
63
- } = {};
64
-
65
- constructor(parent: ClientComponent | null, name: string, domNode: HTMLElement, store: DataStore, runInitializer: boolean = true) {
66
- super();
67
- this.name = name;
68
- this.domNode = domNode;
69
- if (parent === null) {
70
- this.isRoot = true;
71
- this.root = this;
72
- this.parent = this;
73
- } else {
74
- this.isRoot = false;
75
- this.root = parent.root;
76
- this.parent = parent;
77
- }
78
-
79
- this.storeGlobal = store;
80
- this.store = new DataStoreView(this.storeGlobal, this);
81
-
82
- if (this.isRoot) {
83
- // only root gets initialized by itself
84
- // rest of the component tree is initialized during initChildren
85
- // this is in order to be able to await the children to initialize
86
- // and in extension be able to tell when all components are initialized
87
- this.init(runInitializer);
88
- }
89
- }
90
-
91
- // initialize component and it's children recursively
92
- private async init(runInitializer: boolean) {
93
- const initializerExists = window.initializers !== undefined && this.name in window.initializers;
94
- this.initRefs();
95
- this.initData();
96
- this.initModels();
97
- this.initConditionals();
98
- await this.initChildren();
99
- this.promoteRefs();
100
-
101
- // update conditionals whenever any data in component's store has changed
102
- this.store.onChange('*', () => {
103
- this.updateConditionals(true);
104
- });
105
-
106
- // run initializer, if one exists for current component
107
- // if autoInit = false component will not be automatically initialized
108
- if (runInitializer && initializerExists) {
109
- await this.runInitializer();
110
- }
111
-
112
- // update conditionals as soon as component is initialized
113
- if (this.conditionals.length > 0) {
114
- if (! initializerExists) {
115
- // component has no initializer, import all exported fields
116
- this.store.import(undefined, false, false);
117
- }
118
- this.updateConditionals(false);
119
- }
120
-
121
- // deferred component, redraw it immediately
122
- if (this.data.deferred === true) {
123
- this.setData('deferred', false, false);
124
- this.redraw();
125
- }
126
-
127
- this.loaded = true;
128
-
129
- // component emits "ready" when initialized
130
- // when a component emits "ready" it means it and all of it's children recursively have been initialized
131
- // if root emits "ready", that means all components in current document are initialized
132
- this.emit('ready');
133
- }
134
-
135
- // set initializer callback and execute it
136
- private async runInitializer(isRedraw: boolean = false) {
137
- const initializer = window.initializers[this.name];
138
- if (! initializer) {return;}
139
- if (! this.initializerExecuted && ! this.destroyed) {
140
- let initializerFunction: InitializerFunction | null = null;
141
- if (typeof initializer === 'string') {
142
- // create an async function using AsyncFunction constructor
143
- const AsyncFunction = async function () {}.constructor;
144
- // @ts-ignore
145
- initializerFunction = new AsyncFunction('const init = ' + initializer + '; await init.apply(this, [...arguments]);') as InitializerFunction;
146
- } else {
147
- initializerFunction = initializer;
148
- }
149
-
150
- if (initializerFunction) {
151
- this.initializer = initializerFunction;
152
- await this.initializer.apply(this, [{
153
- net: this.net,
154
- isRedraw
155
- }]);
156
- }
157
- }
158
- this.initializerExecuted = true;
159
- }
160
-
161
- // parse all data-[key] attributes found on this.domNode into this.data object
162
- // key converted to camelCase
163
- // values are expected to be encoded using attributeValueToString
164
- // and will be decoded using attributeValueFromString
165
- private initData(): void {
166
- for (let i = 0; i < this.domNode.attributes.length; i++) {
167
- // data-attr, convert to dataAttr and store value
168
- if (/^((number|string|boolean|object|any):)?data-[^\s]+/.test(this.domNode.attributes[i].name)) {
169
- const value = this.domNode.attributes[i].value;
170
- const attrData = attributeValueFromString(value);
171
-
172
- if (typeof attrData === 'object') {
173
- this.setData(attrData.key, attrData.value, false);
174
- } else {
175
- // not a valid attribute data string, assign as is (string)
176
- const key = toCamelCase(this.domNode.attributes[i].name.substring(5));
177
- this.setData(key, attrData, false);
178
- }
179
- }
180
- }
181
- }
182
-
183
- // array of attribute data all the way up to root, or the first component with no dependencies (no data-use attribute)
184
- // used for redraw
185
- public pathData(): Array<{
186
- [key: string]: string;
187
- }> {
188
- let current: ClientComponent = this;
189
- const data = [];
190
- do {
191
- data.push(current.data);
192
- if (current.isRoot || !current.data.use) {
193
- break;
194
- }
195
- current = current.parent;
196
- } while (true);
197
- return data.reverse();
198
- }
199
-
200
- // sets this.data[key] and optionally this.store[key], key is passed through toCamelCase
201
- // sets data-[key]="value" attribute on this.domNode, value passed through attributeValueToString
202
- // if updateStore is true (default) value is also applied to this.store
203
- // returns this to allow chaining
204
- public setData(key: string, value: any, updateStore: boolean = true): ClientComponent {
205
- const dataKey = `data-${key}`;
206
- this.domNode.setAttribute(dataKey, attributeValueToString(key, value));
207
- const keyCamelCase = toCamelCase(key);
208
- this.data[keyCamelCase] = value;
209
- if (updateStore) {
210
- this.store.set(keyCamelCase, value);
211
- }
212
- return this;
213
- }
214
-
215
- // find all DOM nodes with data-structured-component attribute within this component,
216
- // instantiate a ClientComponent with them and add them to this.children
217
- // if callback is a function, for each instantiated child
218
- // callback is executed with child as first argument
219
- private async initChildren(scope?: HTMLElement, callback?: (component: ClientComponent) => void): Promise<void> {
220
- scope = scope || this.domNode;
221
-
222
- // array of promises that are resolved when child nodes are recursively initialized
223
- const childInitPromises: Array<Promise<void>> = [];
224
-
225
- for (let i = 0; i < scope.childNodes.length; i++) {
226
- const childNode = scope.childNodes[i];
227
- if (childNode.nodeType == 1) {
228
- if ((childNode as HTMLElement).hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`)) {
229
- // found a child component, add to children
230
- const component = new ClientComponent(this, (childNode as HTMLElement).getAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`) || '', childNode as HTMLElement, this.storeGlobal);
231
- this.children.push(component);
232
- if (typeof callback === 'function') {
233
- callback(component);
234
- }
235
- childInitPromises.push(component.init(true));
236
- } else {
237
- // not a component, resume from here recursively
238
- childInitPromises.push(this.initChildren((childNode as HTMLElement), callback));
239
- }
240
- }
241
- }
242
-
243
- // await children to be initialized
244
- await Promise.all(childInitPromises);
245
- }
246
-
247
- // fetch from server and replace with new HTML
248
- // if data is provided, each key will be set on component using this.setData
249
- // and as such, component will receive it when rendering
250
- public async redraw(data?: LooseObject): Promise<void> {
251
-
252
- if (window.structuredClientConfig.componentRender === false) {
253
- console.error(`Can't redraw component, component rendering URL disabled`);
254
- return;
255
- }
256
-
257
- if (this.destroyed) {return;}
258
-
259
- // set data if provided
260
- if (data) {
261
- objectEach(data, (key, val) => {
262
- this.setData(key, val, false);
263
- });
264
- }
265
-
266
- // abort existing redraw call, if in progress
267
- if (this.redrawRequest !== null) {
268
- this.redrawRequest.abort();
269
- this.redrawRequest = null;
270
- }
271
-
272
- // request a component to be re-rendered on the server
273
- // unwrap = true so that component container is excluded
274
- // this component already has it's own container and we only care about what changed within it
275
- const redrawRequest = new NetRequest('POST', window.structuredClientConfig.componentRender, {
276
- 'content-type': 'application/json'
277
- });
278
- this.redrawRequest = redrawRequest.xhr;
279
- const componentDataJSON = await redrawRequest.send(JSON.stringify({
280
- component: this.name,
281
- attributes: this.data,
282
- unwrap: true
283
- }));
284
- // clear redraw request as the request is executed and does not need to be cancelled
285
- // in case component gets redrawn again
286
- this.redrawRequest = null;
287
-
288
- // should only happen if a previous redraw attempt was aborted
289
- if (componentDataJSON.length === 0) { return; }
290
-
291
- // mark component as not loaded
292
- this.loaded = false;
293
-
294
- // if user has defined onRedraw callback, run it
295
- if (typeof this.onRedraw === 'function') {
296
- this.onRedraw.apply(this)
297
- }
298
-
299
- // remove all bound event listeners as DOM will get replaced in the process
300
- this.unbindAll();
301
-
302
- // destroy existing children as their associated domNode is no longer part of the DOM
303
- // new children will be initialized based on the new DOM
304
- // new DOM may contain same children (same componentId) however by destroying the children
305
- // any store change listeners will be lost, before destroying each child
306
- // keep a copy of the store change listeners, which we'll use later to restore those listeners
307
- const childStoreChangeCallbacks: Record<string, Record<string, Array<StoreChangeCallback>>> = {}
308
- Array.from(this.children).forEach((child) => {
309
- childStoreChangeCallbacks[child.getData<string>('componentId')] = child.store.onChangeCallbacks();
310
- child.remove();
311
- });
312
-
313
- const componentData: {
314
- html: string;
315
- initializers: Record<string, string>;
316
- data: LooseObject;
317
- } = JSON.parse(componentDataJSON);
318
-
319
- // populate this.domNode with new HTML
320
- this.domNode.innerHTML = componentData.html;
321
-
322
- // apply new data received from the server as it may have changed
323
- // only exported data is included here
324
- objectEach(componentData.data, (key, val) => {
325
- this.setData(key, val, false);
326
- });
327
-
328
- // add any new initializers to global initializers list
329
- for (const key in componentData.initializers) {
330
- if (!window.initializers[key]) {
331
- window.initializers[key] = componentData.initializers[key];
332
- }
333
- }
334
-
335
- // init new children, restoring their store change listeners in the process
336
- await this.initChildren(this.domNode, (childNew) => {
337
- const childNewId = childNew.getData<string>('componentId');
338
- const existingChild = childNewId in childStoreChangeCallbacks;
339
- if (existingChild) {
340
- // child existed before redraw, re-apply onChange callbacks
341
- objectEach(childStoreChangeCallbacks[childNewId], (key, callbacks) => {
342
- callbacks.forEach((callback) => {
343
- childNew.store.onChange(key, callback);
344
- });
345
- });
346
- }
347
-
348
- // idea was that existing child nodes would be initialized with isRedraw = true
349
- // however after giving it some thought - probably not desirable
350
- // the whole idea with isRedraw is to inform the initializer whether it's
351
- // a fresh instance of ClientComponent or an existing one
352
- // while child (even existing ones) are technically redrawn in this case,
353
- // they do get a fresh instance of a ClientComponent, hence isRedraw = true would be misleading
354
- // keeping this comment here in case in the future a need arises to inform children
355
- // they were redrawn as a consequence of parent redraw
356
- // if ever done, make sure to set 4th argument of initChildren to false (disabling autoInit)
357
- // childNew.init(existingChild);
358
- });
359
-
360
- // re-init conditionals and refs
361
- this.refs = {};
362
- this.refsArray = {};
363
- this.conditionals = [];
364
- this.initRefs();
365
- this.initModels();
366
- this.initConditionals();
367
- this.promoteRefs();
368
-
369
- // run the initializer
370
- if (this.initializer) {
371
- this.initializerExecuted = false;
372
- this.runInitializer(true);
373
- }
374
-
375
- this.updateConditionals(false);
376
-
377
- // mark component as loaded
378
- this.loaded = true;
379
- }
380
-
381
- // populates conditionals and conditionalClassNames
382
- // these react to changes to store data
383
- // to show/hide elements or apply/remove class names to/from them
384
- private initConditionals(node?: HTMLElement): void {
385
- const isSelf = node === undefined;
386
- if (node === undefined) {
387
- node = this.domNode;
388
- }
389
-
390
- for (const attribute of node.attributes) {
391
- // data-if
392
- if (attribute.name === 'data-if') {
393
- this.conditionals.push(node);
394
- }
395
-
396
- // data-classname-[className]
397
- if (attribute.name.startsWith('data-classname')) {
398
- const className = attribute.name.substring(15);
399
- this.conditionalClassNames.push({
400
- element: node,
401
- className
402
- });
403
- }
404
- }
405
-
406
- node.childNodes.forEach((child) => {
407
- if (child.nodeType === 1 && (isSelf || !node?.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`))) {
408
- this.initConditionals(child as HTMLElement);
409
- }
410
- });
411
- }
412
-
413
- // initialize refs and refsArray within this component
414
- // ref="refName"
415
- // any DOM nodes with attribute ref="refName" will be stored under refs as { refName: HTMLElement }
416
- // and can be returned using the ref method, refName should be unique,
417
- // if multiple DOM nodes have the same refName, only the last one will be kept
418
- // if ref is on a component tag, that ref will get promoted to ClientComponent
419
- // array:ref="refName"
420
- // any DOM nodes with attribute array:ref="refName" will be grouped
421
- // under refsArray as { refName: Array<HTMLElement> } and can be returned using refArray method
422
- // in contrast to ref, array:ref's refName does not need to be unique, in fact, since it's used
423
- // to group as set of DOM nodes, it only makes sense if multiple DOM nodes share the same refName
424
- private initRefs(node?: HTMLElement): void {
425
- const isSelf = node === undefined;
426
- if (node === undefined) {
427
- node = this.domNode;
428
- }
429
-
430
- if (node.hasAttribute('ref')) {
431
- this.refs[node.getAttribute('ref') || 'undefined'] = node;
432
- }
433
-
434
- if (node.hasAttribute('array:ref')) {
435
- const key = node.getAttribute('array:ref') || 'undefined';
436
- if (!(key in this.refsArray)) {
437
- this.refsArray[key] = [];
438
- }
439
- this.refsArray[key].push(node);
440
- }
441
-
442
- node.childNodes.forEach((child) => {
443
- if (child.nodeType === 1 && (isSelf || !node?.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`))) {
444
- this.initRefs(child as HTMLElement);
445
- }
446
- });
447
- }
448
-
449
- // make inputs with data-model="field" work
450
- // nested data works too, data-model="obj[nested][key]" or data-model="obj[nested][key][]"
451
- private initModels(node?: HTMLElement, modelNodes: Array<HTMLInputElement> = []) {
452
- const isSelf = node === undefined;
453
- if (node === undefined) {
454
- node = this.domNode;
455
- }
456
-
457
- // given a HTMLInput element that has data-model attribute, returns an object with the data
458
- // for example:
459
- // data-model="name" may result with { name: "John" }
460
- // data-model="user[name]" may result with { user: { name: "John" } }
461
- const modelData = (node: HTMLInputElement): LooseObject => {
462
- const field = node.getAttribute('data-model');
463
- if (field) {
464
- const isCheckbox = node.tagName === 'INPUT' && node.type === 'checkbox';
465
- const valueRaw = isCheckbox ? node.checked : node.value;
466
- const value = queryStringDecodedSetValue(field, valueRaw);
467
- return value;
468
- }
469
- return {}
470
- }
471
-
472
- // given a loose object, sets all keys with corresponding value on current component
473
- const update = (data: LooseObject) => {
474
- objectEach(data, (key, val) => {
475
- this.store.set(key, val);
476
- });
477
- }
478
-
479
- if (node.hasAttribute('data-model') && (node.tagName === 'INPUT' || node.tagName === 'SELECT' || node.tagName === 'TEXTAREA')) {
480
- // found a model node, store to array modelNodes
481
- modelNodes.push(node as HTMLInputElement);
482
- } else {
483
- // not a model, but may contain models
484
- // init model nodes recursively from here
485
- node.childNodes.forEach((child) => {
486
- if (child.nodeType === 1 && (isSelf || !node?.hasAttribute(`data-${window.structuredClientConfig.componentNameAttribute}`))) {
487
- this.initModels(child as HTMLElement, modelNodes);
488
- }
489
- });
490
- }
491
-
492
- if (isSelf) {
493
- // all model nodes are now contained in modelNodes array
494
-
495
- // data for the initial update, we want to gather all data up in one object
496
- // so that nested keys don't trigger more updates than necessary
497
- let data: LooseObject = {}
498
- modelNodes.forEach((modelNode) => {
499
- // on change, update component data
500
- modelNode.addEventListener('input', () => {
501
- let data = modelData(modelNode);
502
- const key = Object.keys(data)[0];
503
- if (typeof data[key] === 'object') {
504
- const dataExisting = this.store.get<LooseObject>(key);
505
- if (dataExisting !== undefined) {
506
- data = mergeDeep({}, {[key]: dataExisting}, data);
507
- } else {
508
- data = mergeDeep({}, data);
509
- }
510
- }
511
- update(data);
512
- });
513
-
514
- // include current node's data into initial update data
515
- const field = modelNode.getAttribute('data-model');
516
- if (field) {
517
- const isCheckbox = modelNode.tagName === 'INPUT' && modelNode.type === 'checkbox';
518
- const valueRaw = isCheckbox ? modelNode.checked : modelNode.value;
519
- const value = queryStringDecodedSetValue(field, valueRaw);
520
- data = mergeDeep(data, value);
521
- }
522
- });
523
-
524
- // run the initial data update with data gathered from all model nodes
525
- update(data);
526
- }
527
- }
528
-
529
- // normally, ref will return a HTMLElement, however if ref attribute is found on a component tag
530
- // this will upgrade it to ClientComponent
531
- private promoteRefs() {
532
- this.children.forEach((child) => {
533
- // promote regular refs
534
- const ref = child.domNode.getAttribute('ref');
535
- if (ref) {
536
- this.refs[ref] = child;
537
- }
538
-
539
- // promote array refs
540
- const refArray = child.domNode.getAttribute('array:ref');
541
- if (refArray !== null && refArray in this.refsArray) {
542
- const nodeIndex = this.refsArray[refArray].indexOf(child.domNode);
543
- if (nodeIndex > -1) {
544
- this.refsArray[refArray].splice(nodeIndex, 1, child);
545
- }
546
- }
547
- });
548
- }
549
-
550
- // returns a single HTMLElement or ClientComponent that has ref="refName" attribute
551
- // if ref attribute is on a component tag, the ref will be promoted to ClientComponent
552
- // in other cases it returns the HTMLElement
553
- // this does not check if the ref exists
554
- // you should make sure it does, otherwise you will get undefined at runtime
555
- public ref<T>(refName: string): T {
556
- return this.refs[refName] as T;
557
- }
558
-
559
- // returns an array of HTMLElement (type of the elements can be specified) that have array:ref="refName"
560
- public refArray<T>(refName: string): Array<T> {
561
- return (this.refsArray[refName] || []) as Array<T>;
562
- }
563
-
564
- // condition can be one of:
565
- // 1) access to a boolean property in component store: [key]
566
- // 2) comparison of a store property [key] ==|===|!=|<|>|<=|>= [comparison value or key]
567
- // 3) method methodName() or methodName(arg)
568
- private execCondition(conditionRaw: string): boolean {
569
- const condition = conditionRaw.trim();
570
- const isMethod = condition.endsWith(')');
571
-
572
- if (isMethod) {
573
- // method (case 3)
574
- // method has to be in format !?[a-zA-Z]+[a-zA-Z0-9_]+\([^)]+\)
575
- // extract expression parts
576
- const parts = /^(!?)\s*([a-zA-Z]+[a-zA-Z0-9_]*)\(([^)]*)\)$/.exec(condition);
577
- if (parts === null) {
578
- console.error(`Could not parse condition ${condition}`);
579
- return false;
580
- }
581
- const negated = parts[1] === '!';
582
- const functionName = parts[2];
583
- const args = parts[3].trim();
584
-
585
- // make sure there is a registered callback with this name
586
- if (typeof this.conditionalCallbacks[functionName] !== 'function') {
587
- console.warn(`No registered conditional callback '${functionName}'`);
588
- return false;
589
- }
590
-
591
- // run registered callback
592
- const isTrue = this.conditionalCallbacks[functionName](args === '' ? undefined : eval(`(${args})`));
593
- if (negated) {
594
- return ! isTrue;
595
- }
596
- return isTrue;
597
- } else {
598
- // expression not a method
599
- const parts = /^(!)?\s*([a-zA-Z]+[a-zA-Z0-9_]*)\s*((?:==)|(?:===)|(?:!=)|(?:!==)|<|>|(?:<=)|(?:>=))?\s*([^=].+)?$/.exec(condition);
600
- if (parts === null) {
601
- console.error(`Could not parse condition ${condition}`);
602
- return false;
603
- }
604
-
605
- const property = parts[2];
606
- const value = this.store.get(property);
607
- const isComparison = parts[3] !== undefined;
608
- if (isComparison) {
609
- // comparison (case 2)
610
- // left hand side is the property name, right hand side is an expression
611
- let rightHandSide = null;
612
- try {
613
- // this won't fail as long as parts[4] is a recognized primitive (number, boolean, string...)
614
- rightHandSide = eval(`${parts[4]}`);
615
- } catch(e) {
616
- // parts[4] failed to be parsed as a primitive
617
- // assume it's a store value to allow comparing one store value to another
618
- rightHandSide = this.store.get(parts[4]);
619
- }
620
-
621
- const comparisonSymbol = parts[3];
622
-
623
- if (comparisonSymbol === '==') {
624
- return value == rightHandSide;
625
- } else if (comparisonSymbol === '===') {
626
- return value === rightHandSide;
627
- } else if (comparisonSymbol === '!=') {
628
- return value != rightHandSide;
629
- } else if (comparisonSymbol === '!==') {
630
- return value !== rightHandSide;
631
- } else {
632
- // number comparison
633
- if (typeof value !== 'number') {
634
- // if value is not a number, these comparisons makes no sense, return false
635
- return false;
636
- }
637
- if (comparisonSymbol === '>') {
638
- return value > rightHandSide;
639
- } else if (comparisonSymbol === '>=') {
640
- return value >= rightHandSide;
641
- } else if (comparisonSymbol === '<') {
642
- return value < rightHandSide;
643
- } else if (comparisonSymbol === '<=') {
644
- return value <= rightHandSide;
645
- }
646
- }
647
-
648
- return false;
649
-
650
- } else {
651
- // not a comparison (case 1)
652
- const negated = parts[1] === '!';
653
- const isTrue = this.store.get<boolean>(property);
654
- if (negated) {
655
- return !isTrue;
656
- }
657
- // value may not be a boolean, coerce to boolean without changing value
658
- return !!isTrue;
659
- }
660
- }
661
- }
662
-
663
- // conditionals (data-if and data-classname-[className]) both support methods in condition
664
- // eg. data-if="someMethod()"
665
- // this allows users to define callbacks used in conditionals' conditions
666
- // by default, this also runs updateConditionals
667
- // as there might be conditionals that are using this callback
668
- public conditionalCallback(name: string, callback: (args?: any) => boolean, updateConditionals: boolean = true): void {
669
- this.conditionalCallbacks[name] = callback;
670
- if (updateConditionals) {
671
- this.updateConditionals(false);
672
- }
673
- }
674
-
675
- // updates conditionals (data-if and data-classname-[className])
676
- // data-if (conditionally show/hide DOM node)
677
- // data-classname-[className] (conditionally add className to classList of the DOM node)
678
- private updateConditionals(enableTransition: boolean) {
679
- if (this.destroyed) {return;}
680
-
681
- // data-if conditions
682
- this.conditionals.forEach((node) => {
683
- const condition = node.getAttribute('data-if');
684
-
685
- if (typeof condition === 'string') {
686
- const show = this.execCondition(condition);
687
-
688
- if (show === true) {
689
- // node.style.display = '';
690
- this.show(node, enableTransition);
691
- } else {
692
- // node.style.display = 'none';
693
- this.hide(node, enableTransition);
694
- }
695
- }
696
- });
697
-
698
- // data-classname conditions
699
- this.conditionalClassNames.forEach((conditional) => {
700
- const condition = conditional.element.getAttribute(`data-classname-${conditional.className}`);
701
-
702
- if (typeof condition === 'string') {
703
- const enableClassName = this.execCondition(condition);
704
-
705
- if (enableClassName === true) {
706
- conditional.element.classList.add(conditional.className);
707
- } else {
708
- conditional.element.classList.remove(conditional.className);
709
- }
710
- }
711
- });
712
- }
713
-
714
- // remove the DOM node and delete from parent.children effectively removing self from the tree
715
- // the method could be sync, but since we want to allow for potentially async user destructors
716
- // it is async
717
- public async remove(): Promise<void> {
718
- if (!this.isRoot) {
719
- // remove children recursively
720
- const children = Array.from(this.children);
721
- for (let i = 0; i < children.length; i++) {
722
- await children[i].remove();
723
- }
724
-
725
- // remove from parent's children array
726
- if (this.parent) {
727
- this.parent.children.splice(this.parent.children.indexOf(this), 1);
728
- }
729
-
730
- // remove DOM node
731
- this.domNode.parentElement?.removeChild(this.domNode);
732
- await this.destroy();
733
-
734
- }
735
- }
736
-
737
- // travel up the tree until a parent with given parentName is found
738
- // if no such parent is found returns null
739
- public parentFind(parentName: string): ClientComponent | null {
740
- let parent = this.parent;
741
- while (true) {
742
- if (parent.name === parentName) {
743
- return parent;
744
- }
745
-
746
- if (parent.isRoot) {
747
- break;
748
- }
749
-
750
- parent = parent.parent;
751
- }
752
-
753
- return null;
754
- }
755
-
756
- // find a component with given name within this component
757
- // if recursive = true, it searches recursively
758
- // returns the first found component or null if no components were found
759
- public find(componentName: string, recursive: boolean = true): null | ClientComponent {
760
- for (let i = 0; i < this.children.length; i++) {
761
- const child = this.children[i];
762
- if (child.name == componentName) {
763
- // found it
764
- return child;
765
- } else {
766
- if (recursive) {
767
- // search recursively, if found return
768
- const inChild = child.find(componentName, recursive);
769
- if (inChild) {
770
- return inChild;
771
- }
772
- }
773
- }
774
- }
775
- return null;
776
- }
777
-
778
- // find all components with given name within this component
779
- // if recursive = true, it searches recursively
780
- // returns an array of found components
781
- public query(componentName: string, recursive: boolean = true, results: Array<ClientComponent> = []): Array<ClientComponent> {
782
- for (let i = 0; i < this.children.length; i++) {
783
- const child = this.children[i];
784
- if (child.name == componentName) {
785
- // found a component with name = componentName, add to results
786
- results.push(child);
787
- } else {
788
- if (recursive) {
789
- // search recursively, if found return
790
- child.query(componentName, recursive, results);
791
- }
792
- }
793
- }
794
- return results;
795
- }
796
-
797
- // adds a new component to DOM/component tree
798
- // appendTo is a selector within this component's DOM or a HTMLElement (which can be outside this component)
799
- // data can be an object which is passed to added component
800
- // regardless whether appendTo is within this component or not,
801
- // added component will always be a child of this component
802
- // returns a promise that resolves with the added component
803
- public async add(appendTo: string | HTMLElement, componentName: string, data?: LooseObject): Promise<ClientComponent | null> {
804
- if (window.structuredClientConfig.componentRender === false) {
805
- console.error(`Can't add component, component rendering URL disabled`);
806
- return null;
807
- }
808
-
809
- const container = typeof appendTo === 'string' ? this.domNode.querySelector(appendTo) : appendTo;
810
-
811
- if (! (container instanceof HTMLElement)) {
812
- throw new Error(`${this.name}.add() - appendTo selector not found within this component`);
813
- }
814
-
815
- // request rendered component from the server
816
- // expected result is JSON, containing { html, initializers, data }
817
- // unwrap set to false as we want the component container to be returned (unlike redraw)
818
- const req = new NetRequest('POST', window.structuredClientConfig.componentRender, {
819
- 'content-type': 'application/json'
820
- });
821
- const componentDataJSON = await req.send(JSON.stringify({
822
- component: componentName,
823
- attributes: data,
824
- unwrap: false
825
- }));
826
-
827
- const res: {
828
- html: string;
829
- initializers: Record<string, string>;
830
- data: LooseObject;
831
- } = JSON.parse(componentDataJSON);
832
-
833
- // if the current document did not include the added component (or components loaded within it)
834
- // it's initializer will not be present in window.initializers
835
- // add any missing initializers to global initializers list
836
- for (let key in res.initializers) {
837
- if (!window.initializers[key]) {
838
- window.initializers[key] = res.initializers[key];
839
- }
840
- }
841
-
842
- // create a temporary container to load the returned HTML into
843
- const tmpContainer = document.createElement('div');
844
- tmpContainer.innerHTML = res.html;
845
-
846
- // get the first child, which is always the component wrapper <div data-structured-component="..."></div>
847
- const componentNode = tmpContainer.firstChild as HTMLElement;
848
-
849
- // create an instance of ClientComponent for the added component and add it to this.children
850
- const component = new ClientComponent(this, componentName, componentNode, this.storeGlobal);
851
- this.children.push(component);
852
- await component.init(true);
853
-
854
- // add the component's DOM node to container
855
- container.appendChild(componentNode);
856
-
857
- return component;
858
- }
859
-
860
- public getData<T>(key?: string): T {
861
- if (!key) {
862
- return this.data as T;
863
- }
864
- return this.data[key] as T;
865
- }
866
-
867
- // shows a previously hidden DOM node (domNode.style.display = '')
868
- // if the DOM node has data-transition attributes, it will run the transition while showing the node
869
- public show(domNode: HTMLElement, enableTransition: boolean = true): void {
870
- if (!enableTransition) {
871
- domNode.style.display = '';
872
- return;
873
- }
874
-
875
- if (domNode.style.display !== 'none') { return; }
876
-
877
- // const transitions = this.transitions.show;
878
- const transitions = this.transitionAttributes(domNode).show;
879
-
880
- const transitionsActive = Object.keys(transitions).filter((key: keyof ClientComponentTransition) => {
881
- return transitions[key] !== false;
882
- }).reduce((prev, curr) => {
883
- const key = curr as keyof ClientComponentTransition;
884
- prev[key] = transitions[key];
885
- return prev;
886
- }, {} as {
887
- [key in keyof ClientComponentTransition]: false | number;
888
- });
889
-
890
- if (Object.keys(transitionsActive).length === 0) {
891
- domNode.style.display = '';
892
- return;
893
- }
894
-
895
- domNode.style.display = '';
896
-
897
- const onTransitionEnd = (e: any) => {
898
- domNode.style.opacity = '1';
899
- domNode.style.transition = '';
900
- domNode.style.transformOrigin = 'unset';
901
- domNode.removeEventListener('transitionend', onTransitionEnd);
902
- domNode.removeEventListener('transitioncancel', onTransitionEnd);
903
- };
904
-
905
- domNode.addEventListener('transitionend', onTransitionEnd);
906
- domNode.addEventListener('transitioncancel', onTransitionEnd);
907
-
908
- if (transitionsActive.slide) {
909
-
910
- // if specified use given transformOrigin
911
- const transformOrigin = domNode.getAttribute('data-transform-origin-show') || '50% 0';
912
-
913
- domNode.style.transformOrigin = transformOrigin;
914
- const axis = this.transitionAxis(domNode, 'show');
915
- domNode.style.transform = `scale${axis}(0.01)`;
916
- domNode.style.transition = `transform ${transitionsActive.slide / 1000}s`;
917
- setTimeout(() => {
918
- // domNode.style.height = height + 'px';
919
- domNode.style.transform = `scale${axis}(1)`;
920
- }, 100);
921
- }
922
-
923
- if (transitionsActive.fade) {
924
- domNode.style.opacity = '0';
925
- domNode.style.transition = `opacity ${transitionsActive.fade / 1000}s`;
926
- setTimeout(() => {
927
- domNode.style.opacity = '1';
928
- }, 100);
929
- }
930
-
931
- }
932
-
933
- // hides the given DOM node (domNode.style.display = 'none')
934
- // if the DOM node has data-transition attributes, it will run the transition before hiding the node
935
- public hide(domNode: HTMLElement, enableTransition: boolean = true): void {
936
- if (!enableTransition) {
937
- domNode.style.display = 'none';
938
- return;
939
- }
940
-
941
- if (domNode.style.display === 'none') { return; }
942
-
943
- // const transitions = this.transitions.hide;
944
- const transitions = this.transitionAttributes(domNode).hide;
945
-
946
- const transitionsActive = Object.keys(transitions).filter((key: keyof ClientComponentTransition) => {
947
- return transitions[key] !== false;
948
- }).reduce((prev, curr) => {
949
- const key = curr as keyof ClientComponentTransition;
950
- prev[key] = transitions[key];
951
- return prev;
952
- }, {} as {
953
- [key in keyof ClientComponentTransition]: false | number;
954
- });
955
-
956
- if (Object.keys(transitionsActive).length === 0) {
957
- // no transitions
958
- domNode.style.display = 'none';
959
- } else {
960
-
961
- const onTransitionEnd = (e: any) => {
962
- domNode.style.display = 'none';
963
- domNode.style.opacity = '1';
964
- domNode.style.transition = '';
965
- domNode.style.transformOrigin = 'unset';
966
- domNode.removeEventListener('transitionend', onTransitionEnd);
967
- domNode.removeEventListener('transitioncancel', onTransitionEnd);
968
- };
969
-
970
- domNode.addEventListener('transitionend', onTransitionEnd);
971
- domNode.addEventListener('transitioncancel', onTransitionEnd);
972
-
973
- if (transitionsActive.slide) {
974
- // domNode.style.overflowY = 'hidden';
975
- // domNode.style.height = domNode.clientHeight + 'px';
976
- // if specified use given transformOrigin
977
- const transformOrigin = domNode.getAttribute('data-transform-origin-hide') || '50% 100%';
978
-
979
- domNode.style.transformOrigin = transformOrigin;
980
- domNode.style.transition = `transform ${transitionsActive.slide / 1000}s ease`;
981
- setTimeout(() => {
982
- // domNode.style.height = '2px';
983
- const axis = this.transitionAxis(domNode, 'hide');
984
- domNode.style.transform = `scale${axis}(0.01)`;
985
- }, 100);
986
- }
987
-
988
- if (transitionsActive.fade) {
989
- domNode.style.opacity = '1';
990
- domNode.style.transition = `opacity ${transitionsActive.fade / 1000}s`;
991
- setTimeout(() => {
992
- domNode.style.opacity = '0';
993
- }, 100);
994
- }
995
-
996
- }
997
-
998
- }
999
-
1000
- // reads attribute values of
1001
- // data-transition-show-slide, data-transition-show-fade,
1002
- // data-transition-hide-slide, data-transition-hide-fade
1003
- // and parses them into ClientComponentTransitions object
1004
- private transitionAttributes(domNode: HTMLElement): ClientComponentTransitions {
1005
- const transitions: ClientComponentTransitions = {
1006
- show: {
1007
- slide: false,
1008
- fade: false
1009
- },
1010
- hide: {
1011
- slide: false,
1012
- fade: false
1013
- }
1014
- };
1015
-
1016
- objectEach(transitions, (transitionEvent, transition) => {
1017
- objectEach(transition, (transitionType) => {
1018
- const attributeName = `data-transition-${transitionEvent}-${transitionType}`;
1019
- if (domNode.hasAttribute(attributeName)) {
1020
- const valueRaw = domNode.getAttribute(attributeName);
1021
- let value: number | false = false;
1022
- if (typeof valueRaw === 'string' && /^\d+$/.test(valueRaw)) {
1023
- value = parseInt(valueRaw);
1024
- }
1025
- transition[transitionType] = value;
1026
- }
1027
- });
1028
- });
1029
-
1030
- return transitions;
1031
- }
1032
-
1033
- // reads data-transition-axis-[show|hide] of given domNode
1034
- // returns "" if attribute is missing or has an unrecognized value
1035
- // return "X" or "Y" if a proper value is found
1036
- private transitionAxis(domNode: HTMLElement, showHide: 'show' | 'hide'): 'X' | 'Y' | '' {
1037
- const attributeName = `data-transition-axis-${showHide}`;
1038
- if (! domNode.hasAttribute(attributeName)) {return '';}
1039
- let val = domNode.getAttribute(attributeName);
1040
- if (typeof val === 'string') {
1041
- val = val.trim().toUpperCase();
1042
- if (val.length > 0) {
1043
- val = val.substring(0, 1);
1044
- }
1045
-
1046
- if (val != 'X' && val != 'Y') {
1047
- // unrecognized value
1048
- return '';
1049
- }
1050
-
1051
- return val;
1052
- }
1053
- return '';
1054
- }
1055
-
1056
- private async destroy(): Promise<void> {
1057
-
1058
- // if being redrawn, abort redraw request
1059
- if (this.redrawRequest) {
1060
- this.redrawRequest.abort();
1061
- this.redrawRequest = null;
1062
- }
1063
-
1064
- // if the user has defined a destroy callback, run it
1065
- if (typeof this.onDestroy === 'function') {
1066
- await this.onDestroy.apply(this);
1067
- }
1068
-
1069
- this.store.destroy();
1070
-
1071
- // remove all event listeners attached to DOM elements
1072
- this.unbindAll();
1073
-
1074
- // clean up and free memory
1075
- this.conditionals = [];
1076
- this.conditionalClassNames = [];
1077
- this.conditionalCallbacks = {};
1078
- this.refs = {};
1079
- this.refsArray = {};
1080
- this.initializer = null;
1081
- this.data = {};
1082
-
1083
- // mark destroyed
1084
- this.destroyed = true;
1085
- }
1086
-
1087
- // add an event listener to given DOM node
1088
- // stores it to ClientComponent.bound so it can be unbound when needed using unbindAll
1089
- public bind(element: HTMLElement, event: string, callback: (e: Event) => void): void {
1090
- if (element instanceof HTMLElement) {
1091
- this.bound.push({
1092
- element,
1093
- event,
1094
- callback
1095
- });
1096
- element.addEventListener(event, callback);
1097
- }
1098
- }
1099
-
1100
- // remove all bound event listeners using ClientComponent.bind
1101
- private unbindAll() {
1102
- this.bound.forEach((bound) => {
1103
- bound.element.removeEventListener(bound.event, bound.callback);
1104
- });
1105
- this.bound = [];
1106
- }
1107
- }