@jinntec/fore 1.0.0-1 → 1.0.0-2

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
  }
@@ -75,7 +97,7 @@ export class FxFore extends HTMLElement {
75
97
  visibility: visible;
76
98
  opacity: 1;
77
99
  }
78
-
100
+
79
101
  .popup {
80
102
  margin: 70px auto;
81
103
  background: #fff;
@@ -111,13 +133,21 @@ export class FxFore extends HTMLElement {
111
133
  .popup .close:focus{
112
134
  outline:none;
113
135
  }
114
-
136
+
115
137
  .popup .close:hover {
116
138
  color: #06D85F;
117
139
  }
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 = `
@@ -143,6 +173,16 @@ export class FxFore extends HTMLElement {
143
173
  }
144
174
 
145
175
  connectedCallback() {
176
+ this.lazyRefresh = this.hasAttribute('refresh-on-view');
177
+ if (this.lazyRefresh) {
178
+ const options = {
179
+ root: null,
180
+ rootMargin: '0px',
181
+ threshold: 0.3,
182
+ };
183
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
184
+ }
185
+
146
186
  const slot = this.shadowRoot.querySelector('slot');
147
187
  slot.addEventListener('slotchange', event => {
148
188
  const children = event.target.assignedElements();
@@ -155,18 +195,62 @@ export class FxFore extends HTMLElement {
155
195
  modelElement = generatedModel;
156
196
  }
157
197
  if (!modelElement.inited) {
158
- console.log('########## FORE: kick off processing... ##########');
198
+ console.log(
199
+ `########## FORE: kick off processing for ... ${window.location.href} ##########`,
200
+ );
159
201
  modelElement.modelConstruct();
160
202
  }
161
203
  this.model = modelElement;
162
204
  });
163
205
  }
164
206
 
207
+ /**
208
+ * refreshes the UI by using IntersectionObserver API. This is the handler being called
209
+ * by the observer whenever elements come into / move out of viewport.
210
+ * @param entries
211
+ * @param observer
212
+ */
213
+ handleIntersect(entries, observer) {
214
+ console.time('refreshLazy');
215
+ entries.forEach(entry => {
216
+ const { target } = entry;
217
+
218
+ if (entry.isIntersecting) {
219
+ console.log('in view', entry);
220
+ // console.log('repeat in view entry', entry.target);
221
+ // const target = entry.target;
222
+ // if(target.hasAttribute('refresh-on-view')){
223
+ target.classList.add('loaded');
224
+ // }
225
+
226
+ // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
227
+ if (typeof target.refresh === 'function') {
228
+ console.log('refreshing target', target);
229
+ target.refresh(target, true);
230
+ } else {
231
+ console.log('refreshing children', target);
232
+ Fore.refreshChildren(target, true);
233
+ }
234
+ }
235
+ });
236
+ entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
237
+
238
+ console.timeEnd('refreshLazy');
239
+ }
240
+
165
241
  evaluateToNodes(xpath, context) {
166
242
  return evaluateXPathToNodes(xpath, context, this);
167
243
  }
168
244
 
169
- disconnectedCallback() {}
245
+ disconnectedCallback() {
246
+ /*
247
+ this.removeEventListener('model-construct-done', this._handleModelConstructDone);
248
+ this.removeEventListener('message', this._displayMessage);
249
+ this.removeEventListener('error', this._displayError);
250
+ this.storedTemplateExpressionByNode=null;
251
+ this.shadowRoot = undefined;
252
+ */
253
+ }
170
254
 
171
255
  /**
172
256
  * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
@@ -176,16 +260,39 @@ export class FxFore extends HTMLElement {
176
260
  * AVT:
177
261
  *
178
262
  */
