Skip to content

Commit

Permalink
Detect whether a component should be refreshed by elementname
Browse files Browse the repository at this point in the history
Use the local name of inserted and deleted nodes to see whether a repeat should update or not.
  • Loading branch information
DrRataplan committed Dec 17, 2024
1 parent d555d2e commit 8faa3cb
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 75 deletions.
32 changes: 32 additions & 0 deletions src/DependentXPathQueries.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,38 @@ export default class DependentXPathQueries {
return !!this._parentDependencies?.isInvalidatedByIndexFunction();
}

/**
* Detects whether there are XPaths that may be affected by child list changes to nodes with the given local names
*
* It does this pessimistically, assuming that any descendant axis can be affected by these changes
*
* @param {string[]} affectedLocalNames
*/
isInvalidatedByChildlistChanges(affectedLocalNames) {
// Scan for any XPath part that may jump over a node without checking what the type is. Can also
// be done dynamically, by listening for the `bucket` parameter of all the dom accessors
const flaggedConstructs = [
'//',
'ancestor',
'descendant',
'element(',
'*',
'..',
'following',
'preceding',
];
for (const xpath of this._xpaths) {
if (flaggedConstructs.some(c => xpath.includes(c))) {
return true;
}
if (affectedLocalNames.some(n => xpath.includes(n))) {
return true;
}
}
// We can also depend on these elements if it was used in our ancestry
return !!this._parentDependencies?.isInvalidatedByChildlistChanges(affectedLocalNames);
}

/**
* Add an XPath to the dependencies
*
Expand Down
33 changes: 19 additions & 14 deletions src/ForeElementMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ export default class ForeElementMixin extends HTMLElement {
*/
this.inScopeVariables = new Map();

this._dependencies = new DependentXPathQueries();
this._dependencies.setParentDependencies(this.parent?.closest('[ref]')?._dependencies);
this.dependencies = new DependentXPathQueries();
}

connectedCallback() {
if (this.parentElement) {
this.dependencies.setParentDependencies(this.parentElement?.closest('[ref]')?.dependencies);
}
}

