@jinntec/fore 2.8.0 → 3.0.1

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-bind.js CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  import { XPathUtil } from './xpath-util.js';
10
10
  import getInScopeContext from './getInScopeContext.js';
11
11
  import { getPath } from './xpath-path.js';
12
- import { evaluateXPathToFirstNode } from 'fontoxpath';
13
12
 
14
13
  /**
15
14
  * FxBind declaratively attaches constraints to nodes in the data (instances).
@@ -157,86 +156,75 @@ export class FxBind extends ForeElementMixin {
157
156
  */
158
157
  init(model) {
159
158
  this.model = model;
160
- // console.log('init binding ', this);
161
159
  this._getInstanceId();
162
160
  this.bindType = this.getModel().getInstance(this.instanceId).type;
163
- // console.log('binding type ', this.bindType);
164
161
 
162
+ // ✅ Always evaluate nodeset first (XML + JSON)
163
+ this._evalInContext();
164
+
165
+ // ✅ Build dependency graph for both types
166
+ this._buildBindGraph();
167
+
168
+ // ✅ Create modelitems for both types
165
169
  if (this.bindType === 'xml') {
166
- this._evalInContext();
167
- this._buildBindGraph();
168
170
  this._createModelItems();
171
+ } else if (this.bindType === 'json') {
172
+ this._createModelItemsForJSON();
169
173
  }
170
- // todo: support json
171
174
 
172
- // ### process child bindings
173
175
  this._processChildren(model);
174
176
  }
175
177
 
176
178
  _buildBindGraph() {
177
- if (this.bindType === 'xml') {
178
- this.nodeset.forEach(node => {
179
- const instance = XPathUtil.resolveInstance(this, this.ref);
180
-
181
- const path = getPath(node, instance);
182
- this.model.mainGraph.addNode(path, node);
183
-
184
- /* ### catching references in the 'ref' itself...
185
- todo: investigate cases where 'ref' attributes use predicates pointing to other nodes. These would not be handled
186
- in current implementation.
187
-
188
- General question: are there valid use-cases for using a 'filter' expression to narrow the nodeset
189
- where to apply constraints? Guess yes and if it's 'just' for reducing the amount of necessary modelItem objects.
179
+ // ✅ Works for XML and JSON (JSON nodes have getPath()/getPath() handles __jsonlens__)
180
+ this.nodeset.forEach(node => {
181
+ const instanceId = XPathUtil.resolveInstance(this, this.ref);
182
+ const path = getPath(node, instanceId);
190
183
 
191
- */
192
- // const foreignRefs = this.getReferences(this.ref);
184
+ this.model.mainGraph.addNode(path, node);
193
185
 
194
- if (this.calculate) {
195
- this.model.mainGraph.addNode(`${path}:calculate`, node);
196
- // Calculated values are a dependency of the model item.
197
- this.model.mainGraph.addDependency(path, `${path}:calculate`);
198
- }
186
+ if (this.calculate) {
187
+ this.model.mainGraph.addNode(`${path}:calculate`, node);
188
+ this.model.mainGraph.addDependency(path, `${path}:calculate`);
189
+ }
199
190
 
200
- const calculateRefs = this._getReferencesForProperty(this.calculate, node);
201
- if (calculateRefs.length !== 0) {
202
- this._addDependencies(calculateRefs, node, path, 'calculate');
203
- }
191
+ const calculateRefs = this._getReferencesForProperty(this.calculate, node);
192
+ if (calculateRefs.length !== 0) {
193
+ this._addDependencies(calculateRefs, node, path, 'calculate', instanceId);
194
+ }
204
195
 
205
- if (!this.calculate) {
206
- const readonlyRefs = this._getReferencesForProperty(this.readonly, node);
207
- if (readonlyRefs.length !== 0) {
208
- this._addDependencies(readonlyRefs, node, path, 'readonly');
209
- } else if (this.readonly) {
210
- this.model.mainGraph.addNode(`${path}:readonly`, node);
211
- }
196
+ if (!this.calculate) {
197
+ const readonlyRefs = this._getReferencesForProperty(this.readonly, node);
198
+ if (readonlyRefs.length !== 0) {
199
+ this._addDependencies(readonlyRefs, node, path, 'readonly', instanceId);
200
+ } else if (this.readonly) {
201
+ this.model.mainGraph.addNode(`${path}:readonly`, node);
212
202
  }
203
+ }
213
204
 
214
- // const requiredRefs = this.requiredReferences;
215
- const requiredRefs = this._getReferencesForProperty(this.required, node);
216
- if (requiredRefs.length !== 0) {
217
- this._addDependencies(requiredRefs, node, path, 'required');
218
- } else if (this.required) {
219
- this.model.mainGraph.addNode(`${path}:required`, node);
220
- }
205
+ const requiredRefs = this._getReferencesForProperty(this.required, node);
206
+ if (requiredRefs.length !== 0) {
207
+ this._addDependencies(requiredRefs, node, path, 'required', instanceId);
208
+ } else if (this.required) {
209
+ this.model.mainGraph.addNode(`${path}:required`, node);
210
+ }
221
211
 
222
- const relevantRefs = this._getReferencesForProperty(this.relevant, node);
223
- if (relevantRefs.length !== 0) {
224
- this._addDependencies(relevantRefs, node, path, 'relevant');
225
- } else if (this.relevant) {
226
- this.model.mainGraph.addNode(`${path}:relevant`, node);
227
- }
212
+ const relevantRefs = this._getReferencesForProperty(this.relevant, node);
213
+ if (relevantRefs.length !== 0) {
214
+ this._addDependencies(relevantRefs, node, path, 'relevant', instanceId);
215
+ } else if (this.relevant) {
216
+ this.model.mainGraph.addNode(`${path}:relevant`, node);
217
+ }
228
218
 
229
- const constraintRefs = this._getReferencesForProperty(this.constraint, node);
230
- if (constraintRefs.length !== 0) {
231
- this._addDependencies(constraintRefs, node, path, 'constraint');
232
- } else if (this.constraint) {
233
- this.model.mainGraph.addNode(`${path}:constraint`, node);
234
- this.model.mainGraph.addDependency(path, `${path}:constraint`);
235
- }
236
- });
237
- }
219
+ const constraintRefs = this._getReferencesForProperty(this.constraint, node);
220
+ if (constraintRefs.length !== 0) {
221
+ this._addDependencies(constraintRefs, node, path, 'constraint', instanceId);
222
+ } else if (this.constraint) {
223
+ this.model.mainGraph.addNode(`${path}:constraint`, node);
224
+ this.model.mainGraph.addDependency(path, `${path}:constraint`);
225
+ }
226
+ });
238
227
  }
