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..633ae2b67bc 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..b7d9af761d2 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. @@ -162,12 +196,8 @@ 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 && - null !== getModelDirectiveFromElement(fromEl, false) - ) { - setValueOnElement(toEl, getElementValue(fromEl)); + if (fromEl === document.activeElement && fromEl !== document.body && modelDirective !== null) { + setValueOnElement(toEl, currentValue); } // handle any external changes to this element