@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
855 lines (753 loc) • 29.7 kB
JavaScript
import { DepGraph } from './dep_graph.js';
import { Fore } from './fore.js';
import './fx-instance.js';
import { ModelItem } from './modelitem.js';
import { parseJsonRef, getPath } from './xpath-path.js';
import { evaluateXPath, evaluateXPathToBoolean, evaluateXPathToNodes } from './xpath-evaluation.js';
import { XPathUtil } from './xpath-util.js';
import { getLensForNode } from './json/JSONNode.js';
/**
* The model of this Fore scope. It holds all the intances, binding, submissions and custom
* functions that as required.
*
* The model is updated by executing rebuild (as needed), recalculate and revalidate in turn.
*
* After the cycle is run all modelItems have updated their stete to reflect latest computations.
*
*/
export class FxModel extends HTMLElement {
static dataChanged = false;
constructor() {
super();
// this.id = '';
/**
* @type {import('./fx-instance.js').FxInstance[]}
*/
this.instances = [];
/**
* @type {import('./modelitem.js').ModelItem[]}
*/
this.modelItems = [];
this.defaultContext = {};
this.changed = [];
// this.mainGraph = new DepGraph(false);
this.inited = false;
this.modelConstructed = false;
this.attachShadow({ mode: 'open' });
this.computes = 0;
this.fore = {};
/**
* @type {import('./fx-bind.js').FxBind[]}
*/
this.binds = [];
}
/**
* @returns {import('./fx-fore.js').FxFore}
*/
get formElement() {
return this.parentElement;
}
connectedCallback() {
// console.log('connectedCallback ', this);
this.setAttribute('inert', 'true');
this.shadowRoot.innerHTML = `
<slot></slot>
`;
/*
this.addEventListener('model-construct-done', () => {
// this.modelConstructed = true;
// console.log('model-construct-done fired ', this.modelConstructed);
// console.log('model-construct-done fired ', e.detail.model.instances);
},
{ once: true },
);
*/
this.skipUpdate = false;
this.fore = this.parentNode;
}
/**
* Get the correct fx-bind for this element. Assumes the refs of all binds are always downwards.
*
* @param {ChildNode | Attr} elementOrAttribute - the element or attribute to resolve
*
* @returns {import('./fx-bind.js').FxBind | null}
*/
getBindForElement(elementOrAttribute) {
if (typeof elementOrAttribute !== 'object' || !('nodeType' in elementOrAttribute)) {
// We only do binds over nodes. Not JSON.
return null;
}
/**
* @type {import('./fx-bind.js').FxBind | FxModel}
*/
let bindForParent;
const parent =
elementOrAttribute.nodeType === elementOrAttribute.ATTRIBUTE_NODE
? elementOrAttribute.ownerElement
: elementOrAttribute.parentNode;
if (!parent?.parentElement) {
// The root. Search from here
bindForParent = this;
} else {
bindForParent = this.getBindForElement(parent);
}
if (!bindForParent) {
return null;
}
/**
* @type {import('./fx-bind.js').FxBind[]}
*/
const childBinds = Array.from(bindForParent.children).filter(c => c.nodeName === 'FX-BIND');
for (const childBind of childBinds) {
const ref = childBind.ref;
const matches = evaluateXPathToNodes(ref, parent, childBind);
if (matches.includes(elementOrAttribute)) {
return childBind;
}
}
return null;
}
/**
* Lazily create a ModelItem for nodes not explicitly bound via fx-bind
* @param {FxModel} model The model to create a model item for
* @param {string} ref The XPath ref that led to this model item
* @param {Node} node The node the XPath led to
* @param {ForeElementMixin)} formElement The form element making this model. Used to resolve variables against
* @returns {ModelItem}
*/
static lazyCreateModelItem(model, ref, node, formElement) {
const instanceId = XPathUtil.resolveInstance(formElement, ref);
const instance = model.getInstance(instanceId);
const fore = model.formElement;
if (fore?.createNodes && (node === null || node === undefined)) {
const mi = new ModelItem(undefined, ref, null, null, instanceId, fore);
mi.isSynthetic = true;
model.registerModelItem(mi);
return mi;
}
if (node === null || node === undefined) return null;
let targetNode = Array.isArray(node) ? node[0] : node;
// Wrap JSON primitives / raw values into a lens node when needed
if (instance.type === 'json') {
const parentLens = instance.nodeset;
const parsedRef = parseJsonRef(ref);
if (parsedRef && parsedRef.steps && parsedRef.steps.length > 0) {
const key = parsedRef.steps[parsedRef.steps.length - 1];
targetNode = getLensForNode(targetNode, parentLens, key, instanceId);
}
}
// Compute canonical path
let path = null;
if (targetNode?.nodeType || targetNode?.__jsonlens__) {
path = getPath(targetNode, instanceId);
}
const isLensObject =
!!targetNode && typeof targetNode === 'object' &&
typeof targetNode.get === 'function' && typeof targetNode.set === 'function';
// If ModelItem for same path exists, RETARGET it (node OR lens)
if (path) {
const existingModelItem = model.modelItems.find(mi => mi.path === path);
if (existingModelItem) {
if (isLensObject) {
if (existingModelItem.lens !== targetNode) {
existingModelItem.lens = targetNode;
existingModelItem.node = null;
}
} else {
if (existingModelItem.node !== targetNode) {
existingModelItem.node = targetNode;
existingModelItem.lens = null;
}
}
return existingModelItem;
}
}
const mi = new ModelItem(
path,
ref,
targetNode,
model.getBindForElement(targetNode),
instanceId,
fore,
);
mi.isSynthetic = true;
model.registerModelItem(mi);
return mi;
}
/**
* modelConstruct starts actual processing of the model by
*
* 1. loading instances if present or constructing one
* 2. calling updateModel to run the model update cycle of rebuild, recalculate and revalidate
*
* @event model-construct-done is fired once all instances have be loaded or after generating instance
*
*/
async modelConstruct() {
console.info(`📌 model-construct for #${this.parentNode.id}`);
// this.dispatchEvent(new CustomEvent('model-construct', { detail: this }));
Fore.dispatch(this, 'model-construct', { model: this });
// console.time('instance-loading');
const instances = this.querySelectorAll('fx-instance');
if (instances.length > 0) {
const promises = [];
instances.forEach(instance => {
promises.push(instance.init());
});
// Wait until all the instances are built
await Promise.all(promises);
this.instances = Array.from(instances);
// Build in-memory variable bindings for instances (Variant A: no <fx-var> DOM nodes).
// These bindings are merged into XPath variable resolution by xpath-evaluation.js.
if (this.formElement) {
const bindings = Object.create(null);
// $default always points to the model's default instance (first instance)
// IMPORTANT: For JSON instances, bind RAW JS root so `?` lookup works.
try {
const defInst = this.getDefaultInstance();
if (defInst) {
const t = (defInst.getAttribute && defInst.getAttribute('type')) || defInst.type;
bindings.default = t === 'json' ? defInst.getInstanceData() : defInst.getDefaultContext();
}
} catch (_e) {
// ignore
}
// Also expose $<id> for explicitly id'ed instances
this.instances.forEach(inst => {
const explicitId = inst.getAttribute('id');
if (!explicitId) return;
// Do not overwrite $default binding; $default remains the first instance
if (explicitId === 'default') return;
const t = (inst.getAttribute && inst.getAttribute('type')) || inst.type;
bindings[explicitId] = t === 'json' ? inst.getInstanceData() : inst.getDefaultContext();
});
this.formElement._instanceVarBindings = bindings;
}
// console.log('_modelConstruct this.instances ', this.instances);
// Await until the model-construct-done event is handled off
this.modelConstructed = true;
await Fore.dispatch(this, 'model-construct-done', { model: this });
this.inited = true;
this.updateModel();
} else {
// ### if there's no instance one will created
console.log(`### <<<<< dispatching model-construct-done for '${this.fore.id}' >>>>>`);
this.modelConstructed = true;
await this.dispatchEvent(
new CustomEvent('model-construct-done', {
composed: false,
bubbles: true,
detail: { model: this },
}),
);
}
const functionlibImports = Array.from(this.querySelectorAll('fx-functionlib'));
await Promise.all(functionlibImports.map(lib => lib.readyPromise));
// console.timeEnd('instance-loading');
this.inited = true;
}
registerModelItem(modelItem) {
if (!modelItem) return null;
const path = modelItem.path;
const resetComputedState = mi => {
// Tabula rasa for computed facets; keep identity (boundControls/observers)
mi.readonly = ModelItem.READONLY_DEFAULT;
mi.relevant = ModelItem.RELEVANT_DEFAULT;
mi.required = ModelItem.REQUIRED_DEFAULT;
mi.constraint = ModelItem.CONSTRAINT_DEFAULT;
mi.type = ModelItem.TYPE_DEFAULT;
// common extras in Fore's ModelItem
if ('valid' in mi) mi.valid = true;
if ('enabled' in mi) mi.enabled = true;
mi.changed = false;
// observer/dependency bookkeeping (safe to reset; will be rebuilt)
if (mi.dependencies && typeof mi.dependencies.clear === 'function') mi.dependencies.clear();
if (mi.stateExpressions) mi.stateExpressions = {};
if (mi.state) mi.state = {};
};
const retarget = (target, source) => {
// point to current backing node/lens
if (source.lens) {
target.lens = source.lens;
target.node = null;
} else if (source.node) {
target.node = source.node;
target.lens = null;
}
// keep metadata current
if (source.ref) target.ref = source.ref;
if (source.bind) target.bind = source.bind;
if (source.instanceId) target.instanceId = source.instanceId;
if (source.fore) target.fore = source.fore;
// ✅ IMPORTANT: do NOT copy value!
// For XML nodes, assigning `value` sets `node.textContent` and can delete child elements.
resetComputedState(target);
if (!target.boundControls) target.boundControls = [];
};
// ---- rebuild reuse-by-path (approach A) ----
if (path && this._prevModelItemsByPath) {
const prev = this._prevModelItemsByPath.get(path);
if (prev) {
retarget(prev, modelItem);
if (!this.modelItems.includes(prev)) {
this.modelItems.push(prev);
}
this._prevModelItemsByPath.delete(path);
return prev;
}
}
// ---- normal path ----
if (!path) {
// No path => can't reuse; keep as-is
this.modelItems.push(modelItem);
return modelItem;
}
const existing = this.modelItems.find(mi => mi.path === path);
if (!existing) {
// New canonical item
resetComputedState(modelItem);
this.modelItems.push(modelItem);
return modelItem;
}
// Re-target canonical item
retarget(existing, modelItem);
return existing;
}
/**
* update action triggering the update cycle
*/
updateModel() {
// console.time('updateModel');
this.rebuild();
/*
if (this.skipUpdate){
console.info('%crecalculate/revalidate skipped - no bindings', 'font-style: italic; background: #90a4ae; color:lightgrey; padding:0.3rem 5rem 0.3rem 0.3rem;display:block;width:100%;');
return;
}
*/
this.recalculate();
this.revalidate();
// console.log('updateModel finished with modelItems ', this.modelItems);
// console.timeEnd('updateModel');
}
/**
* (Recursively) remove the model item of a node.
* @param {Node} node - The node for which to remove the model item
*/
removeModelItem(node) {
if (!node) return;
// Support both XML nodes (mi.node) and JSON lens nodes (mi.lens)
const index = this.modelItems.findIndex(mi => mi.node === node || mi.lens === node);
// The model item is not always there. Might be the case if a node is 'skipped' during rendering.
// It may still have descendants that can have model items.
if (index !== -1) {
const mi = this.modelItems[index];
// IMPORTANT:
// Before removing the ModelItem, enqueue all observers (bound UI controls) for refresh.
// Otherwise, deleting a bound node can orphan controls (eg. fx-group) because their ModelItem
// disappears before the refresh scheduler can reach them.
try {
const fore = this.formElement || this.parentNode || mi.fore;
if (fore && typeof fore.addToBatchedNotifications === 'function' && mi && mi.observers) {
mi.observers.forEach(observer => {
if (observer && typeof observer.refresh === 'function') {
fore.addToBatchedNotifications(observer);
}
});
}
} catch (_e) {
// ignore
}
this.modelItems.splice(index, 1);
}
// Recurse for XML descendants only
if (node.childNodes) {
for (const child of Array.from(node.childNodes)) {
this.removeModelItem(child);
}
}
}
rebuild() {
console.log(`🔷 rebuild() '${this.fore.id}'`);
// Build a lookup for existing ModelItems so we can reuse them by path (approach A)
const prevItems = Array.isArray(this.modelItems) ? this.modelItems : [];
this._prevModelItemsByPath = new Map();
prevItems.forEach(mi => {
if (mi && mi.path) this._prevModelItemsByPath.set(mi.path, mi);
});
this.mainGraph = new DepGraph(false);
this.modelItems = [];
const binds = this.querySelectorAll('fx-model > fx-bind');
if (binds.length === 0) {
this.skipUpdate = true;
this._prevModelItemsByPath = null;
return;
}
binds.forEach(bind => bind.init(this));
if (this.formElement.createNodes) {
this.formElement.initData();
}
// Drop unused previous ModelItems (not re-registered this rebuild)
this._prevModelItemsByPath = null;
console.log('mainGraph', this.mainGraph);
console.log('rebuild mainGraph calc order', this.mainGraph.overallOrder());
Fore.dispatch(this, 'rebuild-done', { maingraph: this.mainGraph });
}
/**
* recalculation of all modelItems. Uses dependency graph to determine order of computation.
*
* todo: use 'changed' flag on modelItems to determine subgraph for recalculation. Flag already exists but is not used.
*/
async recalculate() {
if (!this.mainGraph) {
return;
}
console.log(`🔷🔷 recalculate() '${this.fore.id}'`);
// console.log('changed nodes ', this.changed);
this.computes = 0;
this.subgraph = new DepGraph(false);
// ### create the subgraph for all changed modelItems
if (this.changed.length !== 0) {
// ### build the subgraph
this.changed.forEach(modelItem => {
this.subgraph.addNode(modelItem.path, modelItem.node);
// const dependents = this.mainGraph.dependantsOf(modelItem.path, false);
// this._addSubgraphDependencies(modelItem.path);
if (this.mainGraph.hasNode(modelItem.path)) {
// const dependents = this.mainGraph.directDependantsOf(modelItem.path)
const all = this.mainGraph.dependantsOf(modelItem.path, false);
const dependents = all.reverse();
if (dependents.length !== 0) {
dependents.forEach(dep => {
// const subdep = this.mainGraph.dependentsOf(dep,false);
// subgraph.addDependency(dep, modelItem.path);
const val = this.mainGraph.getNodeData(dep);
this.subgraph.addNode(dep, val);
if (dep.includes(':')) {
const path = dep.substring(0, dep.indexOf(':'));
this.subgraph.addNode(path, val);
const deps = this.mainGraph.dependentsOf(modelItem.path, false);
// if we find the dep to be first in list of dependents we are dependent on ourselves not adding edge to modelItem.path
if (deps.indexOf(dep) !== 0) {
this.subgraph.addDependency(dep, modelItem.path);
}
}
// subgraph.addDependency(dep,modelItem.path);
});
}
}
});
// ### compute the subgraph
const ordered = this.subgraph.overallOrder(false);
ordered.forEach(path => {
if (this.mainGraph.hasNode(path)) {
const node = this.mainGraph.getNodeData(path);
this.compute(node, path);
}
});
/*
const toRefresh = [...this.changed];
this.formElement.toRefresh = toRefresh;
*/
this.changed = [];
Fore.dispatch(this, 'recalculate-done', { graph: this.subgraph, computes: this.computes });
} else {
const v = this.mainGraph.overallOrder(false);
v.forEach(path => {
const node = this.mainGraph.getNodeData(path);
this.compute(node, path);
});
Fore.dispatch(this, 'recalculate-done', { graph: this.mainGraph, computes: this.computes });
}
console.log(`${this.parentElement.id} recalculate finished with modelItems `, this.modelItems);
}
/*
_addSubgraphDependencies(path){
const dependents = this.mainGraph.directDependantsOf(path)
const alreadyInGraph = this.subgraph.incomingEdges[path];
// const alreadyInGraph = path in this.subgraph;
if(dependents.length !== 0 && alreadyInGraph.length === 0){
dependents.forEach(dep => {
// const val= this.mainGraph.getNodeData(dep);
// this.subgraph.addNode(dep,val);
if(dep.includes(':')){
const subpath = dep.substring(0, dep.indexOf(':'));
// this.subgraph.addNode(subpath,val);
this.subgraph.addDependency(subpath,dep);
this.subgraph.addDependency(dep,path);
/!*
const subdeps = this.mainGraph.directDependantsOf(path);
console.log('subdeps',path, subdeps);
subdeps.forEach(sdep => {
const sval= this.mainGraph.getNodeData(sdep);
this.subgraph.addNode(sdep,sval);
console.log('subdep',sdep);
});
*!/
if(this.subgraph.incomingEdges[dep] === 0){
this._addSubgraphDependencies(subpath)
}
}
});
}
}
*/
/**
* (re-) computes a modelItem.
* @param {Node} node - the node the modelItem is attached to
* @param {string} path - the canonical XPath of the node
*/
compute(node, path) {
// Nodes in dep graphs can be transient during JSON insert/rebuild windows.
// Preserve depGraph semantics, but avoid crashing when a ModelItem is momentarily missing.
// Resolve facet property (eg. "$data/movies[3]/title:relevant")
const isFacetPath = typeof path === 'string' && path.includes(':');
if (!isFacetPath) return;
const property = path.split(':')[1];
if (!property) return;
// Try to resolve the model item primarily by node, but fall back to canonical path.
// The depGraph stores node data that may not be the same object identity after lens rebuild.
let modelItem = this.getModelItem(node);
if (!modelItem && node && (node.__jsonlens__ === true || typeof node.getPath === 'function')) {
try {
const instanceId = node.instanceId || XPathUtil.resolveInstance(this, path);
const canonical = getPath(node, instanceId);
modelItem = this.getModelItem(canonical);
} catch (_e) {
// ignore
}
}
// If still missing, fall back to the prefix path of the facet node.
// eg. "$data/movies[3]/title:relevant" => "$data/movies[3]/title"
if (!modelItem) {
const basePath = path.substring(0, path.indexOf(':'));
modelItem = this.getModelItem(basePath);
}
// ✅ Minimal fix: don't crash the update cycle if the ModelItem doesn't exist.
// This can happen during insert/delete when rebuild retargeting is in progress.
if (!modelItem) {
return;
}
if (modelItem && typeof path === 'string') {
const expr = modelItem.bind ? modelItem.bind[property] : null;
const context = modelItem.node || modelItem.lens;
if (property === 'calculate') {
const compute = evaluateXPath(expr, context, this);
modelItem.value = compute;
modelItem.readonly = true; // calculated nodes are always readonly
modelItem.notify(); // Notify observers directly
} else if (property !== 'constraint' && property !== 'type') {
// ### re-compute the Boolean value of all facets expect 'constraint' and 'type' which are handled in revalidate()
if (expr) {
const compute = evaluateXPathToBoolean(expr, context, this);
modelItem[property] = compute;
// modelItem.notify(); // Notify observers directly
this.fore.addToBatchedNotifications(modelItem);
}
}
this.computes += 1;
}
}
/**
* Iterates all modelItems to calculate the validation status.
*
* Model alerts are given on 'fx-bind' elements as either attribute `alert` or as `fx-alert` child elements.
*
* During model-construct all model alerts are added to the modelItem if any
*
* to revalidate:
* Gets the `constraint` attribute declaration from modelItem.bind
* Computes the XPath to a Boolean
* Updates the modelItem.constraint property
*
* todo: type checking
* todo: run browser validation API
*
*/
revalidate() {
if (this.modelItems.length === 0) return true;
console.log(`🔷🔷🔷 revalidate() '${this.fore.id}'`);
// reset submission validation
// this.parentNode.classList.remove('submit-validation-failed')
let valid = true;
this.modelItems.forEach(modelItem => {
// console.log('validating node ', modelItem.node);
const { bind } = modelItem;
if (bind) {
/*
todo: investigate why bind is an element when created in fx-bind.init() and an fx-bind object when
created lazily.
*/
if (typeof bind.hasAttribute === 'function' && bind.hasAttribute('constraint')) {
const constraint = bind.getAttribute('constraint');
if (constraint && modelItem.node) {
const compute = evaluateXPathToBoolean(constraint, modelItem.node, this);
// console.log('modelItem validity computed: ', compute);
modelItem.constraint = compute;
// this.formElement.addToRefresh(modelItem); // let fore know that modelItem needs refresh
modelItem.notify(); // Notify observers directly
if (!compute) {
console.log('validation failed on modelitem ', modelItem);
valid = false;
}
}
}
if (typeof bind.hasAttribute === 'function' && bind.hasAttribute('required')) {
const required = bind.getAttribute('required');
if (required) {
const compute = evaluateXPathToBoolean(required, modelItem.node, this);
// console.log('modelItem required computed: ', compute);
modelItem.required = compute;
// this.formElement.addToRefresh(modelItem); // let fore know that modelItem needs refresh
modelItem.notify(); // Notify observers directly
if (modelItem.required && !modelItem.node.textContent) {
/*
console.log(
'node is required but has no value ',
XPathUtil.getDocPath(modelItem.node),
);
*/
valid = false;
}
// if (!compute) valid = false;
/*
if (!this.modelConstructed) {
// todo: get alert from attribute or child element
const alert = bind.getAlert();
if (alert) {
modelItem.addAlert(alert);
}
}
*/
}
}
}
});
console.log('modelItems after revalidate: ', this.modelItems);
console.log('changed after revalidate: ', this.changed);
console.log(
'changed after revalidate changed: ',
Array.from(this.parentNode._localNamesWithChanges),
);
console.log(
'changed after revalidate batchedNotifications: ',
Array.from(this.parentNode.batchedNotifications),
);
return valid;
}
addChanged(modelItem) {
if (this.inited) {
this.changed.push(modelItem);
}
}
/**
* Find a ModelItem by exact node or path
* @param {Node|string} nodeOrPath
* @returns {ModelItem|null}
*/
getModelItem(nodeOrPath) {
if (nodeOrPath == null) return null;
// Path lookup
if (typeof nodeOrPath === 'string') {
const key = nodeOrPath.includes(':') ? nodeOrPath.substring(0, nodeOrPath.indexOf(':')) : nodeOrPath;
return this.modelItems.find(mi => mi.path === key) || null;
}
// Node/lens lookup
return (
this.modelItems.find(mi => mi.node === nodeOrPath || mi.lens === nodeOrPath) || null
);
}
/**
* get the default evaluation context for this model.
* @returns {Element}
*/
getDefaultContext() {
return this.instances[0].getDefaultContext();
}
/**
* @returns {import('./fx-instance.js').FxInstance}
*/
getDefaultInstance() {
/*
if (this.instances.length === 0) {
throw new Error('No instances defined. Fore cannot work without any <data/> elements.');
}
*/
if (this.instances.length) {
return this.instances[0];
}
return this.getInstance('default');
}
getDefaultInstanceData() {
return this.instances[0].getInstanceData();
}
/**
* @returns {import('./fx-instance.js').FxInstance}
*/
getInstance(id) {
let found = null;
// default instance is first instance in this model
if (id === 'default') {
found = this.instances[0];
}
// ### lookup in local instances first
if (!found) {
const instArray = Array.from(this.instances);
found = instArray.find(inst => inst.id === id);
}
// ### lookup in parent Fore if present (shared instances)
if (!found) {
const parentFore =
this.fore.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
? this.fore.parentNode.host.closest('fx-fore')
: this.fore.parentNode.closest('fx-fore');
if (parentFore) {
const parentInstances = parentFore.getModel().instances;
const shared = parentInstances.filter(inst => inst.hasAttribute('shared'));
found = shared.find(inst => inst.id === id);
}
}
// ### search for shared instances in the light DOM (legacy)
if (!found) {
found = document.querySelector(`fx-instance[id="${id}"][shared]`);
}
// ### NEW: search for shared instances inside other fx-fore shadowRoots
// This is required when a fore keeps its model/instances in its own shadow DOM
// and sibling fores want to consume that instance via instance('id').
if (!found) {
const allFores = Array.from(document.querySelectorAll('fx-fore'));
for (const fore of allFores) {
// light DOM inside fore (in case someone authoring without shadow)
const light = fore.querySelector?.(`fx-instance[id="${id}"][shared]`);
if (light) {
found = light;
break;
}
// shadow DOM inside fore (common in your demos)
const shadow = fore.shadowRoot?.querySelector?.(`fx-instance[id="${id}"][shared]`);
if (shadow) {
found = shadow;
break;
}
}
}
if (found) return found;
if (!found && this.fore.strict) {
Fore.dispatch(this, 'error', {
origin: this,
message: `Instance '${id}' does not exist`,
level: 'Error',
});
}
return null;
}
evalBinding(bindingExpr) {
// console.log('MODEL.evalBinding ', bindingExpr);
// default context of evaluation is always the default instance
const result = this.instances[0].evalXPath(bindingExpr);
return result;
}
}
if (!customElements.get('fx-model')) {
customElements.define('fx-model', FxModel);
}