diff --git a/aio/content/guide/form-validation.md b/aio/content/guide/form-validation.md index 3badceaa971c..268b62fa7957 100644 --- a/aio/content/guide/form-validation.md +++ b/aio/content/guide/form-validation.md @@ -191,6 +191,7 @@ Like in AngularJS, Angular automatically mirrors many control properties onto th * `.ng-dirty` * `.ng-untouched` * `.ng-touched` +* `.ng-submitted` The hero form uses the `.ng-valid` and `.ng-invalid` classes to set the color of each form control's border. diff --git a/aio/content/guide/forms.md b/aio/content/guide/forms.md index d7a5e82739f0..3217c6c4bf72 100644 --- a/aio/content/guide/forms.md +++ b/aio/content/guide/forms.md @@ -476,6 +476,22 @@ You can leverage those class names to change the appearance of the control. + + + + The parent form (if any) has been submitted. + + + + ng-submitted + + + + None + + + + Temporarily add a [template reference variable](guide/template-syntax#ref-vars) named `spy` diff --git a/packages/forms/src/directives/ng_control_status.ts b/packages/forms/src/directives/ng_control_status.ts index 735c1b593846..530faa93e74d 100644 --- a/packages/forms/src/directives/ng_control_status.ts +++ b/packages/forms/src/directives/ng_control_status.ts @@ -6,16 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Self} from '@angular/core'; +import {Directive, Optional, Self} from '@angular/core'; import {AbstractControlDirective} from './abstract_control_directive'; import {ControlContainer} from './control_container'; import {NgControl} from './ng_control'; +import {NgForm} from './ng_form'; +import {FormGroupDirective} from './reactive_directives/form_group_directive'; export class AbstractControlStatus { private _cd: AbstractControlDirective; + private _controlContainer: ControlContainer|undefined; - constructor(cd: AbstractControlDirective) { this._cd = cd; } + constructor(cd: AbstractControlDirective, controlContainer?: ControlContainer) { + this._cd = cd; + this._controlContainer = controlContainer; + } get ngClassUntouched(): boolean { return this._cd.control ? this._cd.control.untouched : false; } get ngClassTouched(): boolean { return this._cd.control ? this._cd.control.touched : false; } @@ -24,6 +30,11 @@ export class AbstractControlStatus { get ngClassValid(): boolean { return this._cd.control ? this._cd.control.valid : false; } get ngClassInvalid(): boolean { return this._cd.control ? this._cd.control.invalid : false; } get ngClassPending(): boolean { return this._cd.control ? this._cd.control.pending : false; } + get ngClassSubmitted(): boolean { + return this._controlContainer && this._controlContainer.formDirective ? + (this._controlContainer.formDirective as NgForm | FormGroupDirective).submitted : + false; + } } export const ngControlStatusHost = { @@ -34,6 +45,7 @@ export const ngControlStatusHost = { '[class.ng-valid]': 'ngClassValid', '[class.ng-invalid]': 'ngClassInvalid', '[class.ng-pending]': 'ngClassPending', + '[class.ng-submitted]': 'ngClassSubmitted', }; /** @@ -61,14 +73,16 @@ export const ngControlStatusHost = { */ @Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost}) export class NgControlStatus extends AbstractControlStatus { - constructor(@Self() cd: NgControl) { super(cd); } + constructor(@Self() cd: NgControl, @Optional() controlContainer?: ControlContainer) { + super(cd, controlContainer); + } } /** * @description * Directive automatically applied to Angular form groups that sets CSS classes * based on control status (valid/invalid/dirty/etc). - * + * * @see `NgControlStatus` * * @ngModule ReactiveFormsModule @@ -81,5 +95,5 @@ export class NgControlStatus extends AbstractControlStatus { host: ngControlStatusHost }) export class NgControlStatusGroup extends AbstractControlStatus { - constructor(@Self() cd: ControlContainer) { super(cd); } + constructor(@Self() cd: ControlContainer) { super(cd, cd); } } diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 9a1b144dda26..e69d6f78efb4 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -807,6 +807,14 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + + const formEl = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(formEl, 'submit'); + fixture.detectChanges(); + + expect(sortedClassList(input)).toEqual([ + 'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid' + ]); }); it('should work with formGroup', () => { @@ -830,6 +838,13 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.detectChanges(); expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + + dispatchEvent(formEl, 'submit'); + fixture.detectChanges(); + + expect(sortedClassList(formEl)).toEqual([ + 'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid' + ]); }); }); diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index dcece0fabfef..ad132d146bb2 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -167,6 +167,14 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat dispatchEvent(input, 'input'); fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(sortedClassList(input)).toEqual([ + 'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid' + ]); }); })); @@ -225,6 +233,16 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(sortedClassList(modelGroup)).toEqual([ + 'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid' + ]); + expect(sortedClassList(form)).toEqual([ + 'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid' + ]); }); })); diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index 756a1654d423..c99cbb72b958 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -358,7 +358,7 @@ export declare abstract class NgControl extends AbstractControlDirective { } export declare class NgControlStatus extends AbstractControlStatus { - constructor(cd: NgControl); + constructor(cd: NgControl, controlContainer?: ControlContainer); } export declare class NgControlStatusGroup extends AbstractControlStatus {