/**
Expand Down Expand Up @@ -107,7 +112,7 @@ export default class ForeElementMixin extends HTMLElement {
* evaluation of fx-bind and UiElements differ in details so that each class needs it's own implementation.
*/
evalInContext() {
this._dependencies.resetDependencies();
this.dependencies.resetDependencies();
// const inscopeContext = this.getInScopeContext();
const model = this.getModel();
if (!model) {
Expand All @@ -119,7 +124,7 @@ export default class ForeElementMixin extends HTMLElement {
}
if (this.hasAttribute('ref')) {
inscopeContext = getInScopeContext(this.getAttributeNode('ref') || this, this.ref);
this._dependencies.addXPath(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
Expand All @@ -132,16 +137,16 @@ export default class ForeElementMixin extends HTMLElement {
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);
}
});
*/
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 {
Expand Down
47 changes: 23 additions & 24 deletions src/actions/abstract-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class AbstractAction extends ForeElementMixin {
disconnectedCallback() {}

connectedCallback() {
super.connectedCallback();
this.setAttribute('inert', 'true');
this.style.display = 'none';
this.propagate = this.hasAttribute('propagate') ? this.getAttribute('propagate') : 'continue';
Expand Down Expand Up @@ -212,30 +213,28 @@ export class AbstractAction extends ForeElementMixin {
* @param e
*/
async execute(e) {
if(!this.getModel().modelConstructed) return;
if (!this.getModel().modelConstructed) return;
// console.log(this, this.event);
if(this.event){
if(this.event === 'submit-done'){
if (this.event) {
if (this.event === 'submit-done') {
console.info(
`%csubmit-done ${this.event} #${this?.parentNode?.id}`,
'background:lime; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
`%csubmit-done ${this.event} #${this?.parentNode?.id}`,
'background:lime; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
);
}else{
} else {
console.info(
`%cexecuting ${this.constructor.name} ${this.event}`,
'background:lime; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
`%cexecuting ${this.constructor.name} ${this.event}`,
'background:lime; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
);
}

}else{
} else {
console.info(
`%cexecuting ${this.constructor.name}`,
'background:limegreen; color:black; margin-left:1rem; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
this
`%cexecuting ${this.constructor.name}`,
'background:limegreen; color:black; margin-left:1rem; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
this,
);
}


if (e && e.target.nodeType !== Node.DOCUMENT_NODE && e.target !== window) {
/*
### ignore event if there's a parent fore and the current element is NOT part of it. This avoids
Expand All @@ -260,7 +259,7 @@ export class AbstractAction extends ForeElementMixin {
let resolveThisEvent = () => {};
if (e && e.listenerPromises) {
e.listenerPromises.push(
new Promise((resolve) => {
new Promise(resolve => {
resolveThisEvent = resolve;
}),
);
Expand All @@ -269,9 +268,9 @@ export class AbstractAction extends ForeElementMixin {
// Outermost handling
if (FxFore.outermostHandler === null) {
console.log(
`%coutermost Action on ${this.getOwnerForm().id}`,
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
this,
`%coutermost Action on ${this.getOwnerForm().id}`,
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
this,
);

FxFore.outermostHandler = this;
Expand Down Expand Up @@ -406,9 +405,9 @@ export class AbstractAction extends ForeElementMixin {
this.actionPerformed();
if (FxFore.outermostHandler === this) {
console.log(
`%cfinalizing outermost Action on ${this.getOwnerForm().id}`,
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
this,
`%cfinalizing outermost Action on ${this.getOwnerForm().id}`,
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
this,
);

FxFore.outermostHandler = null;
Expand Down Expand Up @@ -467,8 +466,8 @@ export class AbstractAction extends ForeElementMixin {
return;
}
if (
FxFore.outermostHandler
&& !XPathUtil.contains(FxFore.outermostHandler.ownerDocument, FxFore.outermostHandler)
FxFore.outermostHandler &&
!XPathUtil.contains(FxFore.outermostHandler.ownerDocument, FxFore.outermostHandler)
) {
// The old outermostHandler fell out of the document. An error has happened.
// Just remove the old one and act like we are starting anew.
Expand All @@ -480,7 +479,7 @@ export class AbstractAction extends ForeElementMixin {
// console.log('running update cycle for outermostHandler', this);
model.recalculate();
model.revalidate();
model.parentNode.refresh(true);
this.getOwnerForm().refresh(true);
this.dispatchActionPerformed();
} else if (this.needsUpdate) {
// console.log('Update delayed!');
Expand Down
2 changes: 2 additions & 0 deletions src/actions/fx-append.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ class FxAppend extends AbstractAction {
const data = this._generateInstance(templ.content, rootNode);
// console.log('_dataFromTemplate DATA', data);
inscope.appendChild(data);
parentForm.signalChangeToElement(inscope.localName);
parentForm.signalChangeToElement(data.localName);
// console.log('appended new item ', data);
// return data;
}
Expand Down
16 changes: 14 additions & 2 deletions src/actions/fx-delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,31 @@ class FxDelete extends AbstractAction {
}),
);

const fore = this.getOwnerForm();

let parent;
if (Array.isArray(nodesToDelete)) {
if (nodesToDelete.length === 0) return;
parent = nodesToDelete[0].parentNode;
fore.signalChangeToElement(parent.localName);
nodesToDelete.forEach(item => {
this._deleteNode(parent, item);
fore.signalChangeToElement(item.localName);
});
} else {
parent = nodesToDelete.parentNode;
fore.signalChangeToElement(parent.localName);

this._deleteNode(parent, nodesToDelete);
fore.signalChangeToElement(nodesToDelete.localName);
}
const foreId = this.getOwnerForm().id;
await Fore.dispatch(instance, 'deleted', { ref: path, deletedNodes: nodesToDelete , instanceId:instanceId, foreId:foreId});

await Fore.dispatch(instance, 'deleted', {
ref: path,
deletedNodes: nodesToDelete,
instanceId,
foreId: fore.id,
});
this.needsUpdate = true;
}

Expand Down
22 changes: 15 additions & 7 deletions src/actions/fx-insert.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,11 @@ export class FxInsert extends AbstractAction {
let targetSequence = [];
const inscopeContext = getInScopeContext(this);

const fore = this.getOwnerForm();

// ### 'context' attribute takes precedence over 'ref'
if (this.hasAttribute('context')) {
[context] = evaluateXPathToNodes(
this.getAttribute('context'),
inscopeContext,
this.getOwnerForm(),
);
[context] = evaluateXPathToNodes(this.getAttribute('context'), inscopeContext, fore);
inscope = inscopeContext;
}

Expand All @@ -165,11 +163,15 @@ export class FxInsert extends AbstractAction {
if (context) {
insertLocationNode = context;
context.appendChild(originSequenceClone);
fore.signalChangeToElement(insertLocationNode.localName);
fore.signalChangeToElement(originSequenceClone.localName);
index = 1;
} else {
// No context. We can insert into the `inscope`.
insertLocationNode = inscope;
inscope.appendChild(originSequenceClone);
fore.signalChangeToElement(inscope.localName);
fore.signalChangeToElement(originSequenceClone.localName);
index = 1;
}
} else {
Expand Down Expand Up @@ -205,6 +207,8 @@ export class FxInsert extends AbstractAction {
if (this.position && this.position === 'before') {
// this.at -= 1;
insertLocationNode.parentNode.insertBefore(originSequenceClone, insertLocationNode);
fore.signalChangeToElement(insertLocationNode.parentNode);
fore.signalChangeToElement(originSequenceClone.localName);
}

if (this.position && this.position === 'after') {
Expand All @@ -217,8 +221,12 @@ export class FxInsert extends AbstractAction {
} else if (this.hasAttribute('context')) {
index = 1;
insertLocationNode.prepend(originSequenceClone);
fore.signalChangeToElement(insertLocationNode);
fore.signalChangeToElement(originSequenceClone.localName);
} else {
insertLocationNode.insertAdjacentElement('afterend', originSequenceClone);
fore.signalChangeToElement(insertLocationNode);
fore.signalChangeToElement(originSequenceClone.localName);
}
}
}
Expand Down Expand Up @@ -251,8 +259,8 @@ export class FxInsert extends AbstractAction {
'inserted-nodes': originSequenceClone,
'insert-location-node': insertLocationNode,
position: this.position,
instanceId:instanceId,
foreId:this.getOwnerForm().id
instanceId,
foreId: fore.id,
});

// todo: this actually should dispatch to respective instance
Expand Down
63 changes: 40 additions & 23 deletions src/fore.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export class Fore {
* recursively refreshes all UI Elements.
*
* @param {HTMLElement} startElement
* @param {(boolean|{reason:'index-function'})} force Whether to do a forced refresh. Forced
* @param {(boolean|{reason:'index-function', elementLocalnamesWithChanges: string[]})} force Whether to do a forced refresh. Forced
* refreshes are very bad for performance, try to limit them. If the forced refresh is because index functions may change, it is better to pass the reason
* @returns {Promise<void>}
*/
Expand All @@ -300,30 +300,47 @@ export class Fore {
const { children } = startElement;
if (children) {
for (const element of Array.from(children)) {
if (element.nodeName.toUpperCase() === 'FX-FORE') {
break;
if (element.nodeName.toUpperCase() === 'FX-FORE') {
break;
}
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
/**
* @type {import('./ForeElementMixin.js').default}
*/
const bound = element;
if (!force) {
continue;
}
if (force === true) {
// Unconditional force refresh
bound.refresh(force);
continue;
}
if (typeof force !== 'object') {
continue;
}
if (
force.reason === 'index-function' &&
bound.dependencies.isInvalidatedByIndexFunction()
) {
bound.refresh(force);
continue;
}
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
/**
* @type {import('./ForeElementMixin.js').default}
*/
if (
force &&
typeof force === 'object' &&
force.reason === 'index-function' &&
element._dependencies.isInvalidatedByIndexFunction()
) {
element.refresh(force);
continue;
} else if (force === true) {
element.refresh(force);
}
// console.log('refreshing', element, element?.ref);
// console.log('refreshing ',element);
} else if (!(element.inert === true) ) {
// testing for inert catches model and action elements and should just leave updateable html elements
Fore.refreshChildren(element, force);

if (
bound.dependencies.isInvalidatedByChildlistChanges(force.elementLocalnamesWithChanges)
) {
bound.refresh(force);
continue;
}

// console.log('refreshing', element, element?.ref);
// console.log('refreshing ',element);
}
if (!(element.inert === true)) {
// testing for inert catches model and action elements and should just leave updateable html elements
Fore.refreshChildren(element, force);
}
}
}
resolve('done');
Expand Down
Loading

0 comments on commit 8faa3cb

Please sign in to comment.