Skip to content

Commit b2f0857

Browse files
alxhubmattrbeck
authored andcommitted
refactor(forms): base implementation of custom control binding in NgControl
Update `NgControl` to support binding to custom controls via the new `ngControlCreate` and `ngControlUpdate` lifecycle hooks. This will allow Reactive and Template-driven forms to integrate with components that use the new `FormValueControl` (FVC) binding convention. Key changes: - Implement synchronization of control state between `FormControl` and custom controls. - Add support for `parseErrors` signals from custom controls, integrating them into the Reactive Forms validation loop via a dynamic validator. - Convert Reactive Forms errors into `ReactiveValidationError` objects for consumption by custom controls. - Update `NgModel`, `FormControlDirective`, and `FormControlName` to provide necessary dependencies (`Injector`, `Renderer2`) to `NgControl`. - Rename `setUpControl` to `setUpControlValueAccessor` to clarify its role in the traditional CVA path.
1 parent d1576ee commit b2f0857

13 files changed

Lines changed: 439 additions & 59 deletions

File tree

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { OnInit } from '@angular/core';
1919
import { Renderer2 } from '@angular/core';
2020
import { SimpleChanges } from '@angular/core';
2121
import { Version } from '@angular/core';
22+
import { ɵControlDirectiveHost } from '@angular/core';
2223

