feat(forms): add control status classes to form groups (#10667)
This commit is contained in:
parent
7fac4efede
commit
2291929a15
|
@ -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<any>[] = [
|
||||
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<any>[] = [NgModel, NgModelGroup, NgForm];
|
||||
|
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 =
|
||||
`<form [formGroup]="form"><input type="text" formControlName="name"></form>`;
|
||||
|
@ -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 = `<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',
|
||||
|
|
|
@ -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 = `<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',
|
||||
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue