@jinntec/fore 2.5.0 → 2.7.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.
Files changed (63) hide show
  1. package/dist/fore-dev.js +36088 -9
  2. package/dist/fore.js +35918 -9
  3. package/index.js +3 -1
  4. package/package.json +10 -4
  5. package/resources/fore.css +30 -5
  6. package/src/DataObserver.js +181 -0
  7. package/src/DependencyNotifyingDomFacade.js +27 -21
  8. package/src/DependentXPathQueries.js +32 -0
  9. package/src/ForeElementMixin.js +60 -26
  10. package/src/actions/abstract-action.js +24 -29
  11. package/src/actions/fx-append.js +25 -2
  12. package/src/actions/fx-call.js +2 -2
  13. package/src/actions/fx-delete.js +58 -21
  14. package/src/actions/fx-hide.js +1 -1
  15. package/src/actions/fx-insert.js +62 -48
  16. package/src/actions/fx-load.js +7 -2
  17. package/src/actions/fx-refresh.js +15 -5
  18. package/src/actions/fx-replace.js +18 -3
  19. package/src/actions/fx-reset.js +6 -0
  20. package/src/actions/fx-send.js +10 -7
  21. package/src/actions/fx-setattribute.js +12 -0
  22. package/src/actions/fx-setfocus.js +11 -8
  23. package/src/actions/fx-setvalue.js +20 -2
  24. package/src/actions/fx-show.js +4 -2
  25. package/src/actions/fx-toggle.js +1 -1
  26. package/src/extract-predicate-deps.js +57 -0
  27. package/src/extractPredicateDependencies.js +36 -0
  28. package/src/fore.js +78 -36
  29. package/src/fx-bind.js +128 -23
  30. package/src/fx-connection.js +24 -7
  31. package/src/fx-fore.js +552 -306
  32. package/src/fx-instance.js +9 -11
  33. package/src/fx-model.js +154 -65
  34. package/src/fx-submission.js +45 -51
  35. package/src/fx-var.js +5 -0
  36. package/src/getInScopeContext.js +8 -8
  37. package/src/modelitem.js +218 -72
  38. package/src/tools/fx-action-log.js +21 -19
  39. package/src/tools/fx-log-settings.js +4 -2
  40. package/src/ui/TemplateExpression.js +12 -0
  41. package/src/ui/UIElement.js +206 -0
  42. package/src/ui/abstract-control.js +15 -7
  43. package/src/ui/fx-case.js +15 -3
  44. package/src/ui/fx-container.js +10 -3
  45. package/src/ui/fx-control-menu.js +207 -0
  46. package/src/ui/fx-control.js +116 -32
  47. package/src/ui/fx-dialog.js +2 -2
  48. package/src/ui/fx-group.js +14 -0
  49. package/src/ui/fx-items.js +10 -4
  50. package/src/ui/fx-repeat-attributes.js +111 -35
  51. package/src/ui/fx-repeat.js +364 -87
  52. package/src/ui/fx-repeat.updated.js +821 -0
  53. package/src/ui/fx-repeatitem.js +23 -20
  54. package/src/ui/fx-switch.js +5 -3
  55. package/src/ui/fx-upload.js +36 -40
  56. package/src/ui/repeat-base.js +532 -0
  57. package/src/withDraggability.js +8 -4
  58. package/src/xpath-evaluation.js +26 -8
  59. package/src/xpath-path.js +79 -0
  60. package/src/xpath-util.js +107 -11
  61. package/dist/fore-dev.js.map +0 -1
  62. package/dist/fore.js.map +0 -1
  63. package/src/ui/fx-select.js +0 -89
package/index.js CHANGED
@@ -32,7 +32,6 @@ import './src/tools/fx-devtools.js';
32
32
  import './src/tools/fx-dom-inspector.js';
33
33
  import './src/lab/fore-component.js';
34
34
  import './src/tools/fx-json-instance.js';
35
- import './tools/fx-lens.js';
36
35
  import './src/ui/fx-upload.js';
37
36
  // import './src/tools/fx-minimap.js';
38
37
 
@@ -64,6 +63,9 @@ import './src/actions/fx-call.js';
64
63
  import './src/actions/fx-setattribute.js';
65
64
  import './src/actions/fx-construct-done.js';
66
65
  import './src/actions/fx-unmodified.js';
66
+ import './src/ui/fx-control-menu.js';
67
67
 
68
68
  import './src/functions/fx-function.js';
69
69
  import './src/functions/fx-functionlib.js';
70
+
71
+ export default {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
@@ -32,10 +32,12 @@
32
32
  "dependencies": {
33
33
  "@jinntec/jinn-toast": "^1.0.5",
34
34
  "@picocss/pico": "^2.0.6",
35
+ "chai": "^5.2.1",
35
36
  "fontoxpath": "^3.33.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@babel/plugin-proposal-class-properties": "^7.17.12",
40
+ "@jinntec/jinn-codemirror": "^1.17.6",
39
41
  "@open-wc/building-rollup": "^2.0.1",
40
42
  "@open-wc/eslint-config": "^7.0.0",
41
43
  "@open-wc/prettier-config": "^0.1.10",
@@ -43,6 +45,7 @@
43
45
  "@open-wc/testing": "^3.1.5",
44
46
  "@open-wc/testing-karma": "^4.0.9",
45
47
  "@open-wc/testing-karma-bs": "^1.3.94",
48
+ "@polymer/iron-iconset": "^3.0.1",
46
49
  "@rollup/plugin-commonjs": "^22.0.0",
47
50
  "@rollup/plugin-node-resolve": "^13.3.0",
48
51
  "@rollup/plugin-strip": "^2.1.0",
@@ -51,7 +54,6 @@
51
54
  "@webcomponents/webcomponentsjs": "^2.6.0",
52
55
  "cypress": "^13.16.0",
53
56
  "deepmerge": "^4.2.2",
54
- "es-dev-server": "^2.1.0",
55
57
  "eslint": "^8.16.0",
56
58
  "eslint-plugin-prettier": "^5.1.3",
57
59
  "husky": "^8.0.1",
@@ -66,12 +68,16 @@
66
68
  "rollup-plugin-babel": "^4.4.0",
67
69
  "rollup-plugin-minify-html-literals": "^1.2.6",
68
70
  "rollup-plugin-version-injector": "^1.3.3",
71
+ "sinon": "^15.2.0",
72
+ "sinon-chai": "^4.0.0",
69
73
  "typescript": "^4.7.2",
74
+ "vite": "^7.0.5",
70
75
  "web-component-analyzer": "^1.1.6",
71
76
  "xmlserializer": "^0.6.1"
72
77
  },
73
78
  "scripts": {
74
79
  "test": "karma start",
80
+ "test:all": "karma start --grep test/**/*.test.js",
75
81
  "lint:eslint": "eslint .",
76
82
  "format:eslint": "eslint . --fix",
77
83
  "lint:prettier": "prettier \"**/*.js\" --check --ignore-path .eslintignore",
@@ -86,11 +92,11 @@
86
92
  "test:bs": "karma start karma.bs.config.js --coverage",
87
93
  "start:build": "cd dist && es-dev-server --open",
88
94
  "build": "rimraf dist && rollup -c rollup.config.js",
89
- "start": "es-dev-server --app-index doc/index.html --node-resolve --watch --open",
95
+ "start": "vite --open --port 8090 --host",
90
96
  "preversion": "npm run test",
91
97
  "prepare": "npm run build",
92
98
  "install-demos": "npm i && cd demo && npm i",
93
- "start-cypress": "es-dev-server --app-index doc/index.html --node-resolve",
99
+ "start-cypress": "npm run start",
94
100
  "websocket-server": "node websocket-server.js"
95
101
  },
96
102
  "keywords": [
@@ -12,10 +12,6 @@
12
12
  --inspector-handle-width: 30px;
13
13
  }
14
14
 
15
- [unresolved] {
16
- display: none;
17
- }
18
-
19
15
  [disabled] {
20
16
  pointer-events: none;
21
17
  cursor: default;
@@ -145,7 +141,8 @@
145
141
  }
146
142
 
147
143
  fx-group, fx-switch, fx-repeat, fx-dialog {
148
- display: block;
144
+ /*display: block;*/
145
+ position: relative;
149
146
  }
150
147
 
151
148
  fx-hint {
@@ -215,6 +212,28 @@
215
212
  display: block;
216
213
  }
217
214
 
215
+ /* for on-demand elements */
216
+ [role=group][on-demand=true] {
217
+ display: none;
218
+ }
219
+ .trash {
220
+ display: inline-block;
221
+ position: absolute;
222
+ right:0;
223
+ top:0;
224
+ z-index: 10;
225
+ font-size: 0.8rem;
226
+ }
227
+ .trash svg{
228
+ width: 1rem;
229
+ height: 1rem;
230
+ color:#666666;
231
+ }
232
+ .trash svg:hover{
233
+ color:black;
234
+
235
+ }
236
+
218
237
  /* fx-action-log styles */
219
238
 
220
239
  fx-fore.action-log{
@@ -267,3 +286,9 @@
267
286
  background: var(--paper-orange-500);
268
287
  }
269
288
 
289
+ /*
290
+ * For on-demand stuff, if it has no value initially,hide it
291
+ */
292
+ [on-demand] {
293
+ display: none !important;
294
+ }
@@ -0,0 +1,181 @@
1
+ /* Usage:
2
+
3
+ // Create or load your XML document
4
+ const xmlString = `
5
+ <root>
6
+ <item id="1">Initial Value</item>
7
+ </root>
8
+ `;
9
+ const parser = new DOMParser();
10
+ const xmlDoc = parser.parseFromString(xmlString, "application/xml");
11
+ const rootNode = xmlDoc.querySelector("root");
12
+
13
+ // Create an instance of the DataObserver
14
+ const xmlObserver = new DataObserver(200);
15
+
16
+ // Define a callback to process mutations
17
+ const handleXMLMutations = (mutationsList) => {
18
+ console.log("XML Mutations:", mutationsList);
19
+ };
20
+
21
+ // Attach the observer to the XML root node
22
+ xmlObserver.observe(rootNode, handleXMLMutations);
23
+
24
+ // Modify the XML document to trigger mutations
25
+ setTimeout(() => {
26
+ const newItem = xmlDoc.createElement("item");
27
+ newItem.textContent = "Newly Added Item";
28
+ rootNode.appendChild(newItem); // This triggers a mutation
29
+ }, 1000);
30
+
31
+ For JSON:
32
+ // Create a JSON object
33
+ const jsonObject = {
34
+ name: "John Doe",
35
+ age: 30,
36
+ hobbies: ["reading", "coding"],
37
+ };
38
+
39
+ // Create an instance of the DataObserver
40
+ const jsonObserver = new DataObserver(200);
41
+
42
+ // Define a callback to process changes
43
+ const handleJSONChanges = (changesList) => {
44
+ console.log("JSON Changes:", changesList);
45
+ };
46
+
47
+ // Attach the observer to the JSON object
48
+ jsonObserver.observe(jsonObject, handleJSONChanges);
49
+
50
+ // Modify the JSON object to trigger changes
51
+ setTimeout(() => {
52
+ jsonObject.name = "Jane Doe"; // This triggers a mutation
53
+ jsonObject.age = 31; // Another mutation
54
+ delete jsonObject.hobbies; // This triggers a delete mutation
55
+ }, 1000);
56
+
57
+ */
58
+ export class DataObserver {
59
+ constructor(debounceTime = 0) {
60
+ this.observer = null; // Placeholder for MutationObserver (for XML)
61
+ this.debounceTime = debounceTime; // Time in milliseconds for optional debouncing
62
+ this.mutationsQueue = []; // To batch process mutations
63
+ this.debounceTimer = null; // Timer for debouncing
64
+ this.jsonProxy = null; // Proxy for JSON observation
65
+ }
66
+
67
+ /**
68
+ * Attaches an observer to the given rootNode (DOM Node or JSON object)
69
+ * @param {Node|Object} rootNode - The XML DOM node or JSON object to observe
70
+ * @param {Function} callback - Function to process batch mutations
71
+ */
72
+ observe(rootNode, callback) {
73
+ if (rootNode instanceof Node) {
74
+ // Handle XML Node
75
+ this.observeXML(rootNode, callback);
76
+ } else if (typeof rootNode === "object" && rootNode !== null) {
77
+ // Handle JSON Object
78
+ this.observeJSON(rootNode, callback);
79
+ } else {
80
+ throw new Error("Invalid rootNode. Must be a DOM Node or a JSON object.");
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Observes changes in an XML DOM node
86
+ * @param {Node} xmlNode - The XML DOM node to observe
87
+ * @param {Function} callback - Function to process batch mutations
88
+ */
89
+ observeXML(xmlNode, callback) {
90
+ // Initialize a new MutationObserver
91
+ this.observer = new MutationObserver((mutationsList) => {
92
+ this.mutationsQueue.push(...mutationsList);
93
+
94
+ if (this.debounceTime > 0) {
95
+ clearTimeout(this.debounceTimer); // Clear previous timer
96
+ this.debounceTimer = setTimeout(() => {
97
+ this.processMutations(callback);
98
+ }, this.debounceTime);
99
+ } else {
100
+ this.processMutations(callback);
101
+ }
102
+ });
103
+
104
+ // Start observing the XML node
105
+ this.observer.observe(xmlNode, {
106
+ characterData: true,
107
+ childList: true,
108
+ subtree: true,
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Observes changes in a JSON object using a proxy
114
+ * @param {Object} jsonObject - The JSON object to observe
115
+ * @param {Function} callback - Function to process changes
116
+ */
117
+ observeJSON(jsonObject, callback) {
118
+ const handler = {
119
+ set: (target, key, value) => {
120
+ this.mutationsQueue.push({ type: "update", target, key, value });
121
+
122
+ if (this.debounceTime > 0) {
123
+ clearTimeout(this.debounceTimer); // Clear previous timer
124
+ this.debounceTimer = setTimeout(() => {
125
+ this.processMutations(callback);
126
+ }, this.debounceTime);
127
+ } else {
128
+ this.processMutations(callback);
129
+ }
130
+
131
+ // Perform the actual update
132
+ target[key] = value;
133
+ return true;
134
+ },
135
+
136
+ deleteProperty: (target, key) => {
137
+ this.mutationsQueue.push({ type: "delete", target, key });
138
+
139
+ if (this.debounceTime > 0) {
140
+ clearTimeout(this.debounceTimer); // Clear previous timer
141
+ this.debounceTimer = setTimeout(() => {
142
+ this.processMutations(callback);
143
+ }, this.debounceTime);
144
+ } else {
145
+ this.processMutations(callback);
146
+ }
147
+
148
+ // Perform the actual delete
149
+ return delete target[key];
150
+ },
151
+ };
152
+
153
+ this.jsonProxy = new Proxy(jsonObject, handler);
154
+ }
155
+
156
+ /**
157
+ * Processes the mutations batch and clears the queue
158
+ * @param {Function} callback - Function to handle the batch of mutations
159
+ */
160
+ processMutations(callback) {
161
+ if (this.mutationsQueue.length > 0) {
162
+ callback(this.mutationsQueue); // Pass the batch to the callback
163
+ this.mutationsQueue = []; // Clear the queue
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Disconnects the observer and clears any pending debounce timer
169
+ */
170
+ disconnect() {
171
+ if (this.observer) {
172
+ this.observer.disconnect(); // Disconnect the MutationObserver
173
+ this.observer = null;
174
+ }
175
+ if (this.debounceTimer) {
176
+ clearTimeout(this.debounceTimer); // Clear any pending debounce timer
177
+ }
178
+ this.mutationsQueue = []; // Clear the mutation queue
179
+ this.jsonProxy = null; // Clear JSON proxy reference
180
+ }
181
+ }
@@ -24,6 +24,7 @@ export class DependencyNotifyingDomFacade {
24
24
  */
25
25
  // eslint-disable-next-line class-methods-use-this
26
26
  getAllAttributes(node) {
27
+ this._onNodeTouched(node); // <== Important!
27
28
  return Array.from(node.attributes);
28
29
  }
29
30
 
@@ -35,7 +36,10 @@ export class DependencyNotifyingDomFacade {
35
36
  */
36
37
  // eslint-disable-next-line class-methods-use-this
37
38
  getAttribute(node, attributeName) {
38
- return node.getAttribute(attributeName);
39
+ const attr = node.getAttributeNode(attributeName);
40
+ console.log('[DomFacade] getAttribute touched:', attr);
41
+ if (attr) this._onNodeTouched(attr);
42
+ return attr?.value ?? null;
39
43
  }
40
44
 
41
45
  /**
@@ -61,10 +65,12 @@ export class DependencyNotifyingDomFacade {
61
65
  */
62
66
  getData(node) {
63
67
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
68
+ // console.log('[DomFacade] getData on attribute:', node);
64
69
  this._onNodeTouched(node);
65
70
  return node.value;
66
71
  }
67
72
  // Text node
73
+ // console.log('[DomFacade] getData on text node parent:', node.parentNode);
68
74
  this._onNodeTouched(node.parentNode);
69
75
  return node.data;
70
76
  }
@@ -95,14 +101,12 @@ export class DependencyNotifyingDomFacade {
95
101
  */
96
102
  // eslint-disable-next-line class-methods-use-this
97
103
  getLastChild(node, bucket) {
98
- const matchingNodes = node
99
- .getChildNodes()
100
- .filter(childNode => !bucket || getBucketsForNode(childNode).includes(bucket));
101
- const matchNode = matchingNodes[matchingNodes.length - 1];
102
- if (matchNode) {
103
- return matchNode;
104
- }
105
- return null;
104
+ const children = Array.from(node.childNodes).filter(
105
+ child => !bucket || getBucketsForNode(child).includes(bucket),
106
+ );
107
+ const last = children[children.length - 1];
108
+ if (last) this._onNodeTouched(last);
109
+ return last || null;
106
110
  }
107
111
 
108
112
  /**
@@ -133,7 +137,16 @@ export class DependencyNotifyingDomFacade {
133
137
  */
134
138
  // eslint-disable-next-line class-methods-use-this
135
139
  getParentNode(node) {
136
- return node.parentNode;
140
+ const parent = node.parentNode;
141
+ /*
142
+ if (
143
+ parent &&
144
+ (parent.nodeType === Node.ELEMENT_NODE || parent.nodeType === Node.ATTRIBUTE_NODE)
145
+ ) {
146
+ this._onNodeTouched(parent);
147
+ }
148
+ */
149
+ return parent;
137
150
  }
138
151
 
139
152
  /**
@@ -145,17 +158,10 @@ export class DependencyNotifyingDomFacade {
145
158
  */
146
159
  // eslint-disable-next-line class-methods-use-this
147
160
  getPreviousSibling(node, bucket) {
148
- for (
149
- let { previousSibling } = node;
150
- previousSibling;
151
- previousSibling = previousSibling.previousSibling
152
- ) {
153
- if (bucket && !getBucketsForNode(previousSibling).includes(bucket)) {
154
- // eslint-disable-next-line no-continue
155
- continue;
156
- }
157
-
158
- return previousSibling;
161
+ for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
162
+ if (bucket && !getBucketsForNode(sibling).includes(bucket)) continue;
163
+ this._onNodeTouched(sibling);
164
+ return sibling;
159
165
  }
160
166
  return null;
161
167
  }
@@ -25,6 +25,38 @@ export default class DependentXPathQueries {
25
25
  return !!this._parentDependencies?.isInvalidatedByIndexFunction();
26
26
  }
27
27
 
28
+ /**
29
+ * Detects whether there are XPaths that may be affected by child list changes to nodes with the given local names
30
+ *
31
+ * It does this pessimistically, assuming that any descendant axis can be affected by these changes
32
+ *
33
+ * @param {string[]} affectedLocalNames
34
+ */
35
+ isInvalidatedByChildlistChanges(affectedLocalNames) {
36
+ // Scan for any XPath part that may jump over a node without checking what the type is. Can also
37
+ // be done dynamically, by listening for the `bucket` parameter of all the dom accessors
38
+ const flaggedConstructs = [
39
+ '//',
40
+ 'ancestor',
41
+ 'descendant',
42
+ 'element(',
43
+ '*',
44
+ '..',
45
+ 'following',
46
+ 'preceding',
47
+ ];
48
+ for (const xpath of this._xpaths) {
49
+ if (flaggedConstructs.some(c => xpath.includes(c))) {
50
+ return true;
51
+ }
52
+ if (affectedLocalNames.some(n => xpath.includes(n))) {
53
+ return true;
54
+ }
55
+ }
56
+ // We can also depend on these elements if it was used in our ancestry
57
+ return !!this._parentDependencies?.isInvalidatedByChildlistChanges(affectedLocalNames);
58
+ }
59
+
28
60
  /**
29
61
  * Add an XPath to the dependencies
30
62
  *
@@ -8,6 +8,7 @@ import {
8
8
  import getInScopeContext from './getInScopeContext.js';
9
9
  import { Fore } from './fore.js';
10
10
  import DependentXPathQueries from './DependentXPathQueries.js';
11
+ import { getPath } from './xpath-path.js';
11
12
 
12
13
  /**
13
14
  * Mixin containing all general functions that are shared by all Fore element classes.
@@ -63,8 +64,18 @@ export default class ForeElementMixin extends HTMLElement {
63
64
  */
64
65
  this.inScopeVariables = new Map();
65
66
 
66
- this._dependencies = new DependentXPathQueries();
67
- this._dependencies.setParentDependencies(this.parent?.closest('[ref]')?._dependencies);
67
+ this.dependencies = new DependentXPathQueries();
68
+ this.ownerForm = null;
69
+ }
70
+
71
+ connectedCallback() {
72
+ if (this.parentElement) {
73
+ this.dependencies.setParentDependencies(this.parentElement?.closest('[ref]')?.dependencies);
74
+ }
75
+
76
+ // The fx-model linked to here won't ever change
77
+ this.model = this.getModel();
78
+ this.ownerForm = this.getOwnerForm();
68
79
  }
69
80
 
70
81
  /**
@@ -86,10 +97,12 @@ export default class ForeElementMixin extends HTMLElement {
86
97
  * @returns {import('./fx-fore.js').FxFore} The fx-fore element associated with this form node
87
98
  */
88
99
  getOwnerForm() {
100
+ if (this.ownerForm) {
101
+ return this.ownerForm;
102
+ }
89
103
  let currentElement = this;
90
104
  while (currentElement && currentElement.parentNode) {
91
105
  // console.log('current ', currentElement);
92
-
93
106
  if (currentElement.nodeName.toUpperCase() === 'FX-FORE') {
94
107
  return currentElement;
95
108
  }
@@ -100,14 +113,14 @@ export default class ForeElementMixin extends HTMLElement {
100
113
  currentElement = currentElement.parentNode;
101
114
  }
102
115
  }
103
- return currentElement;
116
+ return null;
104
117
  }
105
118
 
106
119
  /**
107
120
  * evaluation of fx-bind and UiElements differ in details so that each class needs it's own implementation.
108
121
  */
109
122
  evalInContext() {
110
- this._dependencies.resetDependencies();
123
+ this.dependencies.resetDependencies();
111
124
  // const inscopeContext = this.getInScopeContext();
112
125
  const model = this.getModel();
113
126
  if (!model) {
@@ -119,7 +132,7 @@ export default class ForeElementMixin extends HTMLElement {
119
132
  }
120
133
  if (this.hasAttribute('ref')) {
121
134
  inscopeContext = getInScopeContext(this.getAttributeNode('ref') || this, this.ref);
122
- this._dependencies.addXPath(this.ref);
135
+ this.dependencies.addXPath(this.ref);
123
136
  }
124
137
  if (!inscopeContext && this.getModel().instances.length !== 0) {
125
138
  // ### always fall back to default context with there's neither a 'context' or 'ref' present
@@ -132,16 +145,16 @@ export default class ForeElementMixin extends HTMLElement {
132
145
  this.nodeset = inscopeContext;
133
146
  } else if (Array.isArray(inscopeContext)) {
134
147
  /*
135
- inscopeContext.forEach(n => {
136
- if (XPathUtil.isSelfReference(this.ref)) {
137
- this.nodeset = inscopeContext;
138
- } else {
139
- const localResult = evaluateXPathToFirstNode(this.ref, n, this);
140
- // console.log('local result: ', localResult);
141
- this.nodeset.push(localResult);
142
- }
143
- });
144
- */
148
+ inscopeContext.forEach(n => {
149
+ if (XPathUtil.isSelfReference(this.ref)) {
150
+ this.nodeset = inscopeContext;
151
+ } else {
152
+ const localResult = evaluateXPathToFirstNode(this.ref, n, this);
153
+ // console.log('local result: ', localResult);
154
+ this.nodeset.push(localResult);
155
+ }
156
+ });
157
+ */
145
158
  // this.nodeset = evaluateXPathToFirstNode(this.ref, inscopeContext[0], this);
146
159
  this.nodeset = evaluateXPath(this.ref, inscopeContext[0], this);
147
160
  } else {
@@ -230,6 +243,9 @@ export default class ForeElementMixin extends HTMLElement {
230
243
  * @returns {import('./modelitem.js').ModelItem}
231
244
  */
232
245
  getModelItem() {
246
+ if (!this.getModel()) return;
247
+
248
+ // First try to find by node reference
233
249
  const mi = this.getModel().getModelItem(this.nodeset);
234
250
  if (mi) {
235
251
  this.modelItem = mi;
@@ -248,21 +264,39 @@ export default class ForeElementMixin extends HTMLElement {
248
264
  existed = this.nodeset ? this.getModel().getModelItem(this.nodeset) : null;
249
265
  }
250
266
 
251
- if (!existed) {
252
- const lazyCreatedModelItem = FxModel.lazyCreateModelItem(
253
- this.getModel(),
254
- this.ref,
255
- this.nodeset,
256
- this,
257
- );
258
- this.modelItem = lazyCreatedModelItem;
259
- return lazyCreatedModelItem;
267
+ // If we couldn't find by node reference, try to find by path
268
+ if (!existed && this.nodeset) {
269
+ // Get the path for the current nodeset
270
+ const instanceId = XPathUtil.resolveInstance(this, this.ref);
271
+ let targetNode =
272
+ this.nodeset.nodeType === Node.TEXT_NODE ? this.nodeset.parentNode : this.nodeset;
273
+
274
+ if (targetNode?.nodeType) {
275
+ const path = getPath(targetNode, instanceId);
276
+
277
+ // Try to find a ModelItem with this path
278
+ existed = this.getModel().modelItems.find(item => item.path === path);
279
+
280
+ if (existed) {
281
+ // Update the node reference in the existing ModelItem
282
+ existed.node = targetNode;
283
+ }
284
+ }
285
+ if (!existed) {
286
+ const lazyCreatedModelItem = FxModel.lazyCreateModelItem(
287
+ this.getModel(),
288
+ this.ref,
289
+ this.nodeset,
290
+ this,
291
+ );
292
+ this.modelItem = lazyCreatedModelItem;
293
+ return lazyCreatedModelItem;
294
+ }
260
295
  }
261
296
  this.modelItem = existed;
262
297
 
263
298
  return existed;
264
299
  }
265
-
266
300
  /**
267
301
  * Returns the effective value for the element.
268
302
  * a: look for 'value' attribute and if present evaluate it and return the resulting value