feat(forms): add control status classes to form groups (#10667)

This commit is contained in:
Kara 2016-08-11 09:01:09 -07:00 committed by vikerman
parent 7fac4efede
commit 2291929a15
6 changed files with 159 additions and 38 deletions

View File

@ -10,7 +10,7 @@ import {NgModule, Type} from '@angular/core';
import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
import {DefaultValueAccessor} from './directives/default_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 {NgForm} from './directives/ng_form';
import {NgModel} from './directives/ng_model'; import {NgModel} from './directives/ng_model';
import {NgModelGroup} from './directives/ng_model_group'; 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 {ControlValueAccessor} from './directives/control_value_accessor';
export {DefaultValueAccessor} from './directives/default_value_accessor'; export {DefaultValueAccessor} from './directives/default_value_accessor';
export {NgControl} from './directives/ng_control'; 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 {NgForm} from './directives/ng_form';
export {NgModel} from './directives/ng_model'; export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group'; export {NgModelGroup} from './directives/ng_model_group';
@ -45,8 +45,8 @@ export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValida
export const SHARED_FORM_DIRECTIVES: Type<any>[] = [ export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor, NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor,
CheckboxControlValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor,
RadioControlValueAccessor, NgControlStatus, RequiredValidator, MinLengthValidator, RadioControlValueAccessor, NgControlStatus, NgControlStatusGroup, RequiredValidator,
MaxLengthValidator, PatternValidator MinLengthValidator, MaxLengthValidator, PatternValidator
]; ];
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm]; export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];

View File

@ -10,30 +10,14 @@ import {Directive, Self} from '@angular/core';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {AbstractControlDirective} from './abstract_control_directive';
import {ControlContainer} from './control_container';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
export class AbstractControlStatus {
private _cd: AbstractControlDirective;
/** constructor(cd: AbstractControlDirective) { this._cd = cd; }
* 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; }
get ngClassUntouched(): boolean { get ngClassUntouched(): boolean {
return isPresent(this._cd.control) ? this._cd.control.untouched : false; 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; return isPresent(this._cd.control) ? this._cd.control.valid : false;
} }
get ngClassInvalid(): boolean { 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); }
}

View File

@ -30,7 +30,7 @@ export {ControlValueAccessor, NG_VALUE_ACCESSOR} from './directives/control_valu
export {DefaultValueAccessor} from './directives/default_value_accessor'; export {DefaultValueAccessor} from './directives/default_value_accessor';
export {Form} from './directives/form_interface'; export {Form} from './directives/form_interface';
export {NgControl} from './directives/ng_control'; 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 {NgForm} from './directives/ng_form';
export {NgModel} from './directives/ng_model'; export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group'; export {NgModelGroup} from './directives/ng_model_group';

View File

@ -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( inject(
[TestComponentBuilder, AsyncTestCompleter], [TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: 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 = const t =
`<form [formGroup]="form"><input type="text" formControlName="name"></form>`; `<form [formGroup]="form"><input type="text" formControlName="name"></form>`;
@ -1191,7 +1191,8 @@ export function main() {
fixture.debugElement.componentInstance.form = form; fixture.debugElement.componentInstance.form = form;
fixture.detectChanges(); fixture.detectChanges();
var input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual([
'ng-invalid', 'ng-pristine', 'ng-untouched' 'ng-invalid', 'ng-pristine', 'ng-untouched'
]); ]);
@ -1211,6 +1212,57 @@ export function main() {
async.done(); 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 = `<form [formGroup]="form">
<div formGroupName="person">
<input type="text" formControlName="name">
</div>
</form>`;
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', it('should not update the view when the value initially came from the view',

View File

@ -10,7 +10,7 @@ import {NgFor, NgIf} from '@angular/common';
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ComponentFixture, TestBed, TestComponentBuilder, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; 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 {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 {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util'; 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 = `<form>
<div ngModelGroup="person">
<input [(ngModel)]="name" required name="name">
</div>
</form>`;
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', it('should mark controls as dirty before emitting a value change event',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {

View File

@ -349,16 +349,15 @@ export declare abstract class NgControl extends AbstractControlDirective {
} }
/** @experimental */ /** @experimental */
export declare class NgControlStatus { export declare class NgControlStatus extends AbstractControlStatus {
ngClassDirty: boolean;
ngClassInvalid: boolean;
ngClassPristine: boolean;
ngClassTouched: boolean;
ngClassUntouched: boolean;
ngClassValid: boolean;
constructor(cd: NgControl); constructor(cd: NgControl);
} }
/** @experimental */
export declare class NgControlStatusGroup extends AbstractControlStatus {
constructor(cd: ControlContainer);
}
/** @experimental */ /** @experimental */
export declare class NgForm extends ControlContainer implements Form { export declare class NgForm extends ControlContainer implements Form {
control: FormGroup; control: FormGroup;