239
-
240
228
  /**
241
229
  * Resolves a referenced ModelItem using the model's graph and node registry.
242
230
  * @param {string} refName
@@ -266,26 +254,25 @@ export class FxBind extends ForeElementMixin {
266
254
  * @param {string} path The path to the start of the reference
267
255
  * @param {string} property The property with this dependency
268
256
  */
269
- _addDependencies(refs, node, path, property) {
270
- // console.log('_addDependencies',path);
257
+ _addDependencies(refs, node, path, property, instanceId) {
271
258
  const nodeHash = `${path}:${property}`;
259
+
272
260
  if (refs.length !== 0) {
273
261
  if (!this.model.mainGraph.hasNode(nodeHash)) {
274
262
  this.model.mainGraph.addNode(nodeHash, node);
275
263
  }
264
+
276
265
  refs.forEach(ref => {
277
- const instance = XPathUtil.resolveInstance(this, path);
266
+ const otherPath = getPath(ref, instanceId);
278
267
 
279
- const otherPath = getPath(ref, instance);
280
- // console.log('otherPath', otherPath)
268
+ // keep old XML-only hack
269
+ if (this.bindType === 'xml' && otherPath.endsWith('text()[1]')) return;
281
270
 
282
- // todo: nasty hack to prevent duplicate pathes like 'a[1]' and 'a[1]/text()[1]' to end up as separate nodes in the graph
283
- if (!otherPath.endsWith('text()[1]')) {
284
- if (!this.model.mainGraph.hasNode(otherPath)) {
285
- this.model.mainGraph.addNode(otherPath, ref);
286
- }
287
- this.model.mainGraph.addDependency(nodeHash, otherPath);
271
+ if (!this.model.mainGraph.hasNode(otherPath)) {
272
+ this.model.mainGraph.addNode(otherPath, ref);
288
273
  }
274
+
275
+ this.model.mainGraph.addDependency(nodeHash, otherPath);
289
276
  });
290
277
  } else {
291
278
  this.model.mainGraph.addNode(nodeHash, node);
@@ -311,6 +298,23 @@ export class FxBind extends ForeElementMixin {
311
298
  return null;
312
299
  }
313
300
 
301
+ _createModelItemsForJSON() {
302
+ const fore = this.closest('fx-fore');
303
+ const instanceId = this.instanceId;
304
+
305
+ this.nodeset.forEach(jsonNode => {
306
+ const path = getPath(jsonNode, instanceId);
307
+
308
+ // ✅ ModelItem node should be the JSONNode itself (lens), NOT JSONLens
309
+ const newItem = new ModelItem(path, this.getBindingExpr(), jsonNode, this, instanceId, fore);
310
+
311
+ const alert = this.getAlert();
312
+ if (alert) newItem.addAlert(alert);
313
+
314
+ this.getModel().registerModelItem(newItem);
315
+ });
316
+ }
317
+
314
318
  /**
315
319
  * overwrites
316
320
  */
@@ -346,8 +350,11 @@ export class FxBind extends ForeElementMixin {
346
350
  const inst = this.getModel().getInstance(this.instanceId);
347
351
  if (inst.type === 'xml') {
348
352
  this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this);
353
+ } else if (inst.type === 'json') {
354
+ // ✅ JSON must also resolve the nodeset via XPath evaluation
355
+ this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this);
349
356
  } else {
350
- this.nodeset = this.ref;
357
+ this.nodeset = [];
351
358
  }
352
359
  }
353
360
  }
@@ -458,6 +465,33 @@ export class FxBind extends ForeElementMixin {
458
465
  }
459
466
 
460
467
  getReferences(propertyExpr) {
468
+ // For XML, DependencyNotifyingDomFacade reliably reports the nodes touched during evaluation.
469
+ // For JSON lens nodes, the domFacade hook does not fire (evaluation goes through our lens resolver),
470
+ // so we must extract lookup tokens and resolve them explicitly.
471
+
472
+ if (!propertyExpr) return [];
473
+
474
+ // JSON path: resolve dependencies by parsing lens lookups in the expression.
475
+ if (this.bindType === 'json') {
476
+ const touchedNodes = new Set();
477
+ const tokens = this._extractJsonLookupTokens(propertyExpr);
478
+
479
+ // Evaluate each token in the *current* context node (each item in nodeset)
480
+ this.nodeset.forEach(node => {
481
+ tokens.forEach(token => {
482
+ try {
483
+ const refs = evaluateXPathToNodes(token, node, this);
484
+ refs.forEach(r => touchedNodes.add(r));
485
+ } catch (_e) {
486
+ // ignore: dependency extraction must never break bind initialization
487
+ }
488
+ });
489
+ });
490
+
491
+ return Array.from(touchedNodes.values());
492
+ }
493
+
494
+ // XML path: use dom facade for accurate dependency tracking
461
495
  const touchedNodes = new Set();
462
496
  const domFacade = new DependencyNotifyingDomFacade(otherNode => touchedNodes.add(otherNode));
463
497
  this.nodeset.forEach(node => {
@@ -465,6 +499,27 @@ export class FxBind extends ForeElementMixin {
465
499
  });
466
500
  return Array.from(touchedNodes.values());
467
501
  }
502
+ _extractJsonLookupTokens(expr) {
503
+ if (!expr) return [];
504
+
505
+ const src = String(expr);
506
+ const tokens = new Set();
507
+
508
+ // instance('id')?a?b?c or instance('id')?*
509
+ const instRe = /instance\s*\([^)]*\)\s*(?:\?\s*\*|\?\s*[a-zA-Z_][\w-]*)+/g;
510
+ let m;
511
+ while ((m = instRe.exec(src)) !== null) {
512
+ if (m[0]) tokens.add(m[0].replace(/\s+/g, ''));
513
+ }
514
+
515
+ // relative lookups like ?title, ?year, ?ui, ?query (ignore ?*)
516
+ const relRe = /\?[a-zA-Z_][\w-]*/g;
517
+ while ((m = relRe.exec(src)) !== null) {
518
+ if (m[0] && m[0] !== '?*') tokens.add(m[0]);
519
+ }
520
+
521
+ return Array.from(tokens);
522
+ }
468
523
 
469
524
  /*
470
525
  static getReferencesForRef(ref,nodeset){