2324
// @public
2425
export abstract class AbstractControl<TValue = any, TRawValue extends TValue = TValue, TValueWithOptionalControlStates = any> {
@@ -407,7 +408,7 @@ export const FormControl: ɵFormControlCtor;
407408

408409
// @public
409410
export class FormControlDirective extends NgControl implements OnChanges, OnDestroy {
410-
constructor(validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null, callSetDisabledState?: SetDisabledStateOption | undefined);
411+
constructor(validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null, callSetDisabledState?: SetDisabledStateOption | undefined, renderer?: Renderer2, injector?: Injector);
411412
get control(): FormControl;
412413
form: FormControl;
413414
set isDisabled(isDisabled: boolean);
@@ -423,12 +424,12 @@ export class FormControlDirective extends NgControl implements OnChanges, OnDest
423424
// (undocumented)
424425
static ɵdir: i0.ɵɵDirectiveDeclaration<FormControlDirective, "[formControl]", ["ngForm"], { "form": { "alias": "formControl"; "required": false; }; "isDisabled": { "alias": "disabled"; "required": false; }; "model": { "alias": "ngModel"; "required": false; }; }, { "update": "ngModelChange"; }, never, never, false, never>;
425426
// (undocumented)
426-
static ɵfac: i0.ɵɵFactoryDeclaration<FormControlDirective, [{ optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }, { optional: true; }]>;
427+
static ɵfac: i0.ɵɵFactoryDeclaration<FormControlDirective, [{ optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
427428
}
428429

429430
// @public
430431
export class FormControlName extends NgControl implements OnChanges, OnDestroy {
431-
constructor(parent: ControlContainer, validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null);
432+
constructor(parent: ControlContainer, validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null, renderer?: Renderer2, injector?: Injector);
432433
readonly control: FormControl;
433434
get formDirective(): any;
434435
set isDisabled(isDisabled: boolean);
@@ -444,7 +445,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
444445
// (undocumented)
445446
static ɵdir: i0.ɵɵDirectiveDeclaration<FormControlName, "[formControlName]", never, { "name": { "alias": "formControlName"; "required": false; }; "isDisabled": { "alias": "disabled"; "required": false; }; "model": { "alias": "ngModel"; "required": false; }; }, { "update": "ngModelChange"; }, never, never, false, never>;
446447
// (undocumented)
447-
static ɵfac: i0.ɵɵFactoryDeclaration<FormControlName, [{ optional: true; host: true; skipSelf: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }]>;
448+
static ɵfac: i0.ɵɵFactoryDeclaration<FormControlName, [{ optional: true; host: true; skipSelf: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
448449
}
449450

450451
// @public
@@ -677,10 +678,31 @@ export const NG_VALUE_ACCESSOR: InjectionToken<readonly ControlValueAccessor[]>;
677678

678679
// @public
679680
export abstract class NgControl extends AbstractControlDirective {
680-
constructor(rawValueAccessors?: ControlValueAccessor[]);
681+
constructor(injector?: Injector, renderer?: Renderer2, rawValueAccessors?: ControlValueAccessor[]);
682+
protected customControlBindings: {
683+
value?: unknown;
684+
disabled?: boolean;
685+
touched?: boolean;
686+
dirty?: boolean;
687+
valid?: boolean;
688+
invalid?: boolean;
689+
pending?: boolean;
690+
required?: boolean;
691+
errors?: ValidationErrors | null;
692+
} | null;
693+
// (undocumented)
694+
protected isCustomControlBased: boolean;
681695
name: string | number | null;
682696
// (undocumented)
697+
protected ngControlUpdate(host: ɵControlDirectiveHost, bindRequired: boolean): void;
698+
protected parseErrorsValidator: ValidatorFn | null;
699+
// (undocumented)
700+
protected removeParseErrorsValidator(control: AbstractControl | null | undefined): void;
701+
// (undocumented)
683702
protected get selectedValueAccessor(): ControlValueAccessor | null;
703+
// (undocumented)
704+
protected setupCustomControl(): void;
705+
protected get shouldBindRequired(): boolean;
684706
valueAccessor: ControlValueAccessor | null;
685707
abstract viewToModelUpdate(newValue: any): void;
686708
}
@@ -740,7 +762,7 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit {
740762

741763
// @public
742764
export class NgModel extends NgControl implements OnChanges, OnDestroy {
743-
constructor(parent: ControlContainer, validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _changeDetectorRef?: (ChangeDetectorRef | null) | undefined, callSetDisabledState?: SetDisabledStateOption | undefined);
765+
constructor(parent: ControlContainer, validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _changeDetectorRef?: (ChangeDetectorRef | null) | undefined, callSetDisabledState?: SetDisabledStateOption | undefined, injector?: Injector, renderer?: Renderer2);
744766
// (undocumented)
745767
readonly control: FormControl;
746768
get formDirective(): any;
@@ -762,7 +784,7 @@ export class NgModel extends NgControl implements OnChanges, OnDestroy {
762784
// (undocumented)
763785
static ɵdir: i0.ɵɵDirectiveDeclaration<NgModel, "[ngModel]:not([formControlName]):not([formControl])", ["ngModel"], { "name": { "alias": "name"; "required": false; }; "isDisabled": { "alias": "disabled"; "required": false; }; "model": { "alias": "ngModel"; "required": false; }; "options": { "alias": "ngModelOptions"; "required": false; }; }, { "update": "ngModelChange"; }, never, never, false, never>;
764786
// (undocumented)
765-
static ɵfac: i0.ɵɵFactoryDeclaration<NgModel, [{ optional: true; host: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }, { optional: true; }]>;
787+
static ɵfac: i0.ɵɵFactoryDeclaration<NgModel, [{ optional: true; host: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
766788
}
767789

768790
// @public

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
"AbstractControlStatus",
1515
"AbstractFormDirective",
1616
"AbstractFormGroupDirective",
17+
"AbstractValidatorDirective",
1718
"AfterRenderManager",
1819
"AnonymousSubject",
1920
"ApplicationInitStatus",
2021
"ApplicationModule",
2122
"ApplicationRef",
23+
"BASE_EFFECT_NODE",
2224
"BINDING",
2325
"BLOOM_BUCKET_BITS",
2426
"BLOOM_MASK",
@@ -52,6 +54,7 @@
5254
"ChangeDetectionScheduler",
5355
"ChangeDetectionSchedulerImpl",
5456
"ChangeDetectionStrategy",
57+
"ChangeDetectorRef",
5558
"CheckboxControlValueAccessor",
5659
"CommonModule",
5760
"ComponentFactory",
@@ -84,6 +87,7 @@
8487
"DomRendererFactory2",
8588
"EFFECTS",
8689
"EFFECTS_TO_SCHEDULE",
90+
"EFFECT_NODE",
8791
"EMAIL_REGEXP",
8892
"EMBEDDED_VIEW_INJECTOR",
8993
"EMPTY_ARRAY",
@@ -152,6 +156,7 @@
152156
"NEXT",
153157
"NG_ASYNC_VALIDATORS",
154158
"NG_COMP_DEF",
159+
"NG_CONTROL_PARSE_ERRORS_PROVIDER",
155160
"NG_DIR_DEF",
156161
"NG_ELEMENT_ID",
157162
"NG_ENV_ID",
@@ -217,12 +222,16 @@
217222
"REMOVE_STYLES_ON_COMPONENT_DESTROY",
218223
"REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT",
219224
"RENDERER",
225+
"REQUIRED_VALIDATOR",
226+
"ROOT_EFFECT_NODE",
220227
"ReactiveFormsComponent",
221228
"ReactiveFormsComponent_div_14_Template",
222229
"ReactiveFormsModule",
230+
"ReactiveValidationError",
223231
"Renderer2",
224232
"RendererFactory2",
225233
"RendererStyleFlags2",
234+
"RequiredValidator",
226235
"RetrievingInjector",
227236
"RootComponent",
228237
"RuntimeError",
@@ -263,22 +272,26 @@
263272
"USE_VALUE",
264273
"UnsubscriptionError",
265274
"VALID",
275+
"VIEW_EFFECT_NODE",
266276
"VIEW_REFS",
267277
"Validators",
268278
"ValueChangeEvent",
269279
"ViewContainerRef",
280+
"ViewContext",
270281
"ViewEncapsulation",
271282
"ViewRef",
272283
"XSS_SECURITY_URL",
273284
"XhrFactory",
274285
"ZONELESS_ENABLED",
275286
"ZoneAwareEffectScheduler",
287+
"\\u0275FORM_FIELD_PARSE_ERRORS",
276288
"\\u0275InternalFormsSharedModule",
277289
"\\u0275NgNoValidate",
278290
"\\u0275\\u0275InheritDefinitionFeature",
279291
"\\u0275\\u0275NgOnChangesFeature",
280292
"\\u0275\\u0275ProvidersFeature",
281293
"\\u0275\\u0275advance",
294+
"\\u0275\\u0275attribute",
282295
"\\u0275\\u0275classProp",
283296
"\\u0275\\u0275defineComponent",
284297
"\\u0275\\u0275defineDirective",
@@ -382,6 +395,7 @@
382395
"bloomAdd",
383396
"bloomHasToken",
384397
"bloomHashBitOrFactory",
398+
"booleanAttribute",
385399
"bootstrap",
386400
"bootstrapApp",
387401
"borrowReactiveLViewConsumer",
@@ -439,6 +453,7 @@
439453
"createContainerAnchorImpl",
440454
"createContainerRef",
441455
"createDirectivesInstances",
456+
"createEffectFn",
442457
"createElementNode",
443458
"createElementRef",
444459
"createEnvironmentInjector",
@@ -459,6 +474,7 @@
459474
"createPlatform",
460475
"createPlatformFactory",
461476
"createPlatformInjector",
477+
"createRootEffect",
462478
"createRootLViewEnvironment",
463479
"createRootTView",
464480
"createRootViewInjector",
@@ -471,6 +487,8 @@
471487
"createTemplateRef",
472488
"createTextNode",
473489
"createViewBlueprint",
490+
"createViewEffect",
491+
"createViewRef",
474492
"cyclicDependencyError",
475493
"declareDirectiveHostTemplate",
476494
"decreaseElementDepthCount",
@@ -496,6 +514,8 @@
496514
"diPublicInInjector",
497515
"directiveHostEndFirstCreatePass",
498516
"directiveHostFirstCreatePass",
517+
"effect",
518+
"elementAttributeInternal",
499519
"elementLikeEndShared",
500520
"elementLikeStartShared",
501521
"emailValidator",
@@ -668,13 +688,15 @@
668688
"initializeInputAndOutputAliases",
669689
"inject2",
670690
"injectArgs",
691+
"injectChangeDetectorRef",
671692
"injectDestroyRef",
672693
"injectElementRef",
673694
"injectInjectorOnly",
674695
"injectRenderer2",
675696
"injectRootLimpMode",
676697
"injectTemplateRef",
677698
"injectViewContainerRef",
699+
"injectViewContext",
678700
"injectableDefOrInjectorDefFactory",
679701
"innerFrom",
680702
"insertAnchorNode",
@@ -726,6 +748,7 @@
726748
"isLView",
727749
"isListLikeIterable",
728750
"isNameOnlyAttributeMarker",
751+
"isNativeFormElement",
729752
"isNodeMatchingSelector",
730753
"isNodeMatchingSelectorList",
731754
"isNotFound",
@@ -892,6 +915,7 @@
892915
"removeViewFromDOM",
893916
"renderChildComponents",
894917
"renderComponent",
918+
"renderStringify",
895919
"renderView",
896920
"reportUnhandledError",
897921
"requiredTrueValidator",
@@ -906,6 +930,7 @@
906930
"retrieveHydrationInfo",
907931
"reusedNodes",
908932
"runAfterLeaveAnimations",
933+
"runEffect",
909934
"runEffectsInView",
910935
"runInInjectionContext",
911936
"runLeaveAnimationsWithCallback",
@@ -938,13 +963,15 @@
938963
"setDisabledStateDefault",
939964
"setDocument",
940965
"setDomProperty",
966+
"setElementAttribute",
941967
"setIncludeViewProviders",
942968
"setInjectImplementation",
943969
"setInputsFromAttrs",
944970
"setIsRefreshingViews",
945971
"setJitOptions",
946972
"setLocaleId",
947973
"setModuleBootstrapImpl",
974+
"setNativeDomProperty",
948975
"setPropertyAndInputs",
949976
"setRootDomAdapter",
950977
"setSelectedIndex",
@@ -959,7 +986,7 @@
959986
"setThrowInvalidWriteToSignalError",
960987
"setUpAttributes",
961988
"setUpBlurPipeline",
962-
"setUpControl",
989+
"setUpControlValueAccessor",
963990
"setUpDisabledChangeHandler",
964991
"setUpFormContainer",
965992
"setUpModelChangePipeline",

0 commit comments

Comments
 (0)