From c8affca8888975f7bde9f82d3d060aab395713c3 Mon Sep 17 00:00:00 2001 From: Matijn Woudt Date: Fri, 23 Jan 2026 12:00:12 +0100 Subject: [PATCH 1/2] Preserve locally modified inputs during requests on morph --- .../assets/dist/live_controller.d.ts | 1 + .../assets/dist/live_controller.js | 39 ++++++++++++++-- .../assets/src/Component/ValueStore.ts | 4 ++ .../assets/src/Component/index.ts | 3 +- src/LiveComponent/assets/src/morphdom.ts | 46 ++++++++++++++++--- 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/LiveComponent/assets/dist/live_controller.d.ts b/src/LiveComponent/assets/dist/live_controller.d.ts index c032c778f57..c6424afff84 100644 --- a/src/LiveComponent/assets/dist/live_controller.d.ts +++ b/src/LiveComponent/assets/dist/live_controller.d.ts @@ -69,6 +69,7 @@ declare class export_default{ set(name: string, value: any): boolean; getOriginalProps(): any; getDirtyProps(): any; + getPendingProps(): any; getUpdatedPropsFromParent(): any; flushDirtyPropsToPending(): void; reinitializeAllProps(props: any): void; diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index a4d4ba7b20c..ce30cfed4bc 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -1119,7 +1119,22 @@ var syncAttributes = (fromEl, toEl) => { toEl.setAttribute(attr.name, attr.value); } }; -function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) { +var syncAttributesExceptValue = (fromEl, toEl) => { + const valueAttributes = ["value", "checked", "selected"]; + for (let i = fromEl.attributes.length - 1; i >= 0; i--) { + const attr = fromEl.attributes[i]; + if (!valueAttributes.includes(attr.name) && !toEl.hasAttribute(attr.name)) { + fromEl.removeAttribute(attr.name); + } + } + for (let i = 0; i < toEl.attributes.length; i++) { + const attr = toEl.attributes[i]; + if (!valueAttributes.includes(attr.name) && fromEl.getAttribute(attr.name) !== attr.value) { + fromEl.setAttribute(attr.name, attr.value); + } + } +}; +function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker, pendingProps = {}) { const originalElementIdsToSwapAfter = []; const originalElementsToPreserve = /* @__PURE__ */ new Map(); const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => { @@ -1186,11 +1201,21 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, fromEl.insertAdjacentElement("afterend", toEl); return false; } + const modelDirective = getModelDirectiveFromElement(fromEl, false); + const currentValue = getElementValue(fromEl); if (modifiedFieldElements.includes(fromEl)) { - setValueOnElement(toEl, getElementValue(fromEl)); + if (modelDirective) { + const sentValue = pendingProps ? pendingProps[modelDirective.action] : void 0; + if (sentValue !== currentValue) { + syncAttributesExceptValue(fromEl, toEl); + return false; + } + } else { + setValueOnElement(toEl, currentValue); + } } - if (fromEl === document.activeElement && fromEl !== document.body && null !== getModelDirectiveFromElement(fromEl, false)) { - setValueOnElement(toEl, getElementValue(fromEl)); + if (fromEl === document.activeElement && fromEl !== document.body && modelDirective !== null) { + setValueOnElement(toEl, currentValue); } const elementChanges = externalMutationTracker.getChangedElement(fromEl); if (elementChanges) { @@ -1768,6 +1793,9 @@ var ValueStore_default = class { getDirtyProps() { return { ...this.dirtyProps }; } + getPendingProps() { + return { ...this.pendingProps }; + } getUpdatedPropsFromParent() { return { ...this.updatedPropsFromParent }; } @@ -2079,7 +2107,8 @@ var Component = class { newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), - this.externalMutationTracker + this.externalMutationTracker, + this.valueStore.getPendingProps() ); this.externalMutationTracker.start(); const newProps = this.elementDriver.getComponentProps(); diff --git a/src/LiveComponent/assets/src/Component/ValueStore.ts b/src/LiveComponent/assets/src/Component/ValueStore.ts index a92ded2edb5..735883b505e 100644 --- a/src/LiveComponent/assets/src/Component/ValueStore.ts +++ b/src/LiveComponent/assets/src/Component/ValueStore.ts @@ -86,6 +86,10 @@ export default class { return { ...this.dirtyProps }; } + getPendingProps(): any { + return { ...this.pendingProps }; + } + getUpdatedPropsFromParent(): any { return { ...this.updatedPropsFromParent }; } diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts index f59ac23eea2..e47d8b00025 100644 --- a/src/LiveComponent/assets/src/Component/index.ts +++ b/src/LiveComponent/assets/src/Component/index.ts @@ -408,7 +408,8 @@ export default class Component { newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element: HTMLElement) => getValueFromElement(element, this.valueStore), - this.externalMutationTracker + this.externalMutationTracker, + this.valueStore.getPendingProps(), ); this.externalMutationTracker.start(); diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts index 218fb98c4d9..91530e06b6a 100644 --- a/src/LiveComponent/assets/src/morphdom.ts +++ b/src/LiveComponent/assets/src/morphdom.ts @@ -11,12 +11,31 @@ const syncAttributes = (fromEl: Element, toEl: Element): void => { } }; +const syncAttributesExceptValue = (fromEl: Element, toEl: Element): void => { + const valueAttributes = ['value', 'checked', 'selected']; + + for (let i = fromEl.attributes.length - 1; i >= 0; i--) { + const attr = fromEl.attributes[i]; + if (!valueAttributes.includes(attr.name) && !toEl.hasAttribute(attr.name)) { + fromEl.removeAttribute(attr.name); + } + } + + for (let i = 0; i < toEl.attributes.length; i++) { + const attr = toEl.attributes[i]; + if (!valueAttributes.includes(attr.name) && fromEl.getAttribute(attr.name) !== attr.value) { + fromEl.setAttribute(attr.name, attr.value); + } + } +}; + export function executeMorphdom( rootFromElement: HTMLElement, rootToElement: HTMLElement, modifiedFieldElements: Array, getElementValue: (element: HTMLElement) => any, - externalMutationTracker: ExternalMutationTracker + externalMutationTracker: ExternalMutationTracker, + pendingProps: Record = {} ) { /* * Handle "data-live-preserve" elements. @@ -146,10 +165,25 @@ export function executeMorphdom( return false; } - // if this field's value has been modified since this HTML was - // requested, set the toEl's value to match the fromEl + const modelDirective = getModelDirectiveFromElement(fromEl, false); + const currentValue = getElementValue(fromEl); + + // If this field's value has been modified since this HTML was + // requested, preserve the value if it differs from what we sent if (modifiedFieldElements.includes(fromEl)) { - setValueOnElement(toEl, getElementValue(fromEl)); + if (modelDirective) { + // For mapped fields, only preserve if DOM value differs from what we sent + const sentValue = pendingProps ? pendingProps[modelDirective.action] : undefined; + if (sentValue !== currentValue) { + // Sync all attributes except value-related ones, then skip + // morphdom to preserve the input value + syncAttributesExceptValue(fromEl, toEl); + return false; + } + } else { + // Unmapped fields: always preserve (original behavior) + setValueOnElement(toEl, currentValue); + } } // Special handling for the active element of a model field. @@ -165,9 +199,9 @@ export function executeMorphdom( if ( fromEl === document.activeElement && fromEl !== document.body && - null !== getModelDirectiveFromElement(fromEl, false) + modelDirective !== null ) { - setValueOnElement(toEl, getElementValue(fromEl)); + setValueOnElement(toEl, currentValue); } // handle any external changes to this element From 0a36d7f30828c41745d413228ec59688f20f0003 Mon Sep 17 00:00:00 2001 From: Matijn Woudt Date: Fri, 23 Jan 2026 12:00:12 +0100 Subject: [PATCH 2/2] Preserve locally modified inputs during requests on morph --- src/LiveComponent/assets/src/Component/index.ts | 2 +- src/LiveComponent/assets/src/morphdom.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts index e47d8b00025..633ae2b67bc 100644 --- a/src/LiveComponent/assets/src/Component/index.ts +++ b/src/LiveComponent/assets/src/Component/index.ts @@ -409,7 +409,7 @@ export default class Component { this.unsyncedInputsTracker.getUnsyncedInputs(), (element: HTMLElement) => getValueFromElement(element, this.valueStore), this.externalMutationTracker, - this.valueStore.getPendingProps(), + this.valueStore.getPendingProps() ); this.externalMutationTracker.start(); diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts index 91530e06b6a..b7d9af761d2 100644 --- a/src/LiveComponent/assets/src/morphdom.ts +++ b/src/LiveComponent/assets/src/morphdom.ts @@ -196,11 +196,7 @@ export function executeMorphdom( // We skip this for non-model elements and allow this to either // maintain the value if changed (see code above) or for the // morphing process to update it to the value from the server. - if ( - fromEl === document.activeElement && - fromEl !== document.body && - modelDirective !== null - ) { + if (fromEl === document.activeElement && fromEl !== document.body && modelDirective !== null) { setValueOnElement(toEl, currentValue); }