diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index df448ef4a1..fdce1d899d 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/forms/src/directives.ts @@ -10,7 +10,7 @@ import {NgModule, Type} from '@angular/core'; import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; import {DefaultValueAccessor} from './directives/default_value_accessor'; -import {NgControlStatus} from './directives/ng_control_status'; +import {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; import {NgForm} from './directives/ng_form'; import {NgModel} from './directives/ng_model'; import {NgModelGroup} from './directives/ng_model_group'; @@ -28,7 +28,7 @@ export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor export {ControlValueAccessor} from './directives/control_value_accessor'; export {DefaultValueAccessor} from './directives/default_value_accessor'; export {NgControl} from './directives/ng_control'; -export {NgControlStatus} from './directives/ng_control_status'; +export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; export {NgForm} from './directives/ng_form'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; @@ -45,8 +45,8 @@ export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValida export const SHARED_FORM_DIRECTIVES: Type[] = [ NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor, - RadioControlValueAccessor, NgControlStatus, RequiredValidator, MinLengthValidator, - MaxLengthValidator, PatternValidator + RadioControlValueAccessor, NgControlStatus, NgControlStatusGroup, RequiredValidator, + MinLengthValidator, MaxLengthValidator, PatternValidator ]; export const TEMPLATE_DRIVEN_DIRECTIVES: Type[] = [NgModel, NgModelGroup, NgForm]; diff --git a/modules/@angular/forms/src/directives/ng_control_status.ts b/modules/@angular/forms/src/directives/ng_control_status.ts index bd41d87d58..41ff15a3ae 100644 --- a/modules/@angular/forms/src/directives/ng_control_status.ts +++ b/modules/@angular/forms/src/directives/ng_control_status.ts @@ -10,30 +10,14 @@ import {Directive, Self} from '@angular/core'; import {isPresent} from '../facade/lang'; +import {AbstractControlDirective} from './abstract_control_directive'; +import {ControlContainer} from './control_container'; import {NgControl} from './ng_control'; +export class AbstractControlStatus { + private _cd: AbstractControlDirective; -/** - * Directive automatically applied to Angular forms that sets CSS classes - * based on control status (valid/invalid/dirty/etc). - * - * @experimental - */ -@Directive({ - selector: '[formControlName],[ngModel],[formControl]', - host: { - '[class.ng-untouched]': 'ngClassUntouched', - '[class.ng-touched]': 'ngClassTouched', - '[class.ng-pristine]': 'ngClassPristine', - '[class.ng-dirty]': 'ngClassDirty', - '[class.ng-valid]': 'ngClassValid', - '[class.ng-invalid]': 'ngClassInvalid' - } -}) -export class NgControlStatus { - private _cd: NgControl; - - constructor(@Self() cd: NgControl) { this._cd = cd; } + constructor(cd: AbstractControlDirective) { this._cd = cd; } get ngClassUntouched(): boolean { return isPresent(this._cd.control) ? this._cd.control.untouched : false; @@ -51,6 +35,41 @@ export class NgControlStatus { return isPresent(this._cd.control) ? this._cd.control.valid : false; } get ngClassInvalid(): boolean { - return isPresent(this._cd.control) ? !this._cd.control.valid : false; + return isPresent(this._cd.control) ? this._cd.control.invalid : false; } } + +export const ngControlStatusHost = { + '[class.ng-untouched]': 'ngClassUntouched', + '[class.ng-touched]': 'ngClassTouched', + '[class.ng-pristine]': 'ngClassPristine', + '[class.ng-dirty]': 'ngClassDirty', + '[class.ng-valid]': 'ngClassValid', + '[class.ng-invalid]': 'ngClassInvalid' +}; + +/** + * Directive automatically applied to Angular form controls that sets CSS classes + * based on control status (valid/invalid/dirty/etc). + * + * @experimental + */ +@Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost}) +export class NgControlStatus extends AbstractControlStatus { + constructor(@Self() cd: NgControl) { super(cd); } +} + +/** + * Directive automatically applied to Angular form groups that sets CSS classes + * based on control status (valid/invalid/dirty/etc). + * + * @experimental + */ +@Directive({ + selector: + '[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]', + host: ngControlStatusHost +}) +export class NgControlStatusGroup extends AbstractControlStatus { + constructor(@Self() cd: ControlContainer) { super(cd); } +} diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index eb975f8bab..0e45c621a6 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/forms/src/forms.ts @@ -30,7 +30,7 @@ export {ControlValueAccessor, NG_VALUE_ACCESSOR} from './directives/control_valu export {DefaultValueAccessor} from './directives/default_value_accessor'; export {Form} from './directives/form_interface'; export {NgControl} from './directives/ng_control'; -export {NgControlStatus} from './directives/ng_control_status'; +export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status'; export {NgForm} from './directives/ng_form'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index 93aa5a150f..5a7e416050 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -1178,11 +1178,11 @@ export function main() { }); })); - it('should work with complex model-driven forms', + it('should work with single fields in parent forms', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - var form = new FormGroup({'name': new FormControl('', Validators.required)}); + const form = new FormGroup({'name': new FormControl('', Validators.required)}); const t = `
`; @@ -1191,7 +1191,8 @@ export function main() { fixture.debugElement.componentInstance.form = form; fixture.detectChanges(); - var input = fixture.debugElement.query(By.css('input')).nativeElement; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + expect(sortedClassList(input)).toEqual([ 'ng-invalid', 'ng-pristine', 'ng-untouched' ]); @@ -1211,6 +1212,57 @@ export function main() { async.done(); }); })); + + it('should work with formGroup and formGroupName', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const form = new FormGroup( + {'person': new FormGroup({'name': new FormControl('', Validators.required)})}); + + const t = `
+
+ +
+
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + const formGroup = + fixture.debugElement.query(By.css('[formGroupName]')).nativeElement; + const formEl = fixture.debugElement.query(By.css('form')).nativeElement; + + expect(sortedClassList(formGroup)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-untouched' + ]); + + expect(sortedClassList(formEl)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-untouched' + ]); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(sortedClassList(formGroup)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-touched' + ]); + + expect(sortedClassList(formEl)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-touched' + ]); + + input.value = 'updatedValue'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(sortedClassList(formGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + async.done(); + }); + })); }); it('should not update the view when the value initially came from the view', diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index 61be2b837e..c52686b8d6 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -10,7 +10,7 @@ import {NgFor, NgIf} from '@angular/common'; import {Component} from '@angular/core'; import {ComponentFixture, TestBed, TestComponentBuilder, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {AsyncTestCompleter, afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; -import {FormsModule, NgForm} from '@angular/forms'; +import {FormsModule, NgForm, NgModelGroup} 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/browser_util'; @@ -366,6 +366,57 @@ export function main() { }); })); + it('should set status classes with ngModelGroup and ngForm', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const t = `
+
+ +
+
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.name = ''; + fixture.detectChanges(); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + const modelGroup = + fixture.debugElement.query(By.directive(NgModelGroup)).nativeElement; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + // ngModelGroup creates its control asynchronously + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(sortedClassList(modelGroup)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-untouched' + ]); + + expect(sortedClassList(form)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-untouched' + ]); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(sortedClassList(modelGroup)).toEqual([ + 'ng-invalid', 'ng-pristine', 'ng-touched' + ]); + expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); + + input.value = 'updatedValue'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(sortedClassList(modelGroup)).toEqual([ + 'ng-dirty', 'ng-touched', 'ng-valid' + ]); + expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + }); + async.done(); + }); + })); + it('should mark controls as dirty before emitting a value change event', fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index b590463e43..4a5e983898 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -349,16 +349,15 @@ export declare abstract class NgControl extends AbstractControlDirective { } /** @experimental */ -export declare class NgControlStatus { - ngClassDirty: boolean; - ngClassInvalid: boolean; - ngClassPristine: boolean; - ngClassTouched: boolean; - ngClassUntouched: boolean; - ngClassValid: boolean; +export declare class NgControlStatus extends AbstractControlStatus { constructor(cd: NgControl); } +/** @experimental */ +export declare class NgControlStatusGroup extends AbstractControlStatus { + constructor(cd: ControlContainer); +} + /** @experimental */ export declare class NgForm extends ControlContainer implements Form { control: FormGroup;