@jinntec/fore 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fx-model.js CHANGED
@@ -45,6 +45,127 @@ export class FxModel extends HTMLElement {
45
45
  * @type {import('./fx-bind.js').FxBind[]}
46
46
  */
47
47
  this.binds = [];
48
+
49
+ this.debugInfo = {
50
+ debugId: `model-${Math.random().toString(36).slice(2, 9)}`,
51
+ createdAt: performance.now(),
52
+ modelConstructCount: 0,
53
+ updateModelCount: 0,
54
+ rebuildCount: 0,
55
+ recalculateCount: 0,
56
+ revalidateCount: 0,
57
+ lastUpdateAt: null,
58
+ lastCycle: null,
59
+ };
60
+ }
61
+
62
+ getDebugInfo(options = {}) {
63
+ const info = {
64
+ ...this.debugInfo,
65
+ constructed: this.modelConstructed,
66
+ inited: this.inited,
67
+ instanceCount: this.instances.length,
68
+ modelItemCount: this.modelItems.length,
69
+ instances: this.instances.map(instance => instance.getDebugInfo?.()),
70
+ modelItems: this.modelItems.map(item => item.getDebugInfo?.()),
71
+ };
72
+
73
+ if (options.includeGraphs === true) {
74
+ info.graphs = this.getDebugGraphInfo();
75
+ }
76
+
77
+ return info;
78
+ }
79
+
80
+ getDebugGraphInfo() {
81
+ return {
82
+ computes: this.computes,
83
+ mainGraph: this.getDebugGraphSummary(this.mainGraph),
84
+ subGraph: this.getDebugGraphSummary(this.subgraph),
85
+ };
86
+ }
87
+
88
+ getDebugGraphSummary(graph) {
89
+ if (!graph) {
90
+ return null;
91
+ }
92
+
93
+ const nodes = Object.keys(graph.nodes || {});
94
+ const outgoingEdges = graph.outgoingEdges || {};
95
+ const incomingEdges = graph.incomingEdges || {};
96
+ const calculationOrder = this.getDebugGraphOrder(graph);
97
+
98
+ return {
99
+ nodeCount: typeof graph.size === 'function' ? graph.size() : nodes.length,
100
+ edgeCount: this.getDebugEdgeCount(outgoingEdges),
101
+ outgoingEdgeCount: this.getDebugEdgeCount(outgoingEdges),
102
+ incomingEdgeCount: this.getDebugEdgeCount(incomingEdges),
103
+ computeNodeCount: nodes.filter(node => typeof node === 'string' && node.includes(':')).length,
104
+ calculationOrderCount: calculationOrder.length,
105
+ calculationOrder: calculationOrder.map((path, index) =>
106
+ this.getDebugGraphNodeInfo(graph, path, index),
107
+ ),
108
+ };
109
+ }
110
+
111
+ getDebugGraphOrder(graph) {
112
+ if (!graph || typeof graph.overallOrder !== 'function') {
113
+ return [];
114
+ }
115
+
116
+ try {
117
+ return graph.overallOrder(false);
118
+ } catch (error) {
119
+ return [];
120
+ }
121
+ }
122
+
123
+ getDebugEdgeCount(edgeMap = {}) {
124
+ return Object.values(edgeMap).reduce((count, edges) => {
125
+ return count + (Array.isArray(edges) ? edges.length : 0);
126
+ }, 0);
127
+ }
128
+
129
+ getDebugGraphNodeInfo(graph, path, index = 0) {
130
+ let data = null;
131
+
132
+ try {
133
+ data = graph.getNodeData(path);
134
+ } catch (error) {
135
+ data = null;
136
+ }
137
+
138
+ const basePath =
139
+ typeof path === 'string' && path.includes(':')
140
+ ? path.substring(0, path.indexOf(':'))
141
+ : path;
142
+
143
+ const facet =
144
+ typeof path === 'string' && path.includes(':')
145
+ ? path.substring(path.indexOf(':') + 1)
146
+ : null;
147
+
148
+ const modelItem = this.getModelItem(basePath);
149
+
150
+ return {
151
+ index: index + 1,
152
+ path,
153
+ basePath,
154
+ facet,
155
+ isCompute: !!facet,
156
+ ref: modelItem?.ref || null,
157
+ instanceId: modelItem?.instanceId || null,
158
+ value: modelItem?.value,
159
+ dataType: data?.__jsonlens__
160
+ ? 'json-lens'
161
+ : data?.nodeType
162
+ ? 'xml-node'
163
+ : data === null || data === undefined
164
+ ? null
165
+ : typeof data,
166
+ dependencies: graph.outgoingEdges?.[path] || [],
167
+ dependants: graph.incomingEdges?.[path] || [],
168
+ };
48
169
  }
