From cdb2c9bc11a1a4e3d859a43f1832f7312442e1af Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Thu, 15 Feb 2024 09:47:21 -0500 Subject: [PATCH] [Live] Fixing bug where the active input would maintain its value, but lose its cursor position This only affects "model" elements, as unmapped element changes are tracked and the "to" element's value updated in the beforeNodeMorphed() callback. Also, this setting matches Turbo 8. --- .../assets/dist/live_controller.js | 1 + src/LiveComponent/assets/src/morphdom.ts | 6 +++++ .../assets/test/controller/render.test.ts | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index 2968fd823db..08de05ddc64 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -1378,6 +1378,7 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, syncAttributes(newElement, oldElement); }); Idiomorph.morph(rootFromElement, rootToElement, { + ignoreActiveValue: true, callbacks: { beforeNodeMorphed: (fromEl, toEl) => { if (!(fromEl instanceof Element) || !(toEl instanceof Element)) { diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts index 7dba0668d9a..92a31080a14 100644 --- a/src/LiveComponent/assets/src/morphdom.ts +++ b/src/LiveComponent/assets/src/morphdom.ts @@ -53,6 +53,12 @@ export function executeMorphdom( }); Idiomorph.morph(rootFromElement, rootToElement, { + // We handle updating the value of fields that have been changed + // since the HTML was requested. However, the active element is + // a special case: replacing the value isn't enough. We need to + // prevent the value from being changed in the first place so the + // user's cursor position is maintained. + ignoreActiveValue: true, callbacks: { beforeNodeMorphed: (fromEl: Element, toEl: Element) => { // Idiomorph loop also over Text node diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts index df096692a97..70493eb8493 100644 --- a/src/LiveComponent/assets/test/controller/render.test.ts +++ b/src/LiveComponent/assets/test/controller/render.test.ts @@ -149,6 +149,28 @@ describe('LiveController rendering Tests', () => { expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts'); }); + it('conserves cursor position of active model element', async () => { + const test = await createTest({ name: '' }, (data) => ` +
+ +
+ `); + + test.expectsAjaxCall() + .expectUpdatedData({ name: 'Hello' }) + + const input = test.queryByDataModel('name') as HTMLInputElement; + userEvent.type(input, 'Hello'); + userEvent.keyboard('{ArrowLeft}{ArrowLeft}'); + + await test.component.render(); + + // the cursor position should be preserved + expect(input.selectionStart).toBe(3); + userEvent.type(input, '!'); + expect(input.value).toBe('Hel!lo'); + }); + it('does not render over elements with data-live-ignore', async () => { const test = await createTest({ firstName: 'Ryan' }, (data: any) => `