179
- async refresh() {
263
+ async refresh(force) {
180
264
  // refresh () {
181
265
  console.group('### refresh');
182
- // await this.updateComplete;
183
266
 
184
- Fore.refreshChildren(this);
185
- // this.dispatchEvent(new CustomEvent('refresh-done', {detail:'foo'}));
267
+ console.time('refresh');
268
+
269
+ /*
270
+ const changedModelItems = this.getModel().changed;
271
+ const graph = this.getModel().mainGraph;
272
+ let doRefresh = true;
273
+ changedModelItems.forEach(item => {
274
+ if(graph.hasNode(item.path)) {
275
+ const deps = graph.dependentsOf(item.path, false);
276
+ if (deps.length === 0) {
277
+ doRefresh=false;
278
+ }
279
+ }
280
+ });
281
+ this.getModel().changed = [];
282
+
283
+ if (!doRefresh) {
284
+ this.dispatchEvent(new CustomEvent('refresh-done'));
285
+ return ;
286
+ }
287
+ */
288
+ // ### refresh Fore UI elements
289
+ console.time('refreshChildren');
290
+ Fore.refreshChildren(this, true);
291
+ console.timeEnd('refreshChildren');
186
292
 
187
293
  // ### refresh template expressions
188
294
  this._updateTemplateExpressions();
295
+ console.timeEnd('refresh');
189
296
 
190
297
  console.groupEnd();
191
298
  console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
@@ -203,14 +310,18 @@ export class FxFore extends HTMLElement {
203
310
  _updateTemplateExpressions() {
204
311
  // Note the fact we're going over HTML here: therefore the `html` prefix.
205
312
  const search =
206
- "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::xhtml:fx-model/descendant-or-self::node()/(., @*)";
313
+ "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::fx-model/descendant-or-self::node()/(., @*)";
207
314
 
208
315
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
209
316
  console.log('template expressions found ', tmplExpressions);
210
317
 
318
+ if (!this.storedTemplateExpressions) {
319
+ this.storedTemplateExpressions = [];
320
+ }
321
+
211
322
  /*
212
- storing expressions and their nodes for re-evaluation
213
- */
323
+ storing expressions and their nodes for re-evaluation
324
+ */
214
325
  Array.from(tmplExpressions).forEach(node => {
215
326
  if (this.storedTemplateExpressionByNode.has(node)) {
216
327
  // If the node is already known, do not process it twice
@@ -268,13 +379,15 @@ export class FxFore extends HTMLElement {
268
379
  }
269
380
  // Templates are special: they use the namespace configuration from the place where they are
270
381
  // being defined
271
-
382
+ const instanceId = XPathUtil.getInstanceId(naked);
383
+ // console.log('target instance ', instanceId);
384
+ const inst = this.getModel().getInstance(instanceId);
272
385
  try {
273
- const result = evaluateXPathToString(naked, inscope, node, null, namespaceContextNode);
386
+ const result = evaluateXPathToString(naked, inscope, node, null, inst);
274
387
 
275
388
  // console.log('result of eval ', result);
276
389
  const replaced = expr.replaceAll(match, result);
277
- console.log('result of replacing ', replaced);
390
+ // console.log('result of replacing ', replaced);
278
391
 
279
392
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
280
393
  const parent = node.ownerElement;
@@ -286,7 +399,7 @@ export class FxFore extends HTMLElement {
286
399
  }
287
400
 
288
401
  if (replaced.includes('{')) {
289
- console.log('need to go next round');
402
+ // console.log('need to go next round');
290
403
 
291
404
  // todo: duplicated code here - see above
292
405
  naked = replaced.substring(1, replaced.length);
@@ -305,25 +418,28 @@ export class FxFore extends HTMLElement {
305
418
  return node.value;
306
419
  }
307
420
  if (node.nodeType === Node.TEXT_NODE) {
308
- return node.textContent;
421
+ return node.textContent.trim();
309
422
  }
310
423
  return null;
311
424
  }
312
425
 
313
- _refreshChildren() {
314
- const uiElements = this.querySelectorAll('*');
315
-
316
- uiElements.forEach(element => {
317
- if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
318
- element.refresh();
319
- }
320
- });
321
- }
322
-
426
+ /**
427
+ * called when `model-construct-done` event is received to
428
+ * start initing of the UI.
429
+ *
430
+ * @private
431
+ */
323
432
  _handleModelConstructDone() {
324
433
  this._initUI();
325
434
  }
326
435
 
436
+ /**
437
+ * If there's no instance element found in a fx-model during init it will construct
438
+ * an instance from UI bindings.
439
+ *
440
+ * @returns {Promise<void>}
441
+ * @private
442
+ */
327
443
  async _lazyCreateInstance() {
328
444
  const model = this.querySelector('fx-model');
329
445
  if (model.instances.length === 0) {
@@ -422,11 +538,31 @@ export class FxFore extends HTMLElement {
422
538
  }
423
539
  */
424
540
 
541
+ /**
542
+ * Start the initialization of the UI by
543
+ *
544
+ * 1. checking if a instance needs to be generated
545
+ * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
546
+ * 3. doing a full refresh of the UI
547
+ *
548
+ * @returns {Promise<void>}
549
+ * @private
550
+ */
425
551
  async _initUI() {
426
552
  console.log('### _initUI()');
427
553
 
428
554
  await this._lazyCreateInstance();
555
+
556
+ const options = {
557
+ root: null,
558
+ rootMargin: '0px',
559
+ threshold: 0.3,
560
+ };
561
+
429
562
  await this.refresh();
563
+ // this.style.display='block'
564
+ this.classList.add('fx-ready');
565
+
430
566
  this.ready = true;
431
567
  console.log('### <<<<< dispatching ready >>>>>');
432
568
  console.log('########## modelItems: ', this.getModel().modelItems);
@@ -434,6 +570,19 @@ export class FxFore extends HTMLElement {
434
570
  this.dispatchEvent(new CustomEvent('ready', {}));
435
571
  }
436
572
 
573
+ registerLazyElement(element) {
574
+ if (this.intersectionObserver) {
575
+ // console.log('registerLazyElement',element);
576
+ this.intersectionObserver.observe(element);
577
+ }
578
+ }
579
+
580
+ unRegisterLazyElement(element) {
581
+ if (this.intersectionObserver) {
582
+ this.intersectionObserver.unobserve(element);
583
+ }
584
+ }
585
+
437
586
  /**
438
587
  *
439
588
  * @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
  }