@jinntec/fore 1.0.0-0 → 1.0.0-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fx-fore.js CHANGED
@@ -4,24 +4,34 @@ import './fx-model.js';
4
4
  import '@jinntec/jinn-toast';
5
5
  import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
6
6
  import getInScopeContext from './getInScopeContext.js';
7
+ import { XPathUtil } from './xpath-util.js';
7
8
 
8
9
  /**
9
10
  * Main class for Fore.Outermost container element for each Fore application.
10
11
  *
11
- *
12
- *
13
12
  * Root element for Fore. Kicks off initialization and displays messages.
14
13
  *
15
14
  * fx-fore is the outermost container for each form. A form can have exactly one model
16
15
  * with arbitrary number of instances.
17
16
  *
18
- * Main responsiblities are initialization of model, update of UI (refresh) and global messaging
17
+ * Main responsiblities are initialization and updating of model and instances, update of UI (refresh) and global messaging.
18
+ *
19
+ *
19
20
  *
20
21
  * @ts-check
21
22
  */
22
23
  export class FxFore extends HTMLElement {
23
24
  static get properties() {
24
25
  return {
26
+ /**
27
+ * Setting this marker attribute will refresh the UI in a lazy fashion just updating elements being
28
+ * in viewport.
29
+ *
30
+ * this feature is still experimental and should be used with caution and extra testing
31
+ */
32
+ lazyRefresh: {
33
+ type: Boolean,
34
+ },
25
35
  model: {
26
36
  type: Object,
27
37
  },
@@ -31,6 +41,14 @@ export class FxFore extends HTMLElement {
31
41
  };
32
42
  }
33
43
 
44
+ /**
45
+ * attaches handlers for
46
+ *
47
+ * - `model-construct-done` to trigger the processing of the UI
48
+ * - `message` - to display a message triggered by an fx-message action
49
+ * - `error` - to display an error message
50
+ * - 'compute-exception` - warn about circular dependencies in graph
51
+ */
34
52
  constructor() {
35
53
  super();
36
54
  this.model = {};
@@ -42,12 +60,11 @@ export class FxFore extends HTMLElement {
42
60
  });
43
61
 
44
62
  this.ready = false;
45
-
46
63
  this.storedTemplateExpressionByNode = new Map();
47
64
 
48
65
  const style = `
49
66
  :host {
50
- display: block;
67
+ // display: none;
51
68
  height:auto;
52
69
  padding:var(--model-element-padding);
53
70
  font-family:Roboto, sans-serif;
@@ -56,6 +73,11 @@ export class FxFore extends HTMLElement {
56
73
  :host ::slotted(fx-model){
57
74
  display:none;
58
75
  }
76
+ :host(.fx-ready){
77
+ animation: fadein .4s forwards;
78
+ display:block;
79
+ }
80
+
59
81
  #modalMessage .dialogActions{
60
82
  text-align:center;
61
83
  }
@@ -118,6 +140,14 @@ export class FxFore extends HTMLElement {
118
140
  #messageContent{
119
141
  margin-top:40px;
120
142
  }
143
+ @keyframes fadein {
144
+ 0% {
145
+ opacity:0;
146
+ }
147
+ 100% {
148
+ opacity:1;
149
+ }
150
+ }
121
151
  `;
122
152
 
123
153
  const html = `
@@ -140,9 +170,29 @@ export class FxFore extends HTMLElement {
140
170
  </style>
141
171
  ${html}
142
172
  `;
173
+
174
+ this.toRefresh = [];
175
+ this.initialRun = true;
176
+ this.someInstanceDataStructureChanged = false;
143
177
  }
144
178
 
145
179
  connectedCallback() {
180
+ this.lazyRefresh = this.hasAttribute('refresh-on-view');
181
+ if (this.lazyRefresh) {
182
+ const options = {
183
+ root: null,
184
+ rootMargin: '0px',
185
+ threshold: 0.3,
186
+ };
187
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
188
+ }
189
+
190
+ this.src = this.hasAttribute('src')? this.getAttribute('src'):null;
191
+ if(this.src){
192
+ this._loadFromSrc();
193
+ return ;
194
+ }
195
+
146
196
  const slot = this.shadowRoot.querySelector('slot');
147
197
  slot.addEventListener('slotchange', event => {
148
198
  const children = event.target.assignedElements();
@@ -155,18 +205,123 @@ export class FxFore extends HTMLElement {
155
205
  modelElement = generatedModel;
156
206
  }
157
207
  if (!modelElement.inited) {
158
- console.log('########## FORE: kick off processing... ##########');
208
+ console.log(
209
+ `########## FORE: kick off processing for ... ${window.location.href} ##########`,
210
+ );
211
+ if(this.src){
212
+ console.log('########## FORE: loaded from ... ', this.src, '##########');
213
+ }
159
214
  modelElement.modelConstruct();
160
215
  }
161
216
  this.model = modelElement;
162
217
  });
218
+ this.addEventListener('path-mutated', (e) =>{
219
+ console.log('path-mutated event received', e.detail.path, e.detail.index);
220
+ this.someInstanceDataStructureChanged = true;
221
+ });
222
+ }
223
+
224
+
225
+ addToRefresh(modelItem){
226
+ const found = this.toRefresh.find(mi => mi.path === modelItem.path );
227
+ if(!found){
228
+ this.toRefresh.push(modelItem);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * loads a Fore from an URL given by `src`.
234
+ *
235
+ * Will extract the `fx-fore` element from that target file and use and replace current `fx-fore` element with the loaded one.
236
+ * @private
237
+ */
238
+ _loadFromSrc() {
239
+ console.log('########## loading Fore from ',this.src ,'##########');
240
+ fetch(this.src, {
241
+ method: 'GET',
242
+ mode: 'cors',
243
+ credentials: 'include',
244
+ headers: {
245
+ 'Content-Type': 'text/html',
246
+ },
247
+ })
248
+ .then(response => {
249
+ const responseContentType = response.headers.get('content-type').toLowerCase();
250
+ console.log('********** responseContentType *********', responseContentType);
251
+ if (responseContentType.startsWith('text/html')) {
252
+ // const htmlResponse = response.text();
253
+ // return new DOMParser().parseFromString(htmlResponse, 'text/html');
254
+ // return response.text();
255
+ return response.text().then(result =>
256
+ // console.log('xml ********', result);
257
+ new DOMParser().parseFromString(result, 'text/html'),
258
+ );
259
+ }
260
+ return 'done';
261
+ })
262
+ .then(data => {
263
+ // const theFore = fxEvaluateXPathToFirstNode('//fx-fore', data.firstElementChild);
264
+ const theFore = data.querySelector('fx-fore');
265
+
266
+ // console.log('thefore', theFore)
267
+ if(!theFore){
268
+ this.dispatchEvent(new CustomEvent('error',{detail:{message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`}}));
269
+ }
270
+ theFore.setAttribute('from-src', this.src);
271
+ this.replaceWith(theFore);
272
+ })
273
+ .catch(error => {
274
+ this.dispatchEvent(new CustomEvent('error',{detail:{message: `'${this.src}' not found or does not contain Fore element.`}}));
275
+ });
276
+ }
277
+
278
+ /**
279
+ * refreshes the UI by using IntersectionObserver API. This is the handler being called
280
+ * by the observer whenever elements come into / move out of viewport.
281
+ * @param entries
282
+ * @param observer
283
+ */
284
+ handleIntersect(entries, observer) {
285
+ console.time('refreshLazy');
286
+ entries.forEach(entry => {
287
+ const { target } = entry;
288
+
289
+ if (entry.isIntersecting) {
290
+ console.log('in view', entry);
291
+ // console.log('repeat in view entry', entry.target);
292
+ // const target = entry.target;
293
+ // if(target.hasAttribute('refresh-on-view')){
294
+ target.classList.add('loaded');
295
+ // }
296
+
297
+ // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
298
+ if (typeof target.refresh === 'function') {
299
+ console.log('refreshing target', target);
300
+ target.refresh(target, true);
301
+ } else {
302
+ console.log('refreshing children', target);
303
+ Fore.refreshChildren(target, true);
304
+ }
305
+ }
306
+ });
307
+ entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
308
+
309
+ console.timeEnd('refreshLazy');
163
310
  }
164
311
 
165
312
  evaluateToNodes(xpath, context) {
166
313
  return evaluateXPathToNodes(xpath, context, this);
167
314
  }
168
315
 
169
- disconnectedCallback() {}
316
+ disconnectedCallback() {
317
+ /*
318
+ this.removeEventListener('model-construct-done', this._handleModelConstructDone);
319
+ this.removeEventListener('message', this._displayMessage);
320
+ this.removeEventListener('error', this._displayError);
321
+ this.storedTemplateExpressionByNode=null;
322
+ this.shadowRoot = undefined;
323
+ */
324
+ }
170
325
 
171
326
  /**
172
327
  * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
@@ -176,16 +331,63 @@ export class FxFore extends HTMLElement {
176
331
  * AVT:
177
332
  *
178
333
  */
179
- async refresh() {
334
+ async refresh(force) {
180
335
  // refresh () {
181
336
  console.group('### refresh');
182
- // await this.updateComplete;
183
337
 
184
- Fore.refreshChildren(this);
185
- // this.dispatchEvent(new CustomEvent('refresh-done', {detail:'foo'}));
338
+ console.time('refresh');
339
+
340
+ // ### refresh Fore UI elements
341
+ console.time('refreshChildren');
342
+ console.log('toRefresh',this.toRefresh);
343
+
344
+ if(!this.initialRun && this.toRefresh.length !== 0){
345
+ let needsRefresh = false;
346
+
347
+ // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
348
+ this.toRefresh.forEach(modelItem => {
349
+ // check if modelItem has boundControls - if so, call refresh() for each of them
350
+ const controlsToRefresh = modelItem.boundControls;
351
+ if(controlsToRefresh){
352
+ controlsToRefresh.forEach(ctrl => {
353
+ ctrl.refresh();
354
+ });
355
+ }
356
+
357
+ // ### check if other controls depend on current modelItem
358
+ const mainGraph = this.getModel().mainGraph;
359
+ if(mainGraph && mainGraph.hasNode(modelItem.path)){
360
+ const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
361
+ // ### iterate dependant modelItems and refresh all their boundControls
362
+ if(deps.length !== 0){
363
+ deps.forEach(dep => {
364
+ // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
365
+ const basePath = XPathUtil.getBasePath(dep);
366
+ const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
367
+ // ### refresh all boundControls
368
+ modelItemOfDep.boundControls.forEach(control =>{control.refresh()});
369
+ });
370
+ needsRefresh = true;
371
+ }
372
+ }
373
+ });
374
+ this.toRefresh = [];
375
+ if(!needsRefresh){
376
+ console.log('skipping refresh - no dependants');
377
+ }
378
+ }else{
379
+ Fore.refreshChildren(this, true);
380
+ console.timeEnd('refreshChildren');
381
+ }
186
382
 
187
383
  // ### refresh template expressions
188
- this._updateTemplateExpressions();
384
+ if(this.initialRun || this.someInstanceDataStructureChanged){
385
+ this._updateTemplateExpressions();
386
+ this.someInstanceDataStructureChanged = false; //reset
387
+ }
388
+ this._processTemplateExpressions();
389
+
390
+ console.timeEnd('refresh');
189
391
 
190
392
  console.groupEnd();
191
393
  console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
@@ -203,14 +405,20 @@ export class FxFore extends HTMLElement {
203
405
  _updateTemplateExpressions() {
204
406
  // Note the fact we're going over HTML here: therefore the `html` prefix.
205
407
  const search =
206
- "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::xhtml:fx-model/descendant-or-self::node()/(., @*)";
408
+ "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::fx-model/descendant-or-self::node()/(., @*)";
207
409
 
208
410
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
209
411
  console.log('template expressions found ', tmplExpressions);
210
412
 
413
+ if (!this.storedTemplateExpressions) {
414
+ this.storedTemplateExpressions = [];
415
+ }
416
+
417
+ console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
418
+
211
419
  /*
212
- storing expressions and their nodes for re-evaluation
213
- */
420
+ storing expressions and their nodes for re-evaluation
421
+ */
214
422
  Array.from(tmplExpressions).forEach(node => {
215
423
  if (this.storedTemplateExpressionByNode.has(node)) {
216
424
  // If the node is already known, do not process it twice
@@ -218,21 +426,26 @@ export class FxFore extends HTMLElement {
218
426
  }
219
427
  const expr = this._getTemplateExpression(node);
220
428
 
429
+ // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
221
430
  this.storedTemplateExpressionByNode.set(node, expr);
222
431
  });
432
+ console.log('stored template expressions ', this.storedTemplateExpressionByNode);
223
433
 
224
434
  // TODO: Should we clean up nodes that existed but are now gone?
435
+ this._processTemplateExpressions();
436
+
437
+ }
438
+
439
+ _processTemplateExpressions() {
225
440
  for (const node of this.storedTemplateExpressionByNode.keys()) {
226
441
  this._processTemplateExpression({
227
442
  node,
228
443
  expr: this.storedTemplateExpressionByNode.get(node),
229
444
  });
230
445
  }
231
-
232
- console.log('stored template expressions ', this.storedTemplateExpressionByNode);
233
446
  }
234
447
 
235
- // eslint-disable-next-line class-methods-use-this
448
+ // eslint-disable-next-line class-methods-use-this
236
449
  _processTemplateExpression(exprObj) {
237
450
  // console.log('processing template expression ', exprObj);
238
451
 
@@ -249,13 +462,14 @@ export class FxFore extends HTMLElement {
249
462
  * @param form the form element
250
463
  */
251
464
  evaluateTemplateExpression(expr, node) {
465
+ if (expr === '{}') return;
252
466
  const matches = expr.match(/{[^}]*}/g);
253
467
  const namespaceContextNode =
254
468
  node.nodeType === node.TEXT_NODE ? node.parentNode : node.ownerElement;
255
469
  if (matches) {
256
470
  matches.forEach(match => {
257
471
  // console.log('match ', match);
258
- const naked = match.substring(1, match.length - 1);
472
+ let naked = match.substring(1, match.length - 1);
259
473
  const inscope = getInScopeContext(node, naked);
260
474
  if (!inscope) {
261
475
  const errNode =
@@ -267,13 +481,15 @@ export class FxFore extends HTMLElement {
267
481
  }
268
482
  // Templates are special: they use the namespace configuration from the place where they are
269
483
  // being defined
270
-
484
+ const instanceId = XPathUtil.getInstanceId(naked);
485
+ // console.log('target instance ', instanceId);
486
+ const inst = this.getModel().getInstance(instanceId);
271
487
  try {
272
- const result = evaluateXPathToString(naked, inscope, this, null, namespaceContextNode);
488
+ const result = evaluateXPathToString(naked, inscope, node, null, inst);
273
489
 
274
490
  // console.log('result of eval ', result);
275
491
  const replaced = expr.replaceAll(match, result);
276
- console.log('result of replacing ', replaced);
492
+ // console.log('result of replacing ', replaced);
277
493
 
278
494
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
279
495
  const parent = node.ownerElement;
@@ -283,6 +499,14 @@ export class FxFore extends HTMLElement {
283
499
  } else if (node.nodeType === Node.TEXT_NODE) {
284
500
  node.textContent = replaced;
285
501
  }
502
+
503
+ if (replaced.includes('{')) {
504
+ // console.log('need to go next round');
505
+
506
+ // todo: duplicated code here - see above
507
+ naked = replaced.substring(1, replaced.length);
508
+ this.evaluateTemplateExpression(replaced, node);
509
+ }
286
510
  } catch (error) {
287
511
  this.dispatchEvent(new CustomEvent('error', { detail: error }));
288
512
  }
@@ -296,25 +520,28 @@ export class FxFore extends HTMLElement {
296
520
  return node.value;
297
521
  }
298
522
  if (node.nodeType === Node.TEXT_NODE) {
299
- return node.textContent;
523
+ return node.textContent.trim();
300
524
  }
301
525
  return null;
302
526
  }
303
527
 
304
- _refreshChildren() {
305
- const uiElements = this.querySelectorAll('*');
306
-
307
- uiElements.forEach(element => {
308
- if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
309
- element.refresh();
310
- }
311
- });
312
- }
313
-
528
+ /**
529
+ * called when `model-construct-done` event is received to
530
+ * start initing of the UI.
531
+ *
532
+ * @private
533
+ */
314
534
  _handleModelConstructDone() {
315
535
  this._initUI();
316
536
  }
317
537
 
538
+ /**
539
+ * If there's no instance element found in a fx-model during init it will construct
540
+ * an instance from UI bindings.
541
+ *
542
+ * @returns {Promise<void>}
543
+ * @private
544
+ */
318
545
  async _lazyCreateInstance() {
319
546
  const model = this.querySelector('fx-model');
320
547
  if (model.instances.length === 0) {
@@ -413,18 +640,64 @@ export class FxFore extends HTMLElement {
413
640
  }
414
641
  */
415
642
 
643
+ /**
644
+ * Start the initialization of the UI by
645
+ *
646
+ * 1. checking if a instance needs to be generated
647
+ * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
648
+ * 3. doing a full refresh of the UI
649
+ *
650
+ * @returns {Promise<void>}
651
+ * @private
652
+ */
416
653
  async _initUI() {
417
654
  console.log('### _initUI()');
418
655
 
419
656
  await this._lazyCreateInstance();
657
+
658
+ console.log('registering variables!');
659
+ const variables = new Map();
660
+ (function registerVariables(node) {
661
+ for (const child of node.children) {
662
+ if ('setInScopeVariables' in child) {
663
+ child.setInScopeVariables(variables);
664
+ }
665
+ registerVariables(child);
666
+ }
667
+ })(this);
668
+ console.log('Found variables:', variables);
669
+
670
+ const options = {
671
+ root: null,
672
+ rootMargin: '0px',
673
+ threshold: 0.3,
674
+ };
675
+
420
676
  await this.refresh();
677
+ // this.style.display='block'
678
+ this.classList.add('fx-ready');
679
+
421
680
  this.ready = true;
681
+ this.initialRun = false;
422
682
  console.log('### <<<<< dispatching ready >>>>>');
423
683
  console.log('########## modelItems: ', this.getModel().modelItems);
424
684
  console.log('########## FORE: form fully initialized... ##########');
425
685
  this.dispatchEvent(new CustomEvent('ready', {}));
426
686
  }
427
687
 
688
+ registerLazyElement(element) {
689
+ if (this.intersectionObserver) {
690
+ // console.log('registerLazyElement',element);
691
+ this.intersectionObserver.observe(element);
692
+ }
693
+ }
694
+
695
+ unRegisterLazyElement(element) {
696
+ if (this.intersectionObserver) {
697
+ this.intersectionObserver.unobserve(element);
698
+ }
699
+ }
700
+
428
701
  /**
429
702
  *
430
703
  * @returns {FxModel}
@@ -144,6 +144,7 @@ export class FxInstance extends HTMLElement {
144
144
 
145
145
  _createInstanceData() {
146
146
  if (this.type === 'xml') {
147
+ // const doc = new DOMParser().parseFromString('<data data-id="default"></data>', 'application/xml');
147
148
  const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
148
149
  this.instanceData = doc;
149
150
  }
@@ -167,7 +168,19 @@ export class FxInstance extends HTMLElement {
167
168
  .then(response => {
168
169
  const responseContentType = response.headers.get('content-type').toLowerCase();
169
170
  console.log('********** responseContentType *********', responseContentType);
170
- if (responseContentType.startsWith('text/plain')) {
171
+ if (responseContentType.startsWith('text/html')) {
172
+ // const htmlResponse = response.text();
173
+ // return new DOMParser().parseFromString(htmlResponse, 'text/html');
174
+ // return response.text();
175
+ return response.text().then(result =>
176
+ // console.log('xml ********', result);
177
+ new DOMParser().parseFromString(result, 'text/html'),
178
+ );
179
+ }
180
+ if (
181
+ responseContentType.startsWith('text/plain') ||
182
+ responseContentType.startsWith('text/markdown')
183
+ ) {
171
184
  // console.log("********** inside res plain *********");
172
185
  return response.text();
173
186
  }
@@ -176,16 +189,20 @@ export class FxInstance extends HTMLElement {
176
189
  return response.json();
177
190
  }
178
191
  if (responseContentType.startsWith('application/xml')) {
179
- return response.text().then(result => {
180
- console.log('xml ********', result);
181
- return new DOMParser().parseFromString(result, 'application/xml');
182
- });
192
+ return response.text().then(result =>
193
+ // console.log('xml ********', result);
194
+ new DOMParser().parseFromString(result, 'application/xml'),
195
+ );
183
196
  }
184
197
  return 'done';
185
198
  })
186
199
  .then(data => {
200
+ if (data.nodeType) {
201
+ this.instanceData = data;
202
+ console.log('instanceData loaded: ', this.instanceData);
203
+ return;
204
+ }
187
205
  this.instanceData = data;
188
- console.log('instanceData loaded: ', this.instanceData);
189
206
  })
190
207
  .catch(error => {
191
208
  throw new Error(`failed loading data ${error}`);
@@ -218,6 +235,10 @@ export class FxInstance extends HTMLElement {
218
235
  // todo: move innerHTML out to shadowDOM (for later reset)
219
236
  } else if (this.type === 'json') {
220
237
  this.instanceData = JSON.parse(this.textContent);
238
+ } else if (this.type === 'html') {
239
+ this.instanceData = this.firstElementChild.children;
240
+ } else if (this.type === 'text') {
241
+ this.instanceData = this.textContent;
221
242
  } else {
222
243
  console.warn('unknow type for data ', this.type);
223
244
  }