Skip to content

Commit 0ab09e6

Browse files
committed
[Live] Reverting ignoreActiveValue: true in Idiomorph
This setting was recently reverted in Turbo. By setting this to true, it makes it impossible to update the active input's value with a new value from the server.
1 parent b436585 commit 0ab09e6

File tree

5 files changed

+52
-10
lines changed

5 files changed

+52
-10
lines changed

src/LiveComponent/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.16.0
4+
5+
- Reverted setting `ignoreActiveValue: true` in Idiomorph
6+
37
## 2.15.0
48

59
- [BC BREAK] The `data-live-id` attribute was changed to `id` #1484

src/LiveComponent/assets/dist/live_controller.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
13781378
syncAttributes(newElement, oldElement);
13791379
});
13801380
Idiomorph.morph(rootFromElement, rootToElement, {
1381-
ignoreActiveValue: true,
13821381
callbacks: {
13831382
beforeNodeMorphed: (fromEl, toEl) => {
13841383
if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {
@@ -1404,6 +1403,11 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
14041403
if (modifiedFieldElements.includes(fromEl)) {
14051404
setValueOnElement(toEl, getElementValue(fromEl));
14061405
}
1406+
if (fromEl === document.activeElement
1407+
&& fromEl !== document.body
1408+
&& null !== getModelDirectiveFromElement(fromEl, false)) {
1409+
setValueOnElement(toEl, getElementValue(fromEl));
1410+
}
14071411
const elementChanges = externalMutationTracker.getChangedElement(fromEl);
14081412
if (elementChanges) {
14091413
elementChanges.applyToElement(toEl);

src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default class {
7373
/**
7474
* Tracks field & models whose values are "unsynced".
7575
*
76-
* For a model, unsynced means that the value has been updated inside of
76+
* For a model, unsynced means that the value has been updated inside
7777
* a field (e.g. an input), but that this new value hasn't
7878
* yet been set onto the actual model data. It is "unsynced"
7979
* from the underlying model data.

src/LiveComponent/assets/src/morphdom.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cloneHTMLElement, setValueOnElement } from './dom_utils';
1+
import { cloneHTMLElement, getModelDirectiveFromElement, setValueOnElement } from './dom_utils';
22
// @ts-ignore
33
import { Idiomorph } from 'idiomorph/dist/idiomorph.esm.js';
44
import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison';
@@ -53,12 +53,6 @@ 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,
6256
callbacks: {
6357
beforeNodeMorphed: (fromEl: Element, toEl: Element) => {
6458
// Idiomorph loop also over Text node
@@ -106,6 +100,23 @@ export function executeMorphdom(
106100
setValueOnElement(toEl, getElementValue(fromEl));
107101
}
108102

103+
// Special handling for the active element of a model field.
104+
// Make the "to" element match the "from" element's value
105+
// to avoid any value change during the morphing. After morphing,
106+
// the SetValuesOntoModelFieldsPlugin handles setting the value
107+
// to whatever is in the data store.
108+
// Avoiding changing the value during morphing is important
109+
// to maintain the cursor position.
110+
// We skip this for non-model elements and allow this to either
111+
// maintain the value if changed (see code above) or for the
112+
// morphing process to update it to the value from the server.
113+
if (fromEl === document.activeElement
114+
&& fromEl !== document.body
115+
&& null !== getModelDirectiveFromElement(fromEl, false)
116+
) {
117+
setValueOnElement(toEl, getElementValue(fromEl));
118+
}
119+
109120
// handle any external changes to this element
110121
const elementChanges = externalMutationTracker.getChangedElement(fromEl);
111122
if (elementChanges) {

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe('LiveController rendering Tests', () => {
157157
`);
158158

159159
test.expectsAjaxCall()
160-
.expectUpdatedData({ name: 'Hello' })
160+
.expectUpdatedData({ name: 'Hello' });
161161

162162
const input = test.queryByDataModel('name') as HTMLInputElement;
163163
userEvent.type(input, 'Hello');
@@ -171,6 +171,29 @@ describe('LiveController rendering Tests', () => {
171171
expect(input.value).toBe('Hel!lo');
172172
});
173173

174+
it('uses the new value of an unmapped field that was NOT modified even if active', async () => {
175+
const test = await createTest({ title: 'greetings' }, (data: any) => `
176+
<div ${initComponent(data)}>
177+
<!-- An unmapped field -->
178+
<input value="${data.title}">
179+
180+
Title: "${data.title}"
181+
</div>
182+
`);
183+
184+
test.expectsAjaxCall()
185+
.serverWillChangeProps((data: any) => {
186+
// change the data on the server so the template renders differently
187+
data.title = 'Hello';
188+
});
189+
190+
const input = test.element.querySelector('input') as HTMLInputElement;
191+
// focus the input, but don't change it
192+
userEvent.type(input, '');
193+
await test.component.render();
194+
expect(input.value).toEqual('Hello');
195+
});
196+
174197
it('does not render over elements with data-live-ignore', async () => {
175198
const test = await createTest({ firstName: 'Ryan' }, (data: any) => `
176199
<div ${initComponent(data)}>

0 commit comments

Comments
 (0)