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 {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];
|
||||||
|
|
|
@ -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); }
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue