UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

436 lines (385 loc) 14.6 kB
import { DependencyNotifyingDomFacade } from './DependencyNotifyingDomFacade.js'; import ForeElementMixin from './ForeElementMixin.js'; import { ModelItem } from './modelitem.js'; import { evaluateXPathToBoolean, evaluateXPathToNodes, evaluateXPathToString, } from './xpath-evaluation.js'; import { XPathUtil } from './xpath-util.js'; import getInScopeContext from './getInScopeContext.js'; /** * FxBind declaratively attaches constraints to nodes in the data (instances). * * It's major task is to create ModelItem Objects for each Node in the data their ref is pointing to. * * References and constraint attributes use XPath statements to point to the nodes they are attributing. * * Note: why is fx-bind not extending BoundElement? Though fx-bind has a 'ref' attr it is not bound in the sense of * getting updates about changes of the bound nodes. Instead it acts as a factory for modelItems that are used by * BoundElements to track their state. * * @customElements */ export class FxBind extends ForeElementMixin { static READONLY_DEFAULT = false; static REQUIRED_DEFAULT = false; static RELEVANT_DEFAULT = true; static CONSTRAINT_DEFAULT = true; static TYPE_DEFAULT = 'xs:string'; constructor() { super(); this.nodeset = []; this.model = {}; this.contextNode = {}; this.inited = false; } connectedCallback() { // console.log('connectedCallback ', this); // this.id = this.hasAttribute('id')?this.getAttribute('id'):; this.constraint = this.getAttribute('constraint'); this.ref = this.getAttribute('ref'); this.readonly = this.getAttribute('readonly'); this.required = this.getAttribute('required'); this.relevant = this.getAttribute('relevant'); this.type = this.hasAttribute('type') ? this.getAttribute('type') : FxBind.TYPE_DEFAULT; this.calculate = this.getAttribute('calculate'); } /** * initializes the bind element by evaluating the binding expression. * * For each node referred to by the binding expr a ModelItem object is created. * * @param model */ init(model) { this.model = model; // console.log('init binding ', this); this._getInstanceId(); this.bindType = this.getModel().getInstance(this.instanceId).type; // console.log('binding type ', this.bindType); if (this.bindType === 'xml') { this._evalInContext(); this._buildBindGraph(); this._createModelItems(); } // todo: support json // ### process child bindings this._processChildren(model); } _buildBindGraph() { if (this.bindType === 'xml') { this.nodeset.forEach((node) => { const instance = XPathUtil.resolveInstance(this, this.ref); const path = XPathUtil.getPath(node, instance); this.model.mainGraph.addNode(path, node); /* ### catching references in the 'ref' itself... todo: investigate cases where 'ref' attributes use predicates pointing to other nodes. These would not be handled in current implementation. General question: are there valid use-cases for using a 'filter' expression to narrow the nodeset where to apply constraints? Guess yes and if it's 'just' for reducing the amount of necessary modelItem objects. */ // const foreignRefs = this.getReferences(this.ref); if (this.calculate) { this.model.mainGraph.addNode(`${path}:calculate`, node); // Calculated values are a dependency of the model item. this.model.mainGraph.addDependency(path, `${path}:calculate`); } const calculateRefs = this._getReferencesForProperty(this.calculate, node); if (calculateRefs.length !== 0) { this._addDependencies(calculateRefs, node, path, 'calculate'); } if (!this.calculate) { const readonlyRefs = this._getReferencesForProperty(this.readonly, node); if (readonlyRefs.length !== 0) { this._addDependencies(readonlyRefs, node, path, 'readonly'); } else if (this.readonly) { this.model.mainGraph.addNode(`${path}:readonly`, node); } } // const requiredRefs = this.requiredReferences; const requiredRefs = this._getReferencesForProperty(this.required, node); if (requiredRefs.length !== 0) { this._addDependencies(requiredRefs, node, path, 'required'); } else if (this.required) { this.model.mainGraph.addNode(`${path}:required`, node); } const relevantRefs = this._getReferencesForProperty(this.relevant, node); if (relevantRefs.length !== 0) { this._addDependencies(relevantRefs, node, path, 'relevant'); } else if (this.relevant) { this.model.mainGraph.addNode(`${path}:relevant`, node); } const constraintRefs = this._getReferencesForProperty(this.constraint, node); if (constraintRefs.length !== 0) { this._addDependencies(constraintRefs, node, path, 'constraint'); } else if (this.constraint) { this.model.mainGraph.addNode(`${path}:constraint`, node); this.model.mainGraph.addDependency(path, `${path}:constraint`); } }); } } /** * Add the dependencies of this bind * * @param {Node[]} refs The nodes that are referenced by this bind. these need to be resolved before * this bind can be resolved. * @param {Node} node The start of the reference * @param {string} path The path to the start of the reference * @param {string} property The property with this dependency */ _addDependencies(refs, node, path, property) { // console.log('_addDependencies',path); const nodeHash = `${path}:${property}`; if (refs.length !== 0) { if (!this.model.mainGraph.hasNode(nodeHash)) { this.model.mainGraph.addNode(nodeHash, node); } refs.forEach((ref) => { const instance = XPathUtil.resolveInstance(this, path); const otherPath = XPathUtil.getPath(ref, instance); // console.log('otherPath', otherPath) // todo: nasty hack to prevent duplicate pathes like 'a[1]' and 'a[1]/text()[1]' to end up as separate nodes in the graph if (!otherPath.endsWith('text()[1]')) { if (!this.model.mainGraph.hasNode(otherPath)) { this.model.mainGraph.addNode(otherPath, ref); } this.model.mainGraph.addDependency(nodeHash, otherPath); } }); } else { this.model.mainGraph.addNode(nodeHash, node); } } _processChildren(model) { const childbinds = this.querySelectorAll(':scope > fx-bind'); Array.from(childbinds).forEach((bind) => { // console.log('init child bind ', bind); bind.init(model); }); } getAlert() { if (this.hasAttribute('alert')) { return this.getAttribute('alert'); } const alertChild = this.querySelector('fx-alert'); if (alertChild) { return alertChild.innerHTML; } return null; } /** * overwrites */ _evalInContext() { const inscopeContext = getInScopeContext(this.getAttributeNode('ref') || this, this.ref); // reset nodeset this.nodeset = []; if (this.ref === '' || this.ref === null) { this.nodeset = inscopeContext; } else if (Array.isArray(inscopeContext)) { inscopeContext.forEach((n) => { if (XPathUtil.isSelfReference(this.ref)) { this.nodeset = inscopeContext; } else { // eslint-disable-next-line no-lonely-if if (this.ref) { const localResult = evaluateXPathToNodes(this.ref, n, this); localResult.forEach((item) => { this.nodeset.push(item); }); /* const localResult = fx.evaluateXPathToFirstNode(this.ref, n, null, {namespaceResolver: this.namespaceResolver}); this.nodeset.push(localResult); */ } // console.log('local result: ', localResult); // this.nodeset.push(localResult); } }); } else { const inst = this.getModel().getInstance(this.instanceId); if (inst.type === 'xml') { this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this); } else { this.nodeset = this.ref; } } } _createModelItems() { // console.log('#### ', thi+s.nodeset); if (Array.isArray(this.nodeset)) { // console.log('################################################ ', this.nodeset); // Array.from(this.nodeset).forEach((n, index) => { Array.from(this.nodeset).forEach((n) => { // console.log('node ',n); // this._createModelItem(n, index); this._createModelItem(n); }); } else { this._createModelItem(this.nodeset); } } /** * creates a ModelItem for given instance node. * * Please note that for textnode no ModelItem is created but instead the one of its parent is used which either * must exist and be initialized already when we hit the textnode. * @param node * @private */ // _createModelItem(node, index) { _createModelItem(node) { // console.log('_createModelItem node', node, index); /* this.calculateReferences = this._getReferencesForProperty(this.calculate,node); this.readOnlyReferences = this._getReferencesForProperty(this.readonly,node); this.requiredReferences = this._getReferencesForProperty(this.required,node); this.relevantReferences = this._getReferencesForProperty(this.relevant,node); this.constraintReferences = this._getReferencesForProperty(this.constraint,node); */ /* if bind is the dot expression we use the modelitem of the parent */ if (XPathUtil.isSelfReference(this.ref)) { const parentBoundElement = XPathUtil.getClosest('fx-bind[ref]', this.parentElement); // console.log('parent bound element ', parentBoundElement); if (parentBoundElement) { // todo: Could be fancier by combining them parentBoundElement.required = this.required; // overwrite parent property! } else { console.error('no parent bound element'); } return; } // let value = null; // const mItem = {}; /* let targetNode = {}; if (node.nodeType === node.TEXT_NODE) { // const parent = node.parentNode; // console.log('PARENT ', parent); targetNode = node.parentNode; } else { targetNode = node; } */ const targetNode = node; // const path = fx.evaluateXPath('path()',node); // const path = this.getPath(node); const instanceId = XPathUtil.resolveInstance(this, this.ref); const path = XPathUtil.getPath(node, instanceId); // const shortPath = this.shortenPath(path); // ### constructing default modelitem - will get evaluated during recalculate() // ### constructing default modelitem - will get evaluated during recalculate() // ### constructing default modelitem - will get evaluated during recalculate() // const newItem = new ModelItem(shortPath, const newItem = new ModelItem( path, this.getBindingExpr(), FxBind.READONLY_DEFAULT, FxBind.RELEVANT_DEFAULT, FxBind.REQUIRED_DEFAULT, FxBind.CONSTRAINT_DEFAULT, this.type, targetNode, this, instanceId ); const alert = this.getAlert(); if (alert) { newItem.addAlert(alert); } this.getModel().registerModelItem(newItem); } /** * Get the nodes that are referred by the given XPath expression * * @param {string} propertyExpr The XPath to get the referenced nodes from * * @return {Node[]} The nodes that are referenced by the XPath * * todo: DependencyNotifyingDomFacade reports back too much in some cases like 'a[1]' and 'a[1]/text[1]' */ _getReferencesForProperty(propertyExpr) { if (propertyExpr) { return this.getReferences(propertyExpr); } return []; } getReferences(propertyExpr) { const touchedNodes = new Set(); const domFacade = new DependencyNotifyingDomFacade(otherNode => touchedNodes.add(otherNode)); this.nodeset.forEach((node) => { evaluateXPathToString(propertyExpr, node, this, domFacade); }); return Array.from(touchedNodes.values()); } /* static getReferencesForRef(ref,nodeset){ if (ref && nodeset) { const touchedNodes = new Set(); const domFacade = new DependencyNotifyingDomFacade(otherNode => touchedNodes.add(otherNode)); nodeset.forEach(node => { evaluateXPathToString(ref, node, this, domFacade); }); return Array.from(touchedNodes.values()); } return []; } */ _initBooleanModelItemProperty(property, node) { // evaluate expression to boolean const propertyExpr = this[property]; // console.log('####### ', propertyExpr); const result = evaluateXPathToBoolean(propertyExpr, node, this); return result; } static shortenPath(path) { const steps = path.split('/'); let result = ''; for (let i = 2; i < steps.length; i += 1) { const step = steps[i]; if (step.indexOf('{}') !== -1) { const q = step.split('{}'); result += `/${q[1]}`; } else { result += `/${step}`; } } return result; } /** * return the instance id this bind is associated with. Resolves upwards in binds to either find an expr containing * and instance() function or if not found return 'default'. * @private */ _getInstanceId() { const bindExpr = this.getBindingExpr(); // console.log('_getInstanceId bindExpr ', bindExpr); if (bindExpr.startsWith('instance(')) { this.instanceId = XPathUtil.getInstanceId(bindExpr); return; } if (!this.instanceId && this.parentNode.nodeName === 'FX-BIND') { let parent = this.parentNode; while (parent && !this.instanceId) { const ref = parent.getBindingExpr(); if (ref.startsWith('instance(')) { this.instanceId = XPathUtil.getInstanceId(ref); return; } if (parent.parentNode.nodeName !== 'FX-BIND') { this.instanceId = 'default'; break; } parent = parent.parentNode; } } this.instanceId = 'default'; } } if (!customElements.get('fx-bind')) { customElements.define('fx-bind', FxBind); }