fix(forms): fully support rebinding form group directive (#11051)
This commit is contained in:
parent
d7c82f5c0f
commit
515ff61fcb
|
@ -17,7 +17,7 @@ import {ControlContainer} from '../control_container';
|
||||||
import {Form} from '../form_interface';
|
import {Form} from '../form_interface';
|
||||||
import {NgControl} from '../ng_control';
|
import {NgControl} from '../ng_control';
|
||||||
import {ReactiveErrors} from '../reactive_errors';
|
import {ReactiveErrors} from '../reactive_errors';
|
||||||
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
|
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
|
||||||
|
|
||||||
import {FormArrayName, FormGroupName} from './form_group_name';
|
import {FormArrayName, FormGroupName} from './form_group_name';
|
||||||
|
|
||||||
|
@ -124,11 +124,9 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
||||||
|
|
||||||
var async = composeAsyncValidators(this._asyncValidators);
|
var async = composeAsyncValidators(this._asyncValidators);
|
||||||
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
|
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
|
||||||
|
|
||||||
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||||
|
this._updateDomValue(changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateDomValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get submitted(): boolean { return this._submitted; }
|
get submitted(): boolean { return this._submitted; }
|
||||||
|
@ -189,10 +187,15 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_updateDomValue() {
|
_updateDomValue(changes: SimpleChanges) {
|
||||||
|
const oldForm = changes['form'].previousValue;
|
||||||
this.directives.forEach(dir => {
|
this.directives.forEach(dir => {
|
||||||
var ctrl: any = this.form.get(dir.path);
|
const newCtrl: any = this.form.get(dir.path);
|
||||||
dir.valueAccessor.writeValue(ctrl.value);
|
const oldCtrl = oldForm.get(dir.path);
|
||||||
|
if (oldCtrl !== newCtrl) {
|
||||||
|
cleanUpControl(oldCtrl, dir);
|
||||||
|
if (newCtrl) setUpControl(newCtrl, dir);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,12 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
|
||||||
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
|
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cleanUpControl(control: FormControl, dir: NgControl) {
|
||||||
|
dir.valueAccessor.registerOnChange(() => _noControlError(dir));
|
||||||
|
dir.valueAccessor.registerOnTouched(() => _noControlError(dir));
|
||||||
|
if (control) control._clearChangeFns();
|
||||||
|
}
|
||||||
|
|
||||||
export function setUpFormContainer(
|
export function setUpFormContainer(
|
||||||
control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) {
|
control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) {
|
||||||
if (isBlank(control)) _throwError(dir, 'Cannot find control with');
|
if (isBlank(control)) _throwError(dir, 'Cannot find control with');
|
||||||
|
@ -74,6 +80,10 @@ export function setUpFormContainer(
|
||||||
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
|
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _noControlError(dir: NgControl) {
|
||||||
|
return _throwError(dir, 'There is no FormControl instance attached to form control element with');
|
||||||
|
}
|
||||||
|
|
||||||
function _throwError(dir: AbstractControlDirective, message: string): void {
|
function _throwError(dir: AbstractControlDirective, message: string): void {
|
||||||
let messageEnd: string;
|
let messageEnd: string;
|
||||||
if (dir.path.length > 1) {
|
if (dir.path.length > 1) {
|
||||||
|
|
|
@ -519,6 +519,14 @@ export class FormControl extends AbstractControl {
|
||||||
*/
|
*/
|
||||||
registerOnChange(fn: Function): void { this._onChange.push(fn); }
|
registerOnChange(fn: Function): void { this._onChange.push(fn); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_clearChangeFns(): void {
|
||||||
|
this._onChange = [];
|
||||||
|
this._onDisabledChange = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a listener for disabled events.
|
* Register a listener for disabled events.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,7 +27,8 @@ export function main() {
|
||||||
FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup,
|
FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup,
|
||||||
FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue,
|
FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue,
|
||||||
WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel,
|
WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel,
|
||||||
LoginIsEmptyValidator, LoginIsEmptyWrapper, UniqLoginValidator, UniqLoginWrapper
|
LoginIsEmptyValidator, LoginIsEmptyWrapper, UniqLoginValidator, UniqLoginWrapper,
|
||||||
|
NestedFormGroupComp
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
TestBed.compileComponents();
|
TestBed.compileComponents();
|
||||||
|
@ -74,7 +75,11 @@ export function main() {
|
||||||
expect(form.value).toEqual({'login': 'updatedValue'});
|
expect(form.value).toEqual({'login': 'updatedValue'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update DOM elements when rebinding the form group', () => {
|
});
|
||||||
|
|
||||||
|
describe('rebound form groups', () => {
|
||||||
|
|
||||||
|
it('should update DOM elements initially', () => {
|
||||||
const fixture = TestBed.createComponent(FormGroupComp);
|
const fixture = TestBed.createComponent(FormGroupComp);
|
||||||
fixture.debugElement.componentInstance.form =
|
fixture.debugElement.componentInstance.form =
|
||||||
new FormGroup({'login': new FormControl('oldValue')});
|
new FormGroup({'login': new FormControl('oldValue')});
|
||||||
|
@ -87,6 +92,148 @@ export function main() {
|
||||||
const input = fixture.debugElement.query(By.css('input'));
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
expect(input.nativeElement.value).toEqual('newValue');
|
expect(input.nativeElement.value).toEqual('newValue');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update model when UI changes', () => {
|
||||||
|
const fixture = TestBed.createComponent(FormGroupComp);
|
||||||
|
fixture.debugElement.componentInstance.form =
|
||||||
|
new FormGroup({'login': new FormControl('oldValue')});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const newForm = new FormGroup({'login': new FormControl('newValue')});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.nativeElement.value = 'Nancy';
|
||||||
|
dispatchEvent(input.nativeElement, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(newForm.value).toEqual({login: 'Nancy'});
|
||||||
|
|
||||||
|
newForm.setValue({login: 'Carson'});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(input.nativeElement.value).toEqual('Carson');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with radio buttons when reusing control', () => {
|
||||||
|
const fixture = TestBed.createComponent(FormControlRadioButtons);
|
||||||
|
const food = new FormControl('chicken');
|
||||||
|
fixture.debugElement.componentInstance.form =
|
||||||
|
new FormGroup({'food': food, 'drink': new FormControl('')});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const newForm = new FormGroup({'food': food, 'drink': new FormControl('')});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
newForm.setValue({food: 'fish', drink: ''});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
expect(inputs[0].nativeElement.checked).toBe(false);
|
||||||
|
expect(inputs[1].nativeElement.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update nested form group model when UI changes', () => {
|
||||||
|
const fixture = TestBed.createComponent(NestedFormGroupComp);
|
||||||
|
fixture.debugElement.componentInstance.form = new FormGroup(
|
||||||
|
{'signin': new FormGroup({'login': new FormControl(), 'password': new FormControl()})});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const newForm = new FormGroup({
|
||||||
|
'signin': new FormGroup(
|
||||||
|
{'login': new FormControl('Nancy'), 'password': new FormControl('secret')})
|
||||||
|
});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
expect(inputs[0].nativeElement.value).toEqual('Nancy');
|
||||||
|
expect(inputs[1].nativeElement.value).toEqual('secret');
|
||||||
|
|
||||||
|
inputs[0].nativeElement.value = 'Carson';
|
||||||
|
dispatchEvent(inputs[0].nativeElement, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(newForm.value).toEqual({signin: {login: 'Carson', password: 'secret'}});
|
||||||
|
|
||||||
|
newForm.setValue({signin: {login: 'Bess', password: 'otherpass'}});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(inputs[0].nativeElement.value).toEqual('Bess');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pick up dir validators from nested form groups', () => {
|
||||||
|
const fixture = TestBed.createComponent(NestedFormGroupComp);
|
||||||
|
const form = new FormGroup({
|
||||||
|
'signin':
|
||||||
|
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
|
||||||
|
});
|
||||||
|
fixture.debugElement.componentInstance.form = form;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(form.get('signin').valid).toBe(false);
|
||||||
|
|
||||||
|
const newForm = new FormGroup({
|
||||||
|
'signin':
|
||||||
|
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
|
||||||
|
});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(form.get('signin').valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip named controls that are not found', () => {
|
||||||
|
const fixture = TestBed.createComponent(NestedFormGroupComp);
|
||||||
|
const form = new FormGroup({
|
||||||
|
'signin':
|
||||||
|
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
|
||||||
|
});
|
||||||
|
fixture.debugElement.componentInstance.form = form;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
form.addControl('email', new FormControl('email'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let emailInput = fixture.debugElement.query(By.css('[formControlName="email"]'));
|
||||||
|
expect(emailInput.nativeElement.value).toEqual('email');
|
||||||
|
|
||||||
|
const newForm = new FormGroup({
|
||||||
|
'signin':
|
||||||
|
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
|
||||||
|
});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
emailInput = fixture.debugElement.query(By.css('[formControlName="email"]'));
|
||||||
|
expect(emailInput).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip array controls that are not found', () => {
|
||||||
|
const fixture = TestBed.createComponent(FormArrayComp);
|
||||||
|
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
|
||||||
|
const form = new FormGroup({cities: cityArray});
|
||||||
|
fixture.debugElement.componentInstance.form = form;
|
||||||
|
fixture.debugElement.componentInstance.cityArray = cityArray;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
expect(inputs[2]).not.toBeDefined();
|
||||||
|
cityArray.push(new FormControl('LA'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
expect(inputs[2]).toBeDefined();
|
||||||
|
|
||||||
|
const newArr = new FormArray([new FormControl('SF'), new FormControl('NY')]);
|
||||||
|
const newForm = new FormGroup({cities: newArr});
|
||||||
|
fixture.debugElement.componentInstance.form = newForm;
|
||||||
|
fixture.debugElement.componentInstance.cityArray = newArr;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
expect(inputs[2]).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('form arrays', () => {
|
describe('form arrays', () => {
|
||||||
|
@ -1075,7 +1222,7 @@ export function main() {
|
||||||
TestBed.overrideComponent(FormGroupComp, {
|
TestBed.overrideComponent(FormGroupComp, {
|
||||||
set: {
|
set: {
|
||||||
template: `
|
template: `
|
||||||
<form [formGroup]="form">
|
<form [formGroup]="form">hav
|
||||||
<input type="radio" formControlName="food" name="drink" value="chicken">
|
<input type="radio" formControlName="food" name="drink" value="chicken">
|
||||||
</form>
|
</form>
|
||||||
`
|
`
|
||||||
|
@ -1191,6 +1338,22 @@ class FormGroupComp {
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'nested-form-group-comp',
|
||||||
|
template: `
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div formGroupName="signin" login-is-empty-validator>
|
||||||
|
<input formControlName="login">
|
||||||
|
<input formControlName="password">
|
||||||
|
</div>
|
||||||
|
<input *ngIf="form.contains('email')" formControlName="email">
|
||||||
|
</form>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class NestedFormGroupComp {
|
||||||
|
form: FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'form-control-number-input',
|
selector: 'form-control-number-input',
|
||||||
template: `
|
template: `
|
||||||
|
|
Loading…
Reference in New Issue