Skip to content

Commit c480004

Browse files
committed
refactor(compiler): Generate the controlCreate instruction after the native element has been created
This is necessary to exclude a race condition where the MutationObserver initialized by the instruction fired before the inputs are binded. fixes #65678
1 parent 8f3fdc3 commit c480004

File tree

3 files changed

+55
-8
lines changed

3 files changed

+55
-8
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ MyComponent.ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({
1010
i0.ɵɵelementStart(1, "div");
1111
i0.ɵɵtext(2, "Not a form control either.");
1212
i0.ɵɵelementEnd();
13-
i0.ɵɵelementStart(3, "input", 1);
13+
i0.ɵɵelement(3, "input", 1);
1414
i0.ɵɵcontrolCreate();
15-
i0.ɵɵelementEnd();
1615
}
1716
if (rf & 2) {
1817
i0.ɵɵadvance();

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,16 @@ function ingestElement(unit: ViewCompilationUnit, element: t.Element): void {
316316
const endOp = ir.createElementEndOp(id, element.endSourceSpan ?? element.startSourceSpan);
317317
unit.create.push(endOp);
318318

319+
// We want to ensure that the controlCreateOp is after the ops that create the element
320+
const fieldInput = element.inputs.find(
321+
(input) => input.name === 'field' && input.type === e.BindingType.Property,
322+
);
323+
if (fieldInput) {
324+
// If the input name is 'field', this could be a form control binding which requires a
325+
// `ControlCreateOp` to properly initialize.
326+
unit.create.push(ir.createControlCreateOp(fieldInput.sourceSpan));
327+
}
328+
319329
// If there is an i18n message associated with this element, insert i18n start and end ops.
320330
if (i18nBlockId !== null) {
321331
ir.OpList.insertBefore<ir.CreateOp>(
@@ -1347,12 +1357,6 @@ function ingestElementBindings(
13471357
input.sourceSpan,
13481358
),
13491359
);
1350-
1351-
// If the input name is 'field', this could be a form control binding which requires a
1352-
// `ControlCreateOp` to properly initialize.
1353-
if (input.type === e.BindingType.Property && input.name === 'field') {
1354-
unit.create.push(ir.createControlCreateOp(input.sourceSpan));
1355-
}
13561360
}
13571361

13581362
unit.create.push(

packages/forms/signals/test/web/field_directive.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {
10+
ApplicationRef,
1011
Component,
1112
computed,
1213
Directive,
@@ -44,6 +45,7 @@ import {
4445
type ValidationError,
4546
type WithOptionalField,
4647
} from '../../public_api';
48+
import {exec} from 'child_process';
4749

4850
@Component({
4951
selector: 'string-control',
@@ -2527,6 +2529,48 @@ describe('field directive', () => {
25272529
expect(customSubform.classList.contains('always')).toBe(false);
25282530
});
25292531
});
2532+
2533+
it('should create & bind input when a macro task is running', async () => {
2534+
const {promise, resolve} = promiseWithResolvers<void>();
2535+
2536+
@Component({
2537+
selector: 'app-form',
2538+
imports: [Field],
2539+
template: `
2540+
<form>
2541+
<select [field]="form">
2542+
<option value="us">United States</option>
2543+
<option value="ca">Canada</option>
2544+
</select>
2545+
</form>
2546+
`,
2547+
})
2548+
class FormComponent {
2549+
form = form(signal('us'));
2550+
}
2551+
2552+
@Component({
2553+
selector: 'app-root',
2554+
template: ``,
2555+
})
2556+
class App {
2557+
vcr = inject(ViewContainerRef);
2558+
constructor() {
2559+
promise.then(() => {
2560+
this.vcr.createComponent(FormComponent);
2561+
});
2562+
}
2563+
}
2564+
2565+
Error.stackTraceLimit = 1000;
2566+
const fixture = act(() => TestBed.createComponent(App));
2567+
2568+
resolve();
2569+
await fixture.whenStable();
2570+
2571+
const select = fixture.debugElement.parent!.nativeElement.querySelector('select');
2572+
expect(select.value).toBe('us');
2573+
});
25302574
});
25312575

25322576
function setupRadioGroup() {

0 commit comments

Comments
 (0)