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 {NgControl} from '../ng_control';
|
||||
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';
|
||||
|
||||
|
@ -124,11 +124,9 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
|
||||
var async = composeAsyncValidators(this._asyncValidators);
|
||||
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
|
||||
|
||||
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
this._updateDomValue(changes);
|
||||
}
|
||||
|
||||
this._updateDomValue();
|
||||
}
|
||||
|
||||
get submitted(): boolean { return this._submitted; }
|
||||
|
@ -189,10 +187,15 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
_updateDomValue() {
|
||||
_updateDomValue(changes: SimpleChanges) {
|
||||
const oldForm = changes['form'].previousValue;
|
||||
this.directives.forEach(dir => {
|
||||
var ctrl: any = this.form.get(dir.path);
|
||||
dir.valueAccessor.writeValue(ctrl.value);
|
||||
const newCtrl: any = this.form.get(dir.path);
|
||||
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());
|
||||
}
|
||||
|
||||
export function cleanUpControl(control: FormControl, dir: NgControl) {
|
||||
dir.valueAccessor.registerOnChange(() => _noControlError(dir));
|
||||
dir.valueAccessor.registerOnTouched(() => _noControlError(dir));
|
||||
if (control) control._clearChangeFns();
|
||||
}
|
||||
|
||||
export function setUpFormContainer(
|
||||
control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) {
|
||||
if (isBlank(control)) _throwError(dir, 'Cannot find control with');
|
||||
|
@ -74,6 +80,10 @@ export function setUpFormContainer(
|
|||
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 {
|
||||
let messageEnd: string;
|
||||
if (dir.path.length > 1) {
|
||||
|
|
|
@ -519,6 +519,14 @@ export class FormControl extends AbstractControl {
|
|||
*/
|
||||
registerOnChange(fn: Function): void { this._onChange.push(fn); }
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_clearChangeFns(): void {
|
||||
this._onChange = [];
|
||||
this._onDisabledChange = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener for disabled events.
|
||||
*/
|
||||
|
|
|
@ -27,7 +27,8 @@ export function main() {
|
|||
FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup,
|
||||
FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue,
|
||||
WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel,
|
||||
LoginIsEmptyValidator, LoginIsEmptyWrapper, UniqLoginValidator, UniqLoginWrapper
|
||||
LoginIsEmptyValidator, LoginIsEmptyWrapper, UniqLoginValidator, UniqLoginWrapper,
|
||||
NestedFormGroupComp
|
||||
]
|
||||
});
|
||||
TestBed.compileComponents();
|
||||
|
@ -74,7 +75,11 @@ export function main() {
|
|||
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);
|
||||
fixture.debugElement.componentInstance.form =
|
||||
new FormGroup({'login': new FormControl('oldValue')});
|
||||
|
@ -87,6 +92,148 @@ export function main() {
|
|||
const input = fixture.debugElement.query(By.css('input'));
|
||||
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', () => {
|
||||
|
@ -1075,7 +1222,7 @@ export function main() {
|
|||
TestBed.overrideComponent(FormGroupComp, {
|
||||
set: {
|
||||
template: `
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form">hav
|
||||
<input type="radio" formControlName="food" name="drink" value="chicken">
|
||||
</form>
|
||||
`
|
||||
|
@ -1191,6 +1338,22 @@ class FormGroupComp {
|
|||
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({
|
||||
selector: 'form-control-number-input',
|
||||
template: `
|
||||
|
|
Loading…
Reference in New Issue