Skip to content

Commit 9e139d8

Browse files
committed
bug #1501 [Live] Fixing bug where the active input would maintain its value, but lose its cursor position (weaverryan)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Live] Fixing bug where the active input would maintain its value, but lose its cursor position | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Issues | Fix #1490 | License | MIT 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. This also updates Idiomorph to the absolute latest version, which includes a related bug fix. Cheers! Commits ------- d3d475a [Live] Fixing bug where the active input would maintain its value, but lose its cursor position
2 parents 42d28da + d3d475a commit 9e139d8

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)