Skip to content

Commit c4ce3f3

Browse files
alxhubmattrbeck
authored andcommitted
feat(forms): template & reactive support for FVC
Implement support for `FormUiComponent`s in both Reactive and Template-driven forms. This allows components that use the new signal-based form control architecture to be used seamlessly within existing Angular form paradigms. Key changes: - Integrated `ɵngControlCreate` and `ɵngControlUpdate` lifecycle hooks into `NgModel`, `FormControlDirective`, and `FormControlName`. - Implemented branching logic to choose between the traditional `ControlValueAccessor` (CVA) path and the new FVC path based on the host element's capabilities. - Added comprehensive unit tests for FVC integration in both Reactive (`reactive_fvc.spec.ts`) and Template-driven (`template_fvc.spec.ts`) forms, covering: - Value synchronization (model -> view and view -> model). - Status synchronization (touched, dirty, valid, invalid, pending, required). - Error propagation and `parseErrors` support. - Fallback behavior to native DOM properties (disabled, required) when FVC inputs are missing. - Graceful fallback to CVA when no FVC pattern is detected. - Refined `NgModel` to correctly handle `required` validation via its existing `RequiredValidator` directive while supporting FVC for other properties.
1 parent b2f0857 commit c4ce3f3

File tree

14 files changed

+1405
-12
lines changed

14 files changed

+1405
-12
lines changed

goldens/public-api/forms/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ export class NgModel extends NgControl implements OnChanges, OnDestroy {
778778
updateOn?: FormHooks;
779779
};
780780
get path(): string[];
781+
protected get shouldBindRequired(): boolean;
781782
update: EventEmitter<any>;
782783
viewModel: any;
783784
viewToModelUpdate(newValue: any): void;

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/nested_two_way_template.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ function TestCmp_ng_template_1_Template(rf, ctx) {
99
return $r3$.ɵɵresetView($event);
1010
});
1111
$r3$.ɵɵelementEnd();
12+
$r3$.ɵɵcontrolCreate();
1213
} if (rf & 2) {
1314
const $ctx_r0$ = $r3$.ɵɵnextContext();
1415
$r3$.ɵɵtwoWayProperty("ngModel", $ctx_r0$.name);
16+
$r3$.ɵɵcontrol();
1517
}
1618
}

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/simple_two_way_template.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ function TestCmp_Template(rf, ctx) {
77
return $event;
88
});
99
$r3$.ɵɵelementEnd();
10+
$r3$.ɵɵcontrolCreate();
1011
} if (rf & 2) {
1112
$r3$.ɵɵadvance();
1213
$r3$.ɵɵtwoWayProperty("ngModel", ctx.name);
14+
$r3$.ɵɵcontrol();
1315
}
1416
}

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/two_way_binding_to_signal_loop_variable_template.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ function TestCmp_For_1_Template(rf, ctx) {
88
return $r3$.ɵɵresetView($event);
99
});
1010
$r3$.ɵɵelementEnd();
11+
$r3$.ɵɵcontrolCreate();
1112
}
1213
if (rf & 2) {
1314
const $name_r2$ = ctx.$implicit;
1415
$r3$.ɵɵtwoWayProperty("ngModel", $name_r2$);
16+
$r3$.ɵɵcontrol();
1517
}
1618
}
1719

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/two_way_to_any_template.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ function TestCmp_Template(rf, ctx) {
66
return $event;
77
});
88
$r3$.ɵɵelementEnd();
9+
$r3$.ɵɵcontrolCreate();
910
}
1011
if (rf & 2) {
1112
$r3$.ɵɵtwoWayProperty("ngModel", ctx.value);
13+
$r3$.ɵɵcontrol();
1214
}
1315
}

packages/compiler/src/template/pipeline/src/phases/control_directives.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import type {ComponentCompilationJob, ViewCompilationUnit} from '../compilation'
1111

1212
const ELIGIBLE_CONTROL_PROPERTIES = new Map<string, Set<ir.OpKind>>([
1313
['formField', new Set([ir.OpKind.Property])],
14+
['formControl', new Set([ir.OpKind.Property])],
15+
['formControlName', new Set([ir.OpKind.Property, ir.OpKind.Attribute])],
16+
['ngModel', new Set([ir.OpKind.Attribute, ir.OpKind.Property, ir.OpKind.TwoWayProperty])],
1417
]);
1518

