UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

305 lines (283 loc) 8.79 kB
import { XPathUtil } from './xpath-util.js'; import { FxModel } from './fx-model.js'; import { evaluateXPath, evaluateXPathToFirstNode, evaluateXPathToString, } from './xpath-evaluation.js'; import getInScopeContext from './getInScopeContext.js'; import { Fore } from './fore.js'; import DependentXPathQueries from './DependentXPathQueries.js'; /** * Mixin containing all general functions that are shared by all Fore element classes. * @extends {HTMLElement} */ export default class ForeElementMixin extends HTMLElement { static get properties() { return { /** * context object for evaluation */ context: { type: Object, }, /** * the model of this element */ model: { type: Object, }, /** * The modelitem object associated to the bound node holding the evaluated state. */ modelItem: { type: Object, }, /** * the node(s) bound by this element */ nodeset: { type: Object, }, /** * XPath binding expression pointing to bound node */ ref: { type: String, }, inScopeVariables: { type: Map, }, }; } constructor() { super(); this.context = null; this.model = null; this.modelItem = null; this.ref = this.hasAttribute('ref') ? this.getAttribute('ref') : ''; /** * @type {Map<string, import('./fx-var.js').FxVariable>} */ this.inScopeVariables = new Map(); this._dependencies = new DependentXPathQueries(); this._dependencies.setParentDependencies(this.parent?.closest('[ref]')?._dependencies); } /** * @returns {import('./fx-model.js').FxModel} */ getModel() { // console.log('getModel this ', this); if (this.model) { return this.model; } // const ownerForm = this.closest('fx-fore'); // const ownerForm = this.getOwnerForm(this); const ownerForm = this.getOwnerForm(); return ownerForm.querySelector('fx-model'); } /** * * @returns {import('./fx-fore.js').FxFore} The fx-fore element associated with this form node */ getOwnerForm() { let currentElement = this; while (currentElement && currentElement.parentNode) { // console.log('current ', currentElement); if (currentElement.nodeName.toUpperCase() === 'FX-FORE') { return currentElement; } if (currentElement.parentNode instanceof DocumentFragment) { currentElement = currentElement.parentNode.host; } else { currentElement = currentElement.parentNode; } } return currentElement; } /** * evaluation of fx-bind and UiElements differ in details so that each class needs it's own implementation. */ evalInContext() { this._dependencies.resetDependencies(); // const inscopeContext = this.getInScopeContext(); const model = this.getModel(); if (!model) { return; } let inscopeContext; if (this.hasAttribute('context')) { inscopeContext = getInScopeContext(this.getAttributeNode('context') || this, this.context); } if (this.hasAttribute('ref')) { inscopeContext = getInScopeContext(this.getAttributeNode('ref') || this, this.ref); this._dependencies.addXPath(this.ref); } if (!inscopeContext && this.getModel().instances.length !== 0) { // ### always fall back to default context with there's neither a 'context' or 'ref' present inscopeContext = this.getModel().getDefaultInstance().getDefaultContext(); // console.warn('no in scopeContext for ', this); // console.warn('using default context ', this); // return; } if (this.ref === '') { this.nodeset = inscopeContext; } else if (Array.isArray(inscopeContext)) { /* inscopeContext.forEach(n => { if (XPathUtil.isSelfReference(this.ref)) { this.nodeset = inscopeContext; } else { const localResult = evaluateXPathToFirstNode(this.ref, n, this); // console.log('local result: ', localResult); this.nodeset.push(localResult); } }); */ // this.nodeset = evaluateXPathToFirstNode(this.ref, inscopeContext[0], this); this.nodeset = evaluateXPath(this.ref, inscopeContext[0], this); } else { // this.nodeset = fx.evaluateXPathToFirstNode(this.ref, inscopeContext, null, {namespaceResolver: this.namespaceResolver}); if (!inscopeContext) return; const { nodeType } = inscopeContext; if (nodeType && !XPathUtil.isAbsolutePath(this.ref)) { this.nodeset = evaluateXPathToFirstNode(this.ref, inscopeContext, this); } else { [this.nodeset] = evaluateXPath(this.ref, inscopeContext, this); } } // console.log('UiElement evaluated to nodeset: ', this.nodeset); } /** * resolves template expressions for a single attribute * @param {string} expr an attribute value containing curly brackets containing XPath expressions to evaluate * @param {Node} node the attribute node used for scoped resolution * @returns {string} * @protected */ evaluateAttributeTemplateExpression(expr, node) { const matches = expr.match(/{[^}]*}/g); if (matches) { matches.forEach(match => { // console.log('match ', match); const naked = match.substring(1, match.length - 1); const inscope = getInScopeContext(node, naked); const result = evaluateXPathToString(naked, inscope, this); const replaced = expr.replaceAll(match, result); // console.log('replacing ', expr, ' with ', replaced); expr = replaced; }); } return expr; } isNotBound() { return !this.hasAttribute('ref'); } isBound() { return this.hasAttribute('ref'); } getBindingExpr() { if (this.hasAttribute('ref')) { return this.getAttribute('ref'); } // try to get closest parent bind const parent = XPathUtil.getClosest('[ref]', this.parentNode); if (!parent) { return 'instance()'; // the default instance } return parent.getAttribute('ref'); } /** * @returns {import('./fx-instance.js').FxInstance} */ getInstance() { if (this.ref.startsWith('instance(')) { const instId = XPathUtil.getInstanceId(this.ref); return this.getModel().getInstance(instId); } return this.getModel().getInstance('default'); } _getParentBindingElement(start) { if (start.parentNode.host) { const { host } = start.parentNode; if (host.hasAttribute('ref')) { return host; } } else if (start.parentNode) { if (start.parentNode.hasAttribute('ref')) { return this.parentNode; } this._getParentBindingElement(this.parentNode); } return null; } /** * @returns {import('./modelitem.js').ModelItem} */ getModelItem() { const mi = this.getModel().getModelItem(this.nodeset); if (mi) { this.modelItem = mi; } const repeated = XPathUtil.getClosest('fx-repeatitem', this); let existed; if (repeated) { const { index } = repeated; if (Array.isArray(this.nodeset)) { existed = this.getModel().getModelItem(this.nodeset[index - 1]); } else { existed = this.getModel().getModelItem(this.nodeset); } } else { existed = this.nodeset ? this.getModel().getModelItem(this.nodeset) : null; } if (!existed) { const lazyCreatedModelItem = FxModel.lazyCreateModelItem( this.getModel(), this.ref, this.nodeset, this, ); this.modelItem = lazyCreatedModelItem; return lazyCreatedModelItem; } this.modelItem = existed; return existed; } /** * Returns the effective value for the element. * a: look for 'value' attribute and if present evaluate it and return the resulting value * b: look for textContent and return the value if present * c: return null * @returns {string} */ getValue() { if (this.hasAttribute('value')) { const valAttr = this.getAttribute('value'); try { const inscopeContext = getInScopeContext(this, valAttr); return evaluateXPathToString(valAttr, inscopeContext, this.getOwnerForm()); } catch (error) { console.error(error); Fore.dispatch(this, 'error', { message: error }); } } if (this.textContent) { return this.textContent; } return null; } /** * @returns {Node} */ getInScopeContext() { return getInScopeContext(this.getAttributeNode('ref') || this, this.ref); } /** * Set variables in scope here * @param {Map} inScopeVariables */ setInScopeVariables(inScopeVariables) { this.inScopeVariables = inScopeVariables; } }