49
170
 
50
171
  /**
@@ -205,6 +326,7 @@ export class FxModel extends HTMLElement {
205
326
  */
206
327
  async modelConstruct() {
207
328
  console.info(`📌 model-construct for #${this.parentNode.id}`);
329
+ this.debugInfo.modelConstructCount += 1;
208
330
 
209
331
  // this.dispatchEvent(new CustomEvent('model-construct', { detail: this }));
210
332
  Fore.dispatch(this, 'model-construct', { model: this });
@@ -361,7 +483,10 @@ export class FxModel extends HTMLElement {
361
483
  * update action triggering the update cycle
362
484
  */
363
485
  updateModel() {
364
- // console.time('updateModel');
486
+ this.debugInfo.updateModelCount += 1;
487
+ this.debugInfo.lastUpdateAt = performance.now();
488
+
489
+ const rebuildStart = performance.now();
365
490
  this.rebuild();
366
491
  /*
367
492
  if (this.skipUpdate){
@@ -369,11 +494,21 @@ export class FxModel extends HTMLElement {
369
494
  return;
370
495
  }
371
496
  */
497
+ const recalculateStart = performance.now();
372
498
  this.recalculate();
499
+ const revalidateStart = performance.now();
373
500
  this.revalidate();
374
- // console.log('updateModel finished with modelItems ', this.modelItems);
375
-
376
- // console.timeEnd('updateModel');
501
+ const revalidateEnd = performance.now();
502
+
503
+ this.debugInfo.lastCycle = {
504
+ timestamp: revalidateEnd,
505
+ rebuildMs: recalculateStart - rebuildStart,
506
+ recalculateMs: revalidateStart - recalculateStart,
507
+ revalidateMs: revalidateEnd - revalidateStart,
508
+ totalMs: revalidateEnd - rebuildStart,
509
+ computes: this.computes || 0,
510
+ modelItemCount: this.modelItems.length,
511
+ };
377
512
  }
378
513
 
379
514
  /**
@@ -421,6 +556,7 @@ export class FxModel extends HTMLElement {
421
556
 
422
557
  rebuild() {
423
558
  console.log(`🔷 rebuild() '${this.fore.id}'`);
559
+ this.debugInfo.rebuildCount += 1;
424
560
 
425
561
  // Build a lookup for existing ModelItems so we can reuse them by path (approach A)
426
562
  const prevItems = Array.isArray(this.modelItems) ? this.modelItems : [];
@@ -459,6 +595,7 @@ export class FxModel extends HTMLElement {
459
595
  * todo: use 'changed' flag on modelItems to determine subgraph for recalculation. Flag already exists but is not used.
460
596
  */
461
597
  async recalculate() {
598
+ this.debugInfo.recalculateCount += 1;
462
599
  if (!this.mainGraph) {
463
600
  return;
464
601
  }
@@ -629,6 +766,36 @@ export class FxModel extends HTMLElement {
629
766
  this.computes += 1;
630
767
  }
631
768
  }
769
+
770
+ _getNativeValidity(widget) {
771
+ if (!widget?.validity) return true;
772
+
773
+ let nativeValid = widget.validity.valid;
774
+
775
+ // Browsers do not consistently report minlength/maxlength violations
776
+ // for values assigned programmatically. Fore values are often updated
777
+ // programmatically, so enforce these two constraints explicitly.
778
+ const value = widget.value ?? '';
779
+
780
+ const minlength = widget.getAttribute('minlength');
781
+ if (minlength !== null && value !== '') {
782
+ const min = Number.parseInt(minlength, 10);
783
+ if (!Number.isNaN(min) && value.length < min) {
784
+ nativeValid = false;
785
+ }
786
+ }
787
+
788
+ const maxlength = widget.getAttribute('maxlength');
789
+ if (maxlength !== null) {
790
+ const max = Number.parseInt(maxlength, 10);
791
+ if (!Number.isNaN(max) && value.length > max) {
792
+ nativeValid = false;
793
+ }
794
+ }
795
+
796
+ return nativeValid;
797
+ }
798
+
632
799
  /**
633
800
  * Iterates all modelItems to calculate the validation status.
634
801
  *
@@ -646,6 +813,7 @@ export class FxModel extends HTMLElement {
646
813
  *
647
814
  */
648
815
  revalidate() {
816
+ this.debugInfo.revalidateCount += 1;
649
817
  if (this.modelItems.length === 0) return true;
650
818
 
651
819
  console.log(`🔷🔷🔷 revalidate() '${this.fore.id}'`);
@@ -706,6 +874,22 @@ export class FxModel extends HTMLElement {
706
874
  }
707
875
  }
708
876
  });
