From d6d45688303975b9f1f028d65923c74ecbbe1f91 Mon Sep 17 00:00:00 2001 From: Kara Date: Tue, 2 Aug 2016 09:40:42 -0700 Subject: [PATCH] fix(forms): allow arrays as parents (#10440) Closes #10432 --- modules/@angular/forms/src/directives.ts | 6 +- .../reactive_directives/form_array_name.ts | 107 ------------------ .../reactive_directives/form_control_name.ts | 5 +- .../form_group_directive.ts | 3 +- .../reactive_directives/form_group_name.ts | 98 +++++++++++++++- .../forms/src/directives/reactive_errors.ts | 3 +- .../@angular/forms/src/directives/shared.ts | 2 +- modules/@angular/forms/src/forms.ts | 2 +- .../forms/test/reactive_integration_spec.ts | 86 ++++++++++---- 9 files changed, 170 insertions(+), 142 deletions(-) delete mode 100644 modules/@angular/forms/src/directives/reactive_directives/form_array_name.ts diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index 1cb14c5567..278bd26d5c 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/forms/src/directives.ts @@ -16,11 +16,10 @@ import {NgModel} from './directives/ng_model'; import {NgModelGroup} from './directives/ng_model_group'; import {NumberValueAccessor} from './directives/number_value_accessor'; import {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; -import {FormArrayName} from './directives/reactive_directives/form_array_name'; import {FormControlDirective} from './directives/reactive_directives/form_control_directive'; import {FormControlName} from './directives/reactive_directives/form_control_name'; import {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; -import {FormGroupName} from './directives/reactive_directives/form_group_name'; +import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; import {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; @@ -35,11 +34,10 @@ export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; export {NumberValueAccessor} from './directives/number_value_accessor'; export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; -export {FormArrayName} from './directives/reactive_directives/form_array_name'; export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; export {FormControlName} from './directives/reactive_directives/form_control_name'; export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; -export {FormGroupName} from './directives/reactive_directives/form_group_name'; +export {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; export {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_array_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_array_name.ts deleted file mode 100644 index 436c65ddcf..0000000000 --- a/modules/@angular/forms/src/directives/reactive_directives/form_array_name.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; - -import {BaseException} from '../../facade/exceptions'; -import {FormArray} from '../../model'; -import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; -import {ControlContainer} from '../control_container'; -import {ReactiveErrors} from '../reactive_errors'; -import {composeAsyncValidators, composeValidators, controlPath} from '../shared'; -import {AsyncValidatorFn, ValidatorFn} from '../validators'; - -import {FormGroupDirective} from './form_group_directive'; -import {FormGroupName} from './form_group_name'; - -export const formArrayNameProvider: any = { - provide: ControlContainer, - useExisting: forwardRef(() => FormArrayName) -}; - -/** - * Syncs an existing form array to a DOM element. - * - * This directive can only be used as a child of {@link FormGroupDirective}. - * - * ```typescript - * @Component({ - * selector: 'my-app', - * template: ` - *
- *

Angular FormArray Example

- *
- *
- *
- * - *
- *
- *
- * {{ myForm.value | json }} // {cities: ['SF', 'NY']} - *
- * ` - * }) - * export class App { - * cityArray = new FormArray([ - * new FormControl('SF'), - * new FormControl('NY') - * ]); - * myForm = new FormGroup({ - * cities: this.cityArray - * }); - * } - * ``` - * - * @experimental - */ -@Directive({selector: '[formArrayName]', providers: [formArrayNameProvider]}) -export class FormArrayName extends ControlContainer implements OnInit, OnDestroy { - /** @internal */ - _parent: ControlContainer; - - /** @internal */ - _validators: any[]; - - /** @internal */ - _asyncValidators: any[]; - - @Input('formArrayName') name: string; - - constructor( - @Optional() @Host() @SkipSelf() parent: ControlContainer, - @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], - @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { - super(); - this._parent = parent; - this._validators = validators; - this._asyncValidators = asyncValidators; - } - - ngOnInit(): void { - this._checkParentType(); - this.formDirective.addFormArray(this); - } - - ngOnDestroy(): void { this.formDirective.removeFormArray(this); } - - get control(): FormArray { return this.formDirective.getFormArray(this); } - - get formDirective(): FormGroupDirective { return this._parent.formDirective; } - - get path(): string[] { return controlPath(this.name, this._parent); } - - get validator(): ValidatorFn { return composeValidators(this._validators); } - - get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } - - private _checkParentType(): void { - if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) { - ReactiveErrors.arrayParentException(); - } - } -} diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts index 0d4572a0f4..e1a3786c2b 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts @@ -9,7 +9,6 @@ import {Directive, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf, forwardRef} from '@angular/core'; import {EventEmitter, ObservableWrapper} from '../../facade/async'; -import {BaseException} from '../../facade/exceptions'; import {FormControl} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {AbstractFormGroupDirective} from '../abstract_form_group_directive'; @@ -20,10 +19,8 @@ import {ReactiveErrors} from '../reactive_errors'; import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared'; import {AsyncValidatorFn, ValidatorFn} from '../validators'; -import {FormArrayName} from './form_array_name'; import {FormGroupDirective} from './form_group_directive'; -import {FormGroupName} from './form_group_name'; - +import {FormArrayName, FormGroupName} from './form_group_name'; export const controlNameBinding: any = { provide: NgControl, diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts index 74a48688fd..26281c7f04 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts @@ -20,8 +20,7 @@ import {NgControl} from '../ng_control'; import {ReactiveErrors} from '../reactive_errors'; import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared'; -import {FormArrayName} from './form_array_name'; -import {FormGroupName} from './form_group_name'; +import {FormArrayName, FormGroupName} from './form_group_name'; export const formDirectiveProvider: any = { provide: ControlContainer, diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_group_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_group_name.ts index 91e6e94114..e979b7f734 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_group_name.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_group_name.ts @@ -8,11 +8,13 @@ import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; -import {BaseException} from '../../facade/exceptions'; +import {FormArray} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {AbstractFormGroupDirective} from '../abstract_form_group_directive'; import {ControlContainer} from '../control_container'; import {ReactiveErrors} from '../reactive_errors'; +import {composeAsyncValidators, composeValidators, controlPath} from '../shared'; +import {AsyncValidatorFn, ValidatorFn} from '../validators'; import {FormGroupDirective} from './form_group_directive'; @@ -85,8 +87,100 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit, /** @internal */ _checkParentType(): void { - if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) { + if (_hasInvalidParent(this._parent)) { ReactiveErrors.groupParentException(); } } } + +export const formArrayNameProvider: any = { + provide: ControlContainer, + useExisting: forwardRef(() => FormArrayName) +}; + +/** + * Syncs an existing form array to a DOM element. + * + * This directive can only be used as a child of {@link FormGroupDirective}. + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: ` + *
+ *

Angular FormArray Example

+ *
+ *
+ *
+ * + *
+ *
+ *
+ * {{ myForm.value | json }} // {cities: ['SF', 'NY']} + *
+ * ` + * }) + * export class App { + * cityArray = new FormArray([ + * new FormControl('SF'), + * new FormControl('NY') + * ]); + * myForm = new FormGroup({ + * cities: this.cityArray + * }); + * } + * ``` + * + * @experimental + */ +@Directive({selector: '[formArrayName]', providers: [formArrayNameProvider]}) +export class FormArrayName extends ControlContainer implements OnInit, OnDestroy { + /** @internal */ + _parent: ControlContainer; + + /** @internal */ + _validators: any[]; + + /** @internal */ + _asyncValidators: any[]; + + @Input('formArrayName') name: string; + + constructor( + @Optional() @Host() @SkipSelf() parent: ControlContainer, + @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { + super(); + this._parent = parent; + this._validators = validators; + this._asyncValidators = asyncValidators; + } + + ngOnInit(): void { + this._checkParentType(); + this.formDirective.addFormArray(this); + } + + ngOnDestroy(): void { this.formDirective.removeFormArray(this); } + + get control(): FormArray { return this.formDirective.getFormArray(this); } + + get formDirective(): FormGroupDirective { return this._parent.formDirective; } + + get path(): string[] { return controlPath(this.name, this._parent); } + + get validator(): ValidatorFn { return composeValidators(this._validators); } + + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } + + private _checkParentType(): void { + if (_hasInvalidParent(this._parent)) { + ReactiveErrors.arrayParentException(); + } + } +} + +function _hasInvalidParent(parent: ControlContainer): boolean { + return !(parent instanceof FormGroupName) && !(parent instanceof FormGroupDirective) && + !(parent instanceof FormArrayName); +} diff --git a/modules/@angular/forms/src/directives/reactive_errors.ts b/modules/@angular/forms/src/directives/reactive_errors.ts index 72cb455f79..58a0a06a0d 100644 --- a/modules/@angular/forms/src/directives/reactive_errors.ts +++ b/modules/@angular/forms/src/directives/reactive_errors.ts @@ -7,6 +7,7 @@ */ import {BaseException} from '../facade/exceptions'; + import {FormErrorExamples as Examples} from './error_examples'; export class ReactiveErrors { @@ -60,4 +61,4 @@ export class ReactiveErrors { ${Examples.formArrayName}`); } -} \ No newline at end of file +} diff --git a/modules/@angular/forms/src/directives/shared.ts b/modules/@angular/forms/src/directives/shared.ts index bb8b7aa1b0..c03b2e2a63 100644 --- a/modules/@angular/forms/src/directives/shared.ts +++ b/modules/@angular/forms/src/directives/shared.ts @@ -22,7 +22,7 @@ import {NgControl} from './ng_control'; import {normalizeAsyncValidator, normalizeValidator} from './normalize_validator'; import {NumberValueAccessor} from './number_value_accessor'; import {RadioControlValueAccessor} from './radio_control_value_accessor'; -import {FormArrayName} from './reactive_directives/form_array_name'; +import {FormArrayName} from './reactive_directives/form_group_name'; import {SelectControlValueAccessor} from './select_control_value_accessor'; import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor'; import {AsyncValidatorFn, ValidatorFn} from './validators'; diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index 1e170def6d..e0c9b1f5c9 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/forms/src/forms.ts @@ -33,10 +33,10 @@ export {NgControlStatus} from './directives/ng_control_status'; export {NgForm} from './directives/ng_form'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; -export {FormArrayName} from './directives/reactive_directives/form_array_name'; export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; export {FormControlName} from './directives/reactive_directives/form_control_name'; export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; +export {FormArrayName} from './directives/reactive_directives/form_group_name'; export {FormGroupName} from './directives/reactive_directives/form_group_name'; export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index 40697b73f2..9d7048b678 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -350,11 +350,13 @@ export function main() { })); it('should support form arrays', - fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]); - const form = new FormGroup({cities: cityArray}); + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]); + const form = new FormGroup({cities: cityArray}); - const t = `
+ const t = `
@@ -362,27 +364,71 @@ export function main() {
`; - tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { - fixture.debugElement.componentInstance.form = form; - fixture.debugElement.componentInstance.cityArray = cityArray; - fixture.detectChanges(); - tick(); + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.cityArray = cityArray; + fixture.detectChanges(); - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.value).toEqual('SF'); - expect(inputs[1].nativeElement.value).toEqual('NY'); - expect(fixture.componentInstance.form.value).toEqual({cities: ['SF', 'NY']}); + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.value).toEqual('SF'); + expect(inputs[1].nativeElement.value).toEqual('NY'); + expect(fixture.componentInstance.form.value).toEqual({cities: ['SF', 'NY']}); - inputs[0].nativeElement.value = 'LA'; - dispatchEvent(inputs[0].nativeElement, 'input'); + inputs[0].nativeElement.value = 'LA'; + dispatchEvent(inputs[0].nativeElement, 'input'); - fixture.detectChanges(); - tick(); + fixture.detectChanges(); - expect(fixture.componentInstance.form.value).toEqual({cities: ['LA', 'NY']}); + expect(fixture.componentInstance.form.value).toEqual({cities: ['LA', 'NY']}); + async.done(); + }); + })); - }); - }))); + it('should support form groups nested in form arrays', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const cityArray = new FormArray([ + new FormGroup({town: new FormControl('SF'), state: new FormControl('CA')}), + new FormGroup({town: new FormControl('NY'), state: new FormControl('NY')}) + ]); + const form = new FormGroup({cities: cityArray}); + + const t = `
+
+
+ + +
+
+
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.value).toEqual('SF'); + expect(inputs[1].nativeElement.value).toEqual('CA'); + expect(inputs[2].nativeElement.value).toEqual('NY'); + expect(inputs[3].nativeElement.value).toEqual('NY'); + expect(fixture.componentInstance.form.value).toEqual({ + cities: [{town: 'SF', state: 'CA'}, {town: 'NY', state: 'NY'}] + }); + + inputs[0].nativeElement.value = 'LA'; + dispatchEvent(inputs[0].nativeElement, 'input'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.form.value).toEqual({ + cities: [{town: 'LA', state: 'CA'}, {town: 'NY', state: 'NY'}] + }); + + async.done(); + }); + })); it('should support pushing new controls to form arrays', fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {