diff --git a/packages/forms/src/directives.ts b/packages/forms/src/directives.ts index d2fda20449..02e455a4ce 100644 --- a/packages/forms/src/directives.ts +++ b/packages/forms/src/directives.ts @@ -12,6 +12,7 @@ import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor import {DefaultValueAccessor} from './directives/default_value_accessor'; import {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; import {NgForm} from './directives/ng_form'; +import {NgFormSelectorWarning} from './directives/ng_form_selector_warning'; import {NgModel} from './directives/ng_model'; import {NgModelGroup} from './directives/ng_model_group'; import {NgNoValidate} from './directives/ng_no_validate_directive'; @@ -32,6 +33,7 @@ export {DefaultValueAccessor} from './directives/default_value_accessor'; export {NgControl} from './directives/ng_control'; export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; export {NgForm} from './directives/ng_form'; +export {NG_FORM_SELECTOR_WARNING, NgFormSelectorWarning} from './directives/ng_form_selector_warning'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; export {NumberValueAccessor} from './directives/number_value_accessor'; @@ -65,7 +67,8 @@ export const SHARED_FORM_DIRECTIVES: Type[] = [ EmailValidator, ]; -export const TEMPLATE_DRIVEN_DIRECTIVES: Type[] = [NgModel, NgModelGroup, NgForm]; +export const TEMPLATE_DRIVEN_DIRECTIVES: Type[] = + [NgModel, NgModelGroup, NgForm, NgFormSelectorWarning]; export const REACTIVE_DRIVEN_DIRECTIVES: Type[] = [FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName]; diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 1ba42526b6..db914b4a33 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -55,16 +55,31 @@ const resolvedPromise = Promise.resolve(null); * unnecessary because the `
` tags are inert. In that case, you would * refrain from using the `formGroup` directive. * + * Support for using `ngForm` element selector has been deprecated in Angular v6 and will be removed + * in Angular v9. + * + * This has been deprecated to keep selectors consistent with other core Angular selectors, + * as element selectors are typically written in kebab-case. + * + * Now deprecated: + * ```html + * + * ``` + * + * After: + * ```html + * + * ``` + * * {@example forms/ts/simpleForm/simple_form_example.ts region='Component'} * * * **npm package**: `@angular/forms` * * * **NgModule**: `FormsModule` * - * */ @Directive({ - selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]', + selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,ng-form,[ngForm]', providers: [formDirectiveProvider], host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'}, outputs: ['ngSubmit'], diff --git a/packages/forms/src/directives/ng_form_selector_warning.ts b/packages/forms/src/directives/ng_form_selector_warning.ts new file mode 100644 index 0000000000..6f418a7fc3 --- /dev/null +++ b/packages/forms/src/directives/ng_form_selector_warning.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, Inject, InjectionToken, Optional} from '@angular/core'; +import {TemplateDrivenErrors} from './template_driven_errors'; + +/** + * Token to provide to turn off the warning when using 'ngForm' deprecated selector. + */ +export const NG_FORM_SELECTOR_WARNING = new InjectionToken('NgFormSelectorWarning'); + +/** + * This directive is solely used to display warnings when the deprecated `ngForm` selector is used. + * + * @deprecated in Angular v6 and will be removed in Angular v9. + * + */ +@Directive({selector: 'ngForm'}) +export class NgFormSelectorWarning { + /** + * Static property used to track whether the deprecation warning for this selector has been sent. + * Used to support warning config of "once". + * + * @internal + */ + static _ngFormWarning = false; + + constructor(@Optional() @Inject(NG_FORM_SELECTOR_WARNING) ngFormWarning: string|null) { + if (((!ngFormWarning || ngFormWarning === 'once') && !NgFormSelectorWarning._ngFormWarning) || + ngFormWarning === 'always') { + TemplateDrivenErrors.ngFormWarning(); + NgFormSelectorWarning._ngFormWarning = true; + } + } +} diff --git a/packages/forms/src/directives/template_driven_errors.ts b/packages/forms/src/directives/template_driven_errors.ts index 2fcc0785e6..6fee9ad084 100644 --- a/packages/forms/src/directives/template_driven_errors.ts +++ b/packages/forms/src/directives/template_driven_errors.ts @@ -57,4 +57,21 @@ export class TemplateDrivenErrors { ${Examples.ngModelGroup}`); } + + static ngFormWarning() { + console.warn(` + It looks like you're using 'ngForm'. + + Support for using the 'ngForm' element selector has been deprecated in Angular v6 and will be removed + in Angular v9. + + Use 'ng-form' instead. + + Before: + + + After: + + `); + } } diff --git a/packages/forms/src/form_providers.ts b/packages/forms/src/form_providers.ts index a0759331f8..e2084343e1 100644 --- a/packages/forms/src/form_providers.ts +++ b/packages/forms/src/form_providers.ts @@ -8,12 +8,10 @@ import {ModuleWithProviders, NgModule} from '@angular/core'; -import {InternalFormsSharedModule, NG_MODEL_WITH_FORM_CONTROL_WARNING, REACTIVE_DRIVEN_DIRECTIVES, TEMPLATE_DRIVEN_DIRECTIVES} from './directives'; +import {InternalFormsSharedModule, NG_FORM_SELECTOR_WARNING, 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. * @@ -24,6 +22,15 @@ import {FormBuilder} from './form_builder'; exports: [InternalFormsSharedModule, TEMPLATE_DRIVEN_DIRECTIVES] }) export class FormsModule { + static withConfig(opts: { + /** @deprecated as of v6 */ warnOnDeprecatedNgFormSelector?: 'never' | 'once' | 'always', + }): ModuleWithProviders { + return { + ngModule: FormsModule, + providers: + [{provide: NG_FORM_SELECTOR_WARNING, useValue: opts.warnOnDeprecatedNgFormSelector}] + }; + } } /** diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index d15871f88d..324f5668da 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -28,6 +28,7 @@ export {Form} from './directives/form_interface'; export {NgControl} from './directives/ng_control'; export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; export {NgForm} from './directives/ng_form'; +export {NgFormSelectorWarning} from './directives/ng_form_selector_warning'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index c2caf9a067..ed487c032e 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -8,7 +8,7 @@ import {Component, Directive, Type, forwardRef} from '@angular/core'; import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; -import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm, NgModel} from '@angular/forms'; +import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm, NgFormSelectorWarning, NgModel} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; @@ -1630,6 +1630,61 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat })); }); + describe('ngForm deprecation warnings', () => { + let warnSpy: jasmine.Spy; + + @Component({selector: 'ng-form-deprecated', template: ``}) + class ngFormDeprecated { + } + + beforeEach(() => { + (NgFormSelectorWarning as any)._ngFormWarning = false; + + warnSpy = spyOn(console, 'warn'); + }); + + describe(`when using the deprecated 'ngForm' selector`, () => { + it(`should only warn once when global provider is provided with "once"`, () => { + TestBed.configureTestingModule({ + declarations: [ngFormDeprecated], + imports: [FormsModule.withConfig({warnOnDeprecatedNgFormSelector: 'once'})] + }); + TestBed.createComponent(ngFormDeprecated); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch(/It looks like you're using 'ngForm'/gi); + }); + + it(`should only warn once by default`, () => { + initTest(ngFormDeprecated); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch(/It looks like you're using 'ngForm'/gi); + }); + + it(`should not warn when global provider is provided with "never"`, () => { + TestBed.configureTestingModule({ + declarations: [ngFormDeprecated], + imports: [FormsModule.withConfig({warnOnDeprecatedNgFormSelector: 'never'})] + }); + TestBed.createComponent(ngFormDeprecated); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it(`should only warn for each instance when global provider is provided with "always"`, + () => { + TestBed.configureTestingModule({ + declarations: [ngFormDeprecated], + imports: [FormsModule.withConfig({warnOnDeprecatedNgFormSelector: 'always'})] + }); + + TestBed.createComponent(ngFormDeprecated); + expect(warnSpy).toHaveBeenCalledTimes(2); + expect(warnSpy.calls.mostRecent().args[0]) + .toMatch(/It looks like you're using 'ngForm'/gi); + }); + }); + }); }); } @@ -1845,7 +1900,7 @@ class NgModelAsyncValidation { selector: 'ng-model-changes-form', template: ` - ` diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index 9defb7acc0..49695f77f5 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -330,6 +330,8 @@ export declare class FormGroupName extends AbstractFormGroupDirective implements } export declare class FormsModule { + static withConfig(opts: { warnOnDeprecatedNgFormSelector?: 'never' | 'once' | 'always'; + }): ModuleWithProviders; } export declare class MaxLengthValidator implements Validator, OnChanges { @@ -398,6 +400,11 @@ export declare class NgForm extends ControlContainer implements Form, AfterViewI updateModel(dir: NgControl, value: any): void; } +/** @deprecated */ +export declare class NgFormSelectorWarning { + constructor(ngFormWarning: string | null); +} + export declare class NgModel extends NgControl implements OnChanges, OnDestroy { readonly asyncValidator: AsyncValidatorFn | null; readonly control: FormControl;