877
+ // Native browser constraint validation — read ValidityState without side-effects.
878
+ // widget.validity.valid is a live property; no events are fired (unlike checkValidity()).
879
+ // Use querySelector instead of getWidget() to avoid DOM mutation (getWidget creates a
880
+ // fallback input if none exists).
881
+ this.fore.querySelectorAll('fx-control').forEach(control => {
882
+ if (!control.modelItem) return;
883
+ const widget = control.querySelector('.widget, input');
884
+ if (!widget?.validity) return;
885
+ const nativeValid = this._getNativeValidity(widget);
886
+ if (control.modelItem.nativeValid !== nativeValid) {
887
+ control.modelItem.nativeValid = nativeValid;
888
+ control.modelItem.notify();
889
+ }
890
+ if (!nativeValid) valid = false;
891
+ });
892
+
709
893
  console.log('modelItems after revalidate: ', this.modelItems);
710
894
  console.log('changed after revalidate: ', this.changed);
711
895
  console.log(
@@ -848,6 +1032,7 @@ export class FxModel extends HTMLElement {
848
1032
  const result = this.instances[0].evalXPath(bindingExpr);
849
1033
  return result;
850
1034
  }
1035
+
851
1036
  }
852
1037
 
853
1038
  if (!customElements.get('fx-model')) {
@@ -70,6 +70,35 @@ export class FxSubmission extends ForeElementMixin {
70
70
  this.shadowRoot.innerHTML = this.renderHTML();
71
71
  }
72
72
 
73
+ getDebugInfo() {
74
+ return {
75
+ localName: this.localName,
76
+ id: this.id || null,
77
+
78
+ method: this.getAttribute('method') || null,
79
+ url: this.getAttribute('url') || null,
80
+ action: this.getAttribute('action') || null,
81
+ resource: this.getAttribute('resource') || null,
82
+
83
+ ref: this.getAttribute('ref') || null,
84
+ instance: this.getAttribute('instance') || null,
85
+ targetref: this.getAttribute('targetref') || null,
86
+ target: this.getAttribute('target') || null,
87
+
88
+ replace: this.getAttribute('replace') || null,
89
+ mediatype: this.getAttribute('mediatype') || null,
90
+ encoding: this.getAttribute('encoding') || null,
91
+ serialization: this.getAttribute('serialization') || null,
92
+
93
+ validate: this.getAttribute('validate') || null,
94
+ relevant: this.getAttribute('relevant') || null,
95
+
96
+ hasResponse: Boolean(this.response),
97
+ responseStatus: this.response?.status || null,
98
+ responseStatusText: this.response?.statusText || null,
99
+ };
100
+ }
101
+
73
102
  // eslint-disable-next-line class-methods-use-this
74
103
  renderHTML() {
75
104
  return `
package/src/modelitem.js CHANGED
@@ -44,6 +44,7 @@ export class ModelItem {
44
44
  this.instanceId = instance;
45
45
  this.fore = fore;
46
46
  this.changed = false;
47
+ this.nativeValid = true;
47
48
 
48
49
  // console.log('[ModelItem] created:', this.path);
49
50
 
@@ -64,6 +65,39 @@ export class ModelItem {
64
65
  this.state = {}; // evaluated expression results
65
66
  }
66
67
 
68
+ getDebugInfo() {
69
+ return {
70
+ path: this.path,
71
+ ref: this.ref,
72
+ instanceId: this.instanceId,
73
+ type: this.type,
74
+
75
+ value: this.value,
76
+
77
+ facets: {
78
+ readonly: this.readonly,
79
+ relevant: this.relevant,
80
+ required: this.required,
81
+ constraint: this.constraint,
82
+ changed: this.changed,
83
+ },
84
+
85
+ backing: this.lens
86
+ ? 'json-lens'
87
+ : this.node
88
+ ? 'xml-node'
89
+ : 'unknown',
90
+
91
+ observerCount: this.observers?.size || 0,
92
+ boundControlCount: this.boundControls?.length || 0,
93
+ dependencyCount: this.dependencies?.size || 0,
94
+ alertCount: this.alerts?.length || 0,
95
+
96
+ stateExpressions: this.stateExpressions,
97
+ state: this.state,
98
+ };
99
+ }
100
+
67
101
  get value() {
68
102
  if (this.lens) return this.lens.get();
69
103
  if (!this.node) return null;
@@ -278,4 +312,5 @@ export class ModelItem {
278
312
 
279
313
  return [];
280
314
  }
315
+
281
316
  }
@@ -343,7 +343,8 @@ export default class AbstractControl extends UIElement {
343
343
  const hasValue = this.modelItem.value !== '';
344
344
  const isRequired = this.modelItem.required;
345
345
  const isValidAccordingToRequired = isRequired ? hasValue : true;
346
- const isValidNow = this.modelItem.constraint && isValidAccordingToRequired;
346
+ const nativeValid = this.modelItem.nativeValid !== false;
347
+ const isValidNow = this.modelItem.constraint && isValidAccordingToRequired && nativeValid;
347
348
 
348
349
  if (this.isValid() !== isValidNow) {
349
350
  if (isValidNow) {
@@ -1,3 +1,4 @@
1
+ import { createTypedValueFactory, domFacade as fontoDomFacade } from 'fontoxpath';
1
2
  import XfAbstractControl from './abstract-control.js';
2
3
  import {
3
4
  evaluateXPath,
@@ -10,8 +11,10 @@ import { debounce } from '../events.js';
10
11
  import { FxModel } from '../fx-model.js';
11
12
  import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
12
13
  import { extractPredicateDependencies } from '../extract-predicate-deps.js';
14
+ import '../fx-instance.js';
13
15
 
14
16
  const WIDGETCLASS = 'widget';
17
+ const typedValueFactory = createTypedValueFactory('item()*');
15
18
 
16
19
  /**
17
20
  * `fx-control`
@@ -568,12 +571,13 @@ export default class FxControl extends XfAbstractControl {
568
571
  try {
569
572
  this._isRefreshing = true;
570
573
  // console.log('🔄 fx-control refresh', this);
571
- super.refresh(force);
574
+ await super.refresh(force);
572
575
  // console.log('refresh template', this.template);
573
576
  // const {widget} = this;
574
577
 
575
578
  // ### if we find a ref on control we have a 'select' control of some kind
576
579
  const widget = this.getWidget();
580
+ await this._loadDataSrc(widget);
577
581
  this._handleBoundWidget(widget, force);
578
582
  this._handleDataAttributeBinding();
579
583
  } finally {
@@ -582,6 +586,94 @@ export default class FxControl extends XfAbstractControl {
582
586
  await Fore.refreshChildren(this, force);
583
587
  }
584
588
 
589
+ /**
590
+ * Finds (or lazily creates) the anonymous `<fx-instance>` backing a
591
+ * `data-src` lookup document for the given URL, deduped per-model by URL.
592
+ *
593
+ * The created instance has no `id`, so it stays invisible to the global
594
+ * `$default`/`$<id>` variable mechanism. It is appended as the last child
595
+ * of the model, so it is never mistaken for the first/default instance
596
+ * (which assumes the model already declares its own default instance).
597
+ *
598
+ * @param {string} url
599
+ * @param {string|null} type
600
+ * @returns {import('../fx-instance.js').FxInstance}
601
+ * @private
602
+ */
603
+ _ensureDataSrcInstance(url, type) {
604
+ const model = this.getModel();
605
+ let instEl = Array.from(model.children).find(
606
+ el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
607
+ );
608
+
609
+ if (!instEl) {
610
+ instEl = document.createElement('fx-instance');
611
+ instEl.setAttribute('data-src', url);
612
+ instEl.setAttribute('src', url);
613
+ if (type) {
614
+ instEl.setAttribute('type', type);
615
+ }
616
+ model.appendChild(instEl);
617
+ instEl._loadPromise = instEl.init();
618
+ }
619
+ return instEl;
620
+ }
621
+
622
+ /**
623
+ * Implements the `data-src` lookup-list shortcut: if the bound widget
624
+ * declares `data-src="<url>"`, lazily loads that document via an anonymous
625
+ * `<fx-instance>` and binds its root element as a control-local XPath
626
+ * variable (`$src` by default, or `$<data-id>`), available to this
627
+ * control's own `ref` and template expressions.
628
+ *
629
+ * @param {HTMLElement} widget
630
+ * @private
631
+ */
632
+ async _loadDataSrc(widget) {
633
+ const url = widget.getAttribute && widget.getAttribute('data-src');
634
+ if (!url) return;
635
+
636
+ const varName = widget.getAttribute('data-id') || 'src';
637
+
638
+ if (this._dataSrcUrl === url) return;
639
+ this._dataSrcUrl = url;
640
+
641
+ // ### bind to an empty sequence until loaded so `ref` evaluation doesn't
642
+ // throw on the not-yet-bound variable; `_handleBoundWidget` bails out
643
+ // gracefully on an empty nodeset.
644
+ this.inScopeVariables.set(varName, typedValueFactory([], fontoDomFacade));
645
+
646
+ const instEl = this._ensureDataSrcInstance(url, widget.getAttribute('data-type'));
647
+
648
+ try {
649
+ await (instEl._loadPromise ?? Promise.resolve());
650
+
651
+ // ### derive the xpath default namespace for `$src`/`$<data-id>` once per
652
+ // anonymous instance: an explicit `data-xpath-ns` override wins, otherwise
653
+ // fall back to the namespace the loaded document declares on its root element.
654
+ if (!instEl.hasAttribute('xpath-default-namespace')) {
655
+ const override = widget.getAttribute('data-xpath-ns');
656
+ const root = instEl.getDefaultContext();
657
+ const ns = override || (root && root.namespaceURI);
658
+ if (ns) {
659
+ instEl.setAttribute('xpath-default-namespace', ns);
660
+ }
661
+ }
662
+
663
+ this.inScopeVariables.set(
664
+ varName,
665
+ typedValueFactory([instEl.getDefaultContext()], fontoDomFacade),
666
+ );
667
+ this.refresh(true);
668
+ } catch (error) {
669
+ Fore.dispatch(this, 'error', {
670
+ origin: this,
671
+ message: `control couldn't load data-src '${url}'`,
672
+ level: 'Error',
673
+ });
674
+ }
675
+ }
676
+
585
677
  /**
586
678
  * handle non-Fore elements like 'select' and 'datalist' which have a 'data-ref' attribute
587
679
  * @private
@@ -1,4 +1,5 @@
1
1
  import { Fore } from '../fore.js';
2
+ import { FxFore } from '../fx-fore.js';
2
3
 
3
4
  /**
4
5
  * <fx-include>
@@ -50,13 +51,30 @@ export class FxInclude extends HTMLElement {
50
51
  this.reload = this.hasAttribute('reload');
51
52
 
52
53
  if (this.immediate) {
53
- queueMicrotask(() => this.include());
54
+ this._includeWhenReady();
54
55
  return;
55
56
  }
56
57
 
57
58
  this._bindListener();
58
59
  }
59
60
 
61
+ /**
62
+ * Includes content immediately, but waits until the closest `fx-fore` has
63
+ * finished its initial Rebuild/Recalculate/Revalidate/Refresh cycle (i.e. is
64
+ * "ready"), so the included content can resolve binding context against an
65
+ * already-initialized ancestor chain.
66
+ */
67
+ _includeWhenReady() {
68
+ const fore = this.closest('fx-fore');
69
+
70
+ if (!fore) {
71
+ queueMicrotask(() => this.include());
72
+ return;
73
+ }
74
+
75
+ FxFore.waitUntilReady(fore).then(() => this.include());
76
+ }
77
+
60
78
  disconnectedCallback() {
61
79
  this._unbindListener();
62
80
  }
