Skip to content

Commit d3d475a

Browse files
committed
[Live] Fixing bug where the active input would maintain its value, but lose its cursor position
1 parent 30c8dbc commit d3d475a

File tree

5 files changed

+42
-15
lines changed

5 files changed

+42
-15
lines changed

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ var Idiomorph = (function () {
617617
* @returns {boolean}
618618
*/
619619
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
620-
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
620+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
621621
}
622622

623623
/**
@@ -1378,6 +1378,7 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
13781378
syncAttributes(newElement, oldElement);
13791379
});
13801380
Idiomorph.morph(rootFromElement, rootToElement, {
1381+
ignoreActiveValue: true,
13811382
callbacks: {
13821383
beforeNodeMorphed: (fromEl, toEl) => {
13831384
if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {

src/LiveComponent/assets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
}
2828
},
2929
"dependencies": {
30-
"idiomorph": "^0.3.0"
30+
"idiomorph": "https://github.com/bigskysoftware/idiomorph.git"
3131
},
3232
"peerDependencies": {
3333
"@hotwired/stimulus": "^3.0.0"

src/LiveComponent/assets/src/morphdom.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export function executeMorphdom(
5353
});
5454

5555
Idiomorph.morph(rootFromElement, rootToElement, {
56+
// We handle updating the value of fields that have been changed
57+
// since the HTML was requested. However, the active element is
58+
// a special case: replacing the value isn't enough. We need to
59+
// prevent the value from being changed in the first place so the
60+
// user's cursor position is maintained.
61+
ignoreActiveValue: true,
5662
callbacks: {
5763
beforeNodeMorphed: (fromEl: Element, toEl: Element) => {
5864
// Idiomorph loop also over Text node

src/LiveComponent/assets/test/controller/model.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -810,9 +810,9 @@ describe('LiveController data-model Tests', () => {
810810
<div ${initComponent(data)}>
811811
<form data-model>
812812
<textarea name="comment" data-testid="comment">${data.comment}</textarea>
813-
</form>
813+
</form>
814814
815-
<button data-action="live#$render">Reload</button>
815+
<input data-testid="other-input">
816816
</div>
817817
`);
818818

@@ -823,17 +823,18 @@ describe('LiveController data-model Tests', () => {
823823
// delay slightly so we can type in the textarea
824824
.delayResponse(10);
825825

826-
getByText(test.element, 'Reload').click();
826+
const renderPromise = test.component.render();
827827
// mimic changing the field, but without (yet) triggering the change event
828828
const commentField = getByTestId(test.element, 'comment');
829-
if (!(commentField instanceof HTMLTextAreaElement)) {
830-
throw new Error('wrong type');
831-
}
829+
const inputField = getByTestId(test.element, 'other-input');
832830
userEvent.type(commentField, ' ftw!');
831+
// To make the test more robust, make the textarea NOT the active element.
832+
// The active element's value is ignored during morphing. Here, we want
833+
// to test that even if the morph system *does* want to update the value
834+
// of the textarea, the value will be kept.
835+
userEvent.type(inputField, 'making this element active');
833836

834-
// wait for loading start and end
835-
await waitFor(() => expect(test.element).toHaveAttribute('busy'));
836-
await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
837+
await renderPromise;
837838

838839
expect(commentField).toHaveValue('Live components ftw!');
839840

@@ -845,10 +846,7 @@ describe('LiveController data-model Tests', () => {
845846
data.comment = 'server changed comment';
846847
});
847848

848-
getByText(test.element, 'Reload').click();
849-
// wait for loading start and end
850-
await waitFor(() => expect(test.element).toHaveAttribute('busy'));
851-
await waitFor(() => expect(test.element).not.toHaveAttribute('busy'));
849+
await test.component.render();
852850

853851
expect(commentField).toHaveValue('server changed comment');
854852
});

src/LiveComponent/assets/test/controller/render.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ describe('LiveController rendering Tests', () => {
149149
expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts');
150150
});
151151

152+
it('conserves cursor position of active model element', async () => {
153+
const test = await createTest({ name: '' }, (data) => `
154+
<div ${initComponent(data)}>
155+
<input data-model="name" class="anything">
156+
</div>
157+
`);
158+
159+
test.expectsAjaxCall()
160+
.expectUpdatedData({ name: 'Hello' })
161+
162+
const input = test.queryByDataModel('name') as HTMLInputElement;
163+
userEvent.type(input, 'Hello');
164+
userEvent.keyboard('{ArrowLeft}{ArrowLeft}');
165+
166+
await test.component.render();
167+
168+
// the cursor position should be preserved
169+
expect(input.selectionStart).toBe(3);
170+
userEvent.type(input, '!');
171+
expect(input.value).toBe('Hel!lo');
172+
});
173+
152174
it('does not render over elements with data-live-ignore', async () => {
153175
const test = await createTest({ firstName: 'Ryan' }, (data: any) => `
154176
<div ${initComponent(data)}>

0 commit comments

Comments
 (0)