1619
export function specializeControlProperties(job: ComponentCompilationJob): void {

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"ComponentRef2",
6666
"ConsumerObserver",
6767
"ControlContainer",
68+
"ControlDirectiveHostImpl",
6869
"ControlEvent",
6970
"DEBUG_TASK_TRACKER",
7071
"DECLARATION_COMPONENT_VIEW",
@@ -287,12 +288,15 @@
287288
"\\u0275FORM_FIELD_PARSE_ERRORS",
288289
"\\u0275InternalFormsSharedModule",
289290
"\\u0275NgNoValidate",
291+
"\\u0275\\u0275ControlFeature",
290292
"\\u0275\\u0275InheritDefinitionFeature",
291293
"\\u0275\\u0275NgOnChangesFeature",
292294
"\\u0275\\u0275ProvidersFeature",
293295
"\\u0275\\u0275advance",
294296
"\\u0275\\u0275attribute",
295297
"\\u0275\\u0275classProp",
298+
"\\u0275\\u0275control",
299+
"\\u0275\\u0275controlCreate",
296300
"\\u0275\\u0275defineComponent",
297301
"\\u0275\\u0275defineDirective",
298302
"\\u0275\\u0275defineInjectable",
@@ -441,8 +445,10 @@
441445
"consumerMarkDirty",
442446
"consumerPollProducersForChange",
443447
"context",
448+
"controlCreateInternal",
444449
"controlNameBinding",
445450
"controlPath",
451+
"controlUpdateInternal",
446452
"convertToBitFlags",
447453
"convertToInjectOptions",
448454
"couldBeInjectableType",
@@ -659,9 +665,12 @@
659665
"hasClassInput",
660666
"hasDeps",
661667
"hasInSkipHydrationBlockFlag",
668+
"hasInput",
662669
"hasInvalidParent",
663670
"hasLift",
671+
"hasModelInput",
664672
"hasOnDestroy",
673+
"hasOutput",
665674
"hasParentInjector",
666675
"hasStyleInput",
667676
"hasStylingInputShadow",
@@ -683,6 +692,8 @@
683692
"initDomAdapter",
684693
"initFeatures",
685694
"initTNodeFlags",
695+
"initializeControlFirstCreatePass",
696+
"initializeCustomControlStatus",
686697
"initializeDirectives",
687698
"initializeElement",
688699
"initializeInputAndOutputAliases",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"ComponentRef2",
6565
"ConsumerObserver",
6666
"ControlContainer",
67+
"ControlDirectiveHostImpl",
6768
"ControlEvent",
6869
"DEBUG_TASK_TRACKER",
6970
"DECLARATION_COMPONENT_VIEW",
@@ -281,12 +282,15 @@
281282
"\\u0275FORM_FIELD_PARSE_ERRORS",
282283
"\\u0275InternalFormsSharedModule",
283284
"\\u0275NgNoValidate",
285+
"\\u0275\\u0275ControlFeature",
284286
"\\u0275\\u0275InheritDefinitionFeature",
285287
"\\u0275\\u0275NgOnChangesFeature",
286288
"\\u0275\\u0275ProvidersFeature",
287289
"\\u0275\\u0275advance",
288290
"\\u0275\\u0275attribute",
289291
"\\u0275\\u0275classProp",
292+
"\\u0275\\u0275control",
293+
"\\u0275\\u0275controlCreate",
290294
"\\u0275\\u0275defineComponent",
291295
"\\u0275\\u0275defineDirective",
292296
"\\u0275\\u0275defineInjectable",
@@ -439,7 +443,9 @@
439443
"consumerMarkDirty",
440444
"consumerPollProducersForChange",
441445
"context",
446+
"controlCreateInternal",
442447
"controlPath",
448+
"controlUpdateInternal",
443449
"convertToBitFlags",
444450
"convertToInjectOptions",
445451
"couldBeInjectableType",
@@ -655,8 +661,11 @@
655661
"hasClassInput",
656662
"hasDeps",
657663
"hasInSkipHydrationBlockFlag",
664+
"hasInput",
658665
"hasLift",
666+
"hasModelInput",
659667
"hasOnDestroy",
668+
"hasOutput",
660669
"hasParentInjector",
661670
"hasStyleInput",
662671
"hasStylingInputShadow",
@@ -678,6 +687,8 @@
678687
"initDomAdapter",
679688
"initFeatures",
680689
"initTNodeFlags",
690+
"initializeControlFirstCreatePass",
691+
"initializeCustomControlStatus",
681692
"initializeDirectives",
682693
"initializeElement",
683694
"initializeInputAndOutputAliases",

0 commit comments

Comments
 (0)