diff --git a/packages/forms/src/directives.ts b/packages/forms/src/directives.ts index 41a298e90e..d2fda20449 100644 --- a/packages/forms/src/directives.ts +++ b/packages/forms/src/directives.ts @@ -37,7 +37,7 @@ export {NgModelGroup} from './directives/ng_model_group'; export {NumberValueAccessor} from './directives/number_value_accessor'; export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; export {RangeValueAccessor} from './directives/range_value_accessor'; -export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; +export {FormControlDirective, NG_MODEL_WITH_FORM_CONTROL_WARNING} from './directives/reactive_directives/form_control_directive'; export {FormControlName} from './directives/reactive_directives/form_control_name'; export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; export {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; diff --git a/packages/forms/src/directives/reactive_directives/form_control_directive.ts b/packages/forms/src/directives/reactive_directives/form_control_directive.ts index 6fd76607d2..8f36a785ad 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_directive.ts @@ -6,16 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, EventEmitter, Inject, InjectionToken, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; import {FormControl} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor'; import {NgControl} from '../ng_control'; import {ReactiveErrors} from '../reactive_errors'; -import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared'; +import {_ngModelWarning, composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators'; + +/** + * Token to provide to turn off the ngModel warning on formControl and formControlName. + */ +export const NG_MODEL_WITH_FORM_CONTROL_WARNING = + new InjectionToken('NgModelWithFormControlWarning'); + export const formControlBinding: any = { provide: NgControl, useExisting: forwardRef(() => FormControlDirective) @@ -61,6 +68,72 @@ export const formControlBinding: any = { * * * **NgModule**: `ReactiveFormsModule` * + * ### Use with ngModel + * + * Support for using the `ngModel` input property and `ngModelChange` event with reactive + * form directives has been deprecated in Angular v6 and will be removed in Angular v7. + * + * Now deprecated: + * ```html + * + * ``` + * + * ```ts + * this.value = 'some value'; + * ``` + * + * This has been deprecated for a few reasons. First, developers have found this pattern + * confusing. It seems like the actual `ngModel` directive is being used, but in fact it's + * an input/output property named `ngModel` on the reactive form directive that simply + * approximates (some of) its behavior. Specifically, it allows getting/setting the value + * and intercepting value events. However, some of `ngModel`'s other features - like + * delaying updates with`ngModelOptions` or exporting the directive - simply don't work, + * which has understandably caused some confusion. + * + * In addition, this pattern mixes template-driven and reactive forms strategies, which + * we generally don't recommend because it doesn't take advantage of the full benefits of + * either strategy. Setting the value in the template violates the template-agnostic + * principles behind reactive forms, whereas adding a `FormControl`/`FormGroup` layer in + * the class removes the convenience of defining forms in the template. + * + * To update your code before v7, you'll want to decide whether to stick with reactive form + * directives (and get/set values using reactive forms patterns) or switch over to + * template-driven directives. + * + * After (choice 1 - use reactive forms): + * + * ```html + * + * ``` + * + * ```ts + * this.control.setValue('some value'); + * ``` + * + * After (choice 2 - use template-driven forms): + * + * ```html + * + * ``` + * + * ```ts + * this.value = 'some value'; + * ``` + * + * By default, when you use this pattern, you will see a deprecation warning once in dev + * mode. You can choose to silence this warning by providing a config for + * `ReactiveFormsModule` at import time: + * + * ```ts + * imports: [ + * ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'never'}); + * ] + * ``` + * + * Alternatively, you can choose to surface a separate warning for each instance of this + * pattern with a config value of `"always"`. This may help to track down where in the code + * the pattern is being used as the code is being updated. + * * @stable */ @Directive({selector: '[formControl]', providers: [formControlBinding], exportAs: 'ngForm'}) @@ -69,16 +142,39 @@ export class FormControlDirective extends NgControl implements OnChanges { viewModel: any; @Input('formControl') form: FormControl; - @Input('ngModel') model: any; - @Output('ngModelChange') update = new EventEmitter(); @Input('disabled') set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } + // TODO(kara): remove next 4 properties once deprecation period is over + + /** @deprecated as of v6 */ + @Input('ngModel') model: any; + + /** @deprecated as of v6 */ + @Output('ngModelChange') update = new EventEmitter(); + + /** + * Static property used to track whether any ngModel warnings have been sent across + * all instances of FormControlDirective. Used to support warning config of "once". + * + * @internal + */ + static _ngModelWarningSentOnce = false; + + /** + * Instance property used to track whether an ngModel warning has been sent out for this + * particular FormControlDirective instance. Used to support warning config of "always". + * + * @internal + */ + _ngModelWarningSent = false; + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) - valueAccessors: ControlValueAccessor[]) { + valueAccessors: ControlValueAccessor[], + @Optional() @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING) private _ngModelWarningConfig: string|null) { super(); this._rawValidators = validators || []; this._rawAsyncValidators = asyncValidators || []; @@ -94,6 +190,8 @@ export class FormControlDirective extends NgControl implements OnChanges { this.form.updateValueAndValidity({emitEvent: false}); } if (isPropertyUpdated(changes, this.viewModel)) { + _ngModelWarning( + 'formControl', FormControlDirective, this, this._ngModelWarningConfig); this.form.setValue(this.model); this.viewModel = this.model; } diff --git a/packages/forms/src/directives/reactive_directives/form_control_name.ts b/packages/forms/src/directives/reactive_directives/form_control_name.ts index a53f69b68d..04747e608f 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_name.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_name.ts @@ -15,9 +15,10 @@ import {ControlContainer} from '../control_container'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor'; import {NgControl} from '../ng_control'; import {ReactiveErrors} from '../reactive_errors'; -import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared'; +import {_ngModelWarning, composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators'; +import {NG_MODEL_WITH_FORM_CONTROL_WARNING} from './form_control_directive'; import {FormGroupDirective} from './form_group_directive'; import {FormArrayName, FormGroupName} from './form_group_name'; @@ -75,6 +76,76 @@ export const controlNameBinding: any = { * * **NgModule**: {@link ReactiveFormsModule} * + * ### Use with ngModel + * + * Support for using the `ngModel` input property and `ngModelChange` event with reactive + * form directives has been deprecated in Angular v6 and will be removed in Angular v7. + * + * Now deprecated: + * ```html + *
+ * + *
+ * ``` + * + * ```ts + * this.value = 'some value'; + * ``` + * + * This has been deprecated for a few reasons. First, developers have found this pattern + * confusing. It seems like the actual `ngModel` directive is being used, but in fact it's + * an input/output property named `ngModel` on the reactive form directive that simply + * approximates (some of) its behavior. Specifically, it allows getting/setting the value + * and intercepting value events. However, some of `ngModel`'s other features - like + * delaying updates with`ngModelOptions` or exporting the directive - simply don't work, + * which has understandably caused some confusion. + * + * In addition, this pattern mixes template-driven and reactive forms strategies, which + * we generally don't recommend because it doesn't take advantage of the full benefits of + * either strategy. Setting the value in the template violates the template-agnostic + * principles behind reactive forms, whereas adding a `FormControl`/`FormGroup` layer in + * the class removes the convenience of defining forms in the template. + * + * To update your code before v7, you'll want to decide whether to stick with reactive form + * directives (and get/set values using reactive forms patterns) or switch over to + * template-driven directives. + * + * After (choice 1 - use reactive forms): + * + * ```html + *
+ * + *
+ * ``` + * + * ```ts + * this.form.get('first').setValue('some value'); + * ``` + * + * After (choice 2 - use template-driven forms): + * + * ```html + * + * ``` + * + * ```ts + * this.value = 'some value'; + * ``` + * + * By default, when you use this pattern, you will see a deprecation warning once in dev + * mode. You can choose to silence this warning by providing a config for + * `ReactiveFormsModule` at import time: + * + * ```ts + * imports: [ + * ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'never'}); + * ] + * ``` + * + * Alternatively, you can choose to surface a separate warning for each instance of this + * pattern with a config value of `"always"`. This may help to track down where in the code + * the pattern is being used as the code is being updated. + * * @stable */ @Directive({selector: '[formControlName]', providers: [controlNameBinding]}) @@ -86,18 +157,41 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { @Input('formControlName') name: string; - // TODO(kara): Replace ngModel with reactive API - @Input('ngModel') model: any; - @Output('ngModelChange') update = new EventEmitter(); @Input('disabled') set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } + // TODO(kara): remove next 4 properties once deprecation period is over + + /** @deprecated as of v6 */ + @Input('ngModel') model: any; + + /** @deprecated as of v6 */ + @Output('ngModelChange') update = new EventEmitter(); + + /** + * Static property used to track whether any ngModel warnings have been sent across + * all instances of FormControlName. Used to support warning config of "once". + * + * @internal + */ + static _ngModelWarningSentOnce = false; + + /** + * Instance property used to track whether an ngModel warning has been sent out for this + * particular FormControlName instance. Used to support warning config of "always". + * + * @internal + */ + _ngModelWarningSent = false; + constructor( @Optional() @Host() @SkipSelf() parent: ControlContainer, @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, - @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[], + @Optional() @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING) private _ngModelWarningConfig: string| + null) { super(); this._parent = parent; this._rawValidators = validators || []; @@ -108,6 +202,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges) { if (!this._added) this._setUpControl(); if (isPropertyUpdated(changes, this.viewModel)) { + _ngModelWarning('formControlName', FormControlName, this, this._ngModelWarningConfig); this.viewModel = this.model; this.formDirective.updateModel(this, this.model); } diff --git a/packages/forms/src/directives/reactive_errors.ts b/packages/forms/src/directives/reactive_errors.ts index 0f9286fb8e..acca6a319f 100644 --- a/packages/forms/src/directives/reactive_errors.ts +++ b/packages/forms/src/directives/reactive_errors.ts @@ -74,4 +74,17 @@ export class ReactiveErrors { }); `); } + + static ngModelWarning(directiveName: string): void { + console.warn(` + It looks like you're using ngModel on the same form field as ${directiveName}. + Support for using the ngModel input property and ngModelChange event with + reactive form directives has been deprecated in Angular v6 and will be removed + in Angular v7. + + For more information on this, see our API docs here: + https://angular.io/api/forms/${directiveName === 'formControl' ? 'FormControlDirective' + : 'FormControlName'}#use-with-ngmodel + `); + } } diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 0d853f84fc..38a3779b7f 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵlooseIdentical as looseIdentical} from '@angular/core'; +import {isDevMode, ɵlooseIdentical as looseIdentical} from '@angular/core'; + import {FormArray, FormControl, FormGroup} from '../model'; import {Validators} from '../validators'; import {AbstractControlDirective} from './abstract_control_directive'; @@ -21,6 +22,7 @@ import {NumberValueAccessor} from './number_value_accessor'; import {RadioControlValueAccessor} from './radio_control_value_accessor'; import {RangeValueAccessor} from './range_value_accessor'; import {FormArrayName} from './reactive_directives/form_group_name'; +import {ReactiveErrors} from './reactive_errors'; import {SelectControlValueAccessor} from './select_control_value_accessor'; import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators'; @@ -216,3 +218,17 @@ export function removeDir(list: T[], el: T): void { const index = list.indexOf(el); if (index > -1) list.splice(index, 1); } + +// TODO(kara): remove after deprecation period +export function _ngModelWarning( + name: string, type: {_ngModelWarningSentOnce: boolean}, + instance: {_ngModelWarningSent: boolean}, warningConfig: string | null) { + if (!isDevMode() || warningConfig === 'never') return; + + if (((warningConfig === null || warningConfig === 'once') && !type._ngModelWarningSentOnce) || + (warningConfig === 'always' && !instance._ngModelWarningSent)) { + ReactiveErrors.ngModelWarning(name); + type._ngModelWarningSentOnce = true; + instance._ngModelWarningSent = true; + } +} diff --git a/packages/forms/src/form_providers.ts b/packages/forms/src/form_providers.ts index a7dd83bdf8..fd2b452a89 100644 --- a/packages/forms/src/form_providers.ts +++ b/packages/forms/src/form_providers.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgModule} from '@angular/core'; +import {ModuleWithProviders, NgModule} from '@angular/core'; -import {InternalFormsSharedModule, REACTIVE_DRIVEN_DIRECTIVES, TEMPLATE_DRIVEN_DIRECTIVES} from './directives'; +import {InternalFormsSharedModule, NG_MODEL_WITH_FORM_CONTROL_WARNING, REACTIVE_DRIVEN_DIRECTIVES, TEMPLATE_DRIVEN_DIRECTIVES} from './directives'; import {RadioControlRegistry} from './directives/radio_control_value_accessor'; import {FormBuilder} from './form_builder'; + /** * The ng module for forms. * @stable @@ -35,4 +36,15 @@ export class FormsModule { exports: [InternalFormsSharedModule, REACTIVE_DRIVEN_DIRECTIVES] }) export class ReactiveFormsModule { + static withConfig(opts: { + /** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never' | 'once' | 'always' + }): ModuleWithProviders { + return { + ngModule: ReactiveFormsModule, + providers: [{ + provide: NG_MODEL_WITH_FORM_CONTROL_WARNING, + useValue: opts.warnOnNgModelWithFormControl + }] + }; + } } diff --git a/packages/forms/test/directives_spec.ts b/packages/forms/test/directives_spec.ts index dd81a8b4f0..ef1fd2711e 100644 --- a/packages/forms/test/directives_spec.ts +++ b/packages/forms/test/directives_spec.ts @@ -137,7 +137,7 @@ function asyncValidator(expected: any, timeout = 0) { form.form = formModel; loginControlDir = new FormControlName( - form, [Validators.required], [asyncValidator('expected')], [defaultAccessor]); + form, [Validators.required], [asyncValidator('expected')], [defaultAccessor], null); loginControlDir.name = 'login'; loginControlDir.valueAccessor = new DummyControlValueAccessor(); }); @@ -168,7 +168,7 @@ function asyncValidator(expected: any, timeout = 0) { describe('addControl', () => { it('should throw when no control found', () => { - const dir = new FormControlName(form, null !, null !, [defaultAccessor]); + const dir = new FormControlName(form, null !, null !, [defaultAccessor], null); dir.name = 'invalidName'; expect(() => form.addControl(dir)) @@ -176,7 +176,7 @@ function asyncValidator(expected: any, timeout = 0) { }); it('should throw for a named control when no value accessor', () => { - const dir = new FormControlName(form, null !, null !, null !); + const dir = new FormControlName(form, null !, null !, null !, null); dir.name = 'login'; expect(() => form.addControl(dir)) @@ -185,7 +185,7 @@ function asyncValidator(expected: any, timeout = 0) { it('should throw when no value accessor with path', () => { const group = new FormGroupName(form, null !, null !); - const dir = new FormControlName(group, null !, null !, null !); + const dir = new FormControlName(group, null !, null !, null !, null); group.name = 'passwords'; dir.name = 'password'; @@ -496,7 +496,7 @@ function asyncValidator(expected: any, timeout = 0) { }; beforeEach(() => { - controlDir = new FormControlDirective([Validators.required], [], [defaultAccessor]); + controlDir = new FormControlDirective([Validators.required], [], [defaultAccessor], null); controlDir.valueAccessor = new DummyControlValueAccessor(); control = new FormControl(null); @@ -648,7 +648,7 @@ function asyncValidator(expected: any, timeout = 0) { const parent = new FormGroupDirective([], []); parent.form = new FormGroup({'name': formModel}); - controlNameDir = new FormControlName(parent, [], [], [defaultAccessor]); + controlNameDir = new FormControlName(parent, [], [], [defaultAccessor], null); controlNameDir.name = 'name'; (controlNameDir as{control: FormControl}).control = formModel; }); diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 9459bfb822..81b4edef29 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -1627,9 +1627,94 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; describe('ngModel interactions', () => { + describe('deprecation warnings', () => { + let warnSpy: any; + + beforeEach(() => { + warnSpy = jasmine.createSpy('warn'); + console.warn = warnSpy; + }); + + it('should warn once by default when using ngModel with formControlName', fakeAsync(() => { + const fixture = initTest(FormGroupNgModel); + fixture.componentInstance.form = + new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}); + fixture.detectChanges(); + tick(); + + expect(warnSpy.calls.count()).toEqual(1); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch( + /It looks like you're using ngModel on the same form field as formControlName/gi); + + fixture.componentInstance.login = 'some value'; + fixture.detectChanges(); + tick(); + + expect(warnSpy.calls.count()).toEqual(1); + })); + + it('should warn once by default when using ngModel with formControl', fakeAsync(() => { + const fixture = initTest(FormControlNgModel); + fixture.componentInstance.control = new FormControl(''); + fixture.componentInstance.passwordControl = new FormControl(''); + fixture.detectChanges(); + tick(); + + expect(warnSpy.calls.count()).toEqual(1); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch( + /It looks like you're using ngModel on the same form field as formControl/gi); + + fixture.componentInstance.login = 'some value'; + fixture.detectChanges(); + tick(); + + expect(warnSpy.calls.count()).toEqual(1); + })); + + it('should warn once for each instance when global provider is provided with "always"', + fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [FormControlNgModel], + imports: + [ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'always'})] + }); + + const fixture = TestBed.createComponent(FormControlNgModel); + fixture.componentInstance.control = new FormControl(''); + fixture.componentInstance.passwordControl = new FormControl(''); + fixture.detectChanges(); + tick(); + + expect(warnSpy.calls.count()).toEqual(2); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch( + /It looks like you're using ngModel on the same form field as formControl/gi); + })); + + it('should silence warnings when global provider is provided with "never"', + fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [FormControlNgModel], + imports: [ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'never'})] + }); + + const fixture = TestBed.createComponent(FormControlNgModel); + fixture.componentInstance.control = new FormControl(''); + fixture.componentInstance.passwordControl = new FormControl(''); + fixture.detectChanges(); + tick(); + + expect(warnSpy).not.toHaveBeenCalled(); + })); + + }); + it('should support ngModel for complex forms', fakeAsync(() => { const fixture = initTest(FormGroupNgModel); - fixture.componentInstance.form = new FormGroup({'login': new FormControl('')}); + fixture.componentInstance.form = + new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}); fixture.componentInstance.login = 'oldValue'; fixture.detectChanges(); tick(); @@ -1647,6 +1732,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; it('should support ngModel for single fields', fakeAsync(() => { const fixture = initTest(FormControlNgModel); fixture.componentInstance.control = new FormControl(''); + fixture.componentInstance.passwordControl = new FormControl(''); fixture.componentInstance.login = 'oldValue'; fixture.detectChanges(); tick(); @@ -1665,6 +1751,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; if (isNode) return; const fixture = initTest(FormControlNgModel); fixture.componentInstance.control = new FormControl(''); + fixture.componentInstance.passwordControl = new FormControl(''); fixture.detectChanges(); tick(); @@ -1682,7 +1769,8 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; it('should work with updateOn submit', fakeAsync(() => { const fixture = initTest(FormGroupNgModel); - const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); + const formGroup = new FormGroup( + {login: new FormControl('', {updateOn: 'submit'}), password: new FormControl('')}); fixture.componentInstance.form = formGroup; fixture.componentInstance.login = 'initial'; fixture.detectChanges(); @@ -2449,20 +2537,27 @@ class FormArrayNestedGroup { template: `
+
` }) class FormGroupNgModel { form: FormGroup; login: string; + password: string; } @Component({ selector: 'form-control-ng-model', - template: `` + template: ` + + + ` }) class FormControlNgModel { control: FormControl; login: string; + passwordControl: FormControl; + password: string; } @Component({ diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index a718a14a68..f9b72df3cc 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -256,12 +256,12 @@ export declare class FormControlDirective extends NgControl implements OnChanges readonly control: FormControl; form: FormControl; isDisabled: boolean; - model: any; + /** @deprecated */ model: any; readonly path: string[]; - update: EventEmitter<{}>; + /** @deprecated */ update: EventEmitter<{}>; readonly validator: ValidatorFn | null; viewModel: any; - constructor(validators: Array, asyncValidators: Array, valueAccessors: ControlValueAccessor[]); + constructor(validators: Array, asyncValidators: Array, valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null); ngOnChanges(changes: SimpleChanges): void; viewToModelUpdate(newValue: any): void; } @@ -272,12 +272,12 @@ export declare class FormControlName extends NgControl implements OnChanges, OnD readonly control: FormControl; readonly formDirective: any; isDisabled: boolean; - model: any; + /** @deprecated */ model: any; name: string; readonly path: string[]; - update: EventEmitter<{}>; + /** @deprecated */ update: EventEmitter<{}>; readonly validator: ValidatorFn | null; - constructor(parent: ControlContainer, validators: Array, asyncValidators: Array, valueAccessors: ControlValueAccessor[]); + constructor(parent: ControlContainer, validators: Array, asyncValidators: Array, valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null); ngOnChanges(changes: SimpleChanges): void; ngOnDestroy(): void; viewToModelUpdate(newValue: any): void; @@ -491,6 +491,8 @@ export declare class RadioControlValueAccessor implements ControlValueAccessor, /** @stable */ export declare class ReactiveFormsModule { + static withConfig(opts: { warnOnNgModelWithFormControl: 'never' | 'once' | 'always'; + }): ModuleWithProviders; } /** @stable */