@@ -182,12 +200,20 @@ export class FxInclude extends HTMLElement {
182
200
  return fragment;
183
201
  }
184
202
 
185
- const template = parsed.querySelector('template');
203
+ const template = parsed.body.querySelector(':scope > template');
186
204
 
187
205
  if (template) {
188
206
  return document.importNode(template.content, true);
189
207
  }
190
208
 
209
+ const rootElements = Array.from(parsed.body.children);
210
+
211
+ if (rootElements.length === 1) {
212
+ const fragment = document.createDocumentFragment();
213
+ fragment.appendChild(document.importNode(rootElements[0], true));
214
+ return fragment;
215
+ }
216
+
191
217
  const fragment = document.createDocumentFragment();
192
218
 
193
219
  Array.from(parsed.body.childNodes).forEach(node => {
@@ -234,6 +260,12 @@ export class FxInclude extends HTMLElement {
234
260
 
235
261
  async _initializeInsertedContent(startElement) {
236
262
  this._initForeUiDescendants(startElement);
263
+
264
+ const fore = startElement.closest('fx-fore');
265
+ if (fore?.createNodes && typeof fore.initData === 'function') {
266
+ fore.initData(startElement);
267
+ }
268
+
237
269
  await Fore.refreshChildren(startElement, true);
238
270
  }
239
271
 
@@ -56,8 +56,8 @@ export class FxItems extends FxControl {
56
56
  });
57
57
 
58
58
  this.addEventListener('click', e => {
59
- e.preventDefault; // keep as-is (do not change behavior here)
60
- e.stopPropagation();
59
+ // e.preventDefault(); // keep as-is (do not change behavior here)
60
+ // e.stopPropagation();
61
61
  const items = this.querySelectorAll('[value]');
62
62
 
63
63
  let val = '';
@@ -86,7 +86,7 @@ export class FxItems extends FxControl {
86
86
  }
87
87
 
88
88
  async refresh(force = false) {
89
- super.refresh(force);
89
+ await super.refresh(force);
90
90
  // console.log('fx-items.refresh() called');
91
91
  }
92
92
 
@@ -741,6 +741,41 @@ function findInstanceReferences(xpathQuery) {
741
741
  return instanceReferences;
742
742
  }
743
743
 
744
+ /**
745
+ * Finds the anonymous `<fx-instance data-src="...">` (created by
746
+ * `fx-control._ensureDataSrcInstance`) backing a `$src`/`$<data-id>` variable
747
+ * referenced in `xpathQuery`, if any. Used so the namespace resolver can pick up
748
+ * that instance's `xpath-default-namespace` instead of inheriting it from the
749
+ * form's default instance.
750
+ */
751
+ function findDataSrcInstance(xpathQuery, formElement) {
752
+ if (!formElement || typeof formElement.querySelectorAll !== 'function') return null;
753
+
754
+ const varNames = [...xpathQuery.matchAll(/\$([A-Za-z_][\w.-]*)/g)].map(m => m[1]);
755
+ if (varNames.length === 0) return null;
756
+
757
+ const widgets = Array.from(formElement.querySelectorAll('[data-src]')).filter(
758
+ widget => widget.closest('fx-control') === formElement,
759
+ );
760
+ if (widgets.length === 0) return null;
761
+
762
+ const model = typeof formElement.getModel === 'function' ? formElement.getModel() : null;
763
+ if (!model) return null;
764
+
765
+ for (const varName of varNames) {
766
+ const widget = widgets.find(w => (w.getAttribute('data-id') || 'src') === varName);
767
+ if (!widget) continue;
768
+
769
+ const url = widget.getAttribute('data-src');
770
+ const instance = Array.from(model.children).find(
771
+ el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
772
+ );
773
+ if (instance) return instance;
774
+ }
775
+
776
+ return null;
777
+ }
778
+
744
779
  function getCachedNamespaceResolver(xpath, node) {
745
780
  if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) return null;
746
781
  return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
@@ -776,6 +811,14 @@ export function createNamespaceResolver(xpathQuery, formElement) {
776
811
  };
777
812
 
778
813
  if (instanceReferences.length === 0) {
814
+ const dataSrcInstance = findDataSrcInstance(xpathQuery, formElement);
815
+ if (dataSrcInstance && dataSrcInstance.hasAttribute('xpath-default-namespace')) {
816
+ const xpathDefaultNamespace = dataSrcInstance.getAttribute('xpath-default-namespace');
817
+ const resolveNamespacePrefix = prefix => (!prefix ? xpathDefaultNamespace : undefined);
818
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
819
+ return resolveNamespacePrefix;
820
+ }
821
+
779
822
  const ancestorComponent = closestRefExcludingSelf(formElement);
780
823
 
781
824
  if (ancestorComponent && ancestorComponent !== formElement) {