Skip to content

Commit f35b2ef

Browse files
JeanMechepkozlowski-opensource
authored andcommitted
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 a784995 commit f35b2ef

File tree

3 files changed

+52
-8
lines changed

3 files changed

+52
-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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,6 +2527,47 @@ describe('field directive', () => {
25272527
expect(customSubform.classList.contains('always')).toBe(false);
25282528
});
25292529
});
2530+
2531+
it('should create & bind input when a macro task is running', async () => {
2532+
const {promise, resolve} = promiseWithResolvers<void>();
2533+
2534+
@Component({
2535+
selector: 'app-form',
2536+
imports: [Field],
2537+
template: `
2538+
<form>
2539+
<select [field]="form">
2540+
<option value="us">United States</option>
2541+
<option value="ca">Canada</option>
2542+
</select>
2543+
</form>
2544+
`,
2545+
})
2546+
class FormComponent {
2547+
form = form(signal('us'));
2548+
}
2549+
2550+
@Component({
2551+
selector: 'app-root',
2552+
template: ``,
2553+
})
2554+
class App {
2555+
vcr = inject(ViewContainerRef);
2556+
constructor() {
2557+
promise.then(() => {
2558+
this.vcr.createComponent(FormComponent);
2559+
});
2560+
}
2561+
}
2562+
2563+
const fixture = act(() => TestBed.createComponent(App));
2564+
2565+
resolve();
2566+
await fixture.whenStable();
2567+
2568+
const select = fixture.debugElement.parent!.nativeElement.querySelector('select');
2569+
expect(select.value).toBe('us');
2570+
});
25302571
});
25312572

25322573
function setupRadioGroup() {

0 commit comments

Comments
 (0)