Skip to content

Commit

Permalink
[Live] Fixing bug where the active input would maintain its value, bu…
Browse files Browse the repository at this point in the history
…t 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.
  • Loading branch information
weaverryan committed Feb 15, 2024
1 parent 30c8dbc commit cdb2c9b
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions src/LiveComponent/assets/src/morphdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/LiveComponent/assets/test/controller/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => `
<div ${initComponent(data)}>
<input data-model="name" class="anything">
</div>
`);

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) => `
<div ${initComponent(data)}>
Expand Down

0 comments on commit cdb2c9b

Please sign in to comment.