diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index cda095a753..150c7b93ce 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/forms/src/directives.ts @@ -16,6 +16,7 @@ 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'; @@ -34,6 +35,7 @@ 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'; @@ -72,4 +74,6 @@ export const FORM_DIRECTIVES: Type[] = /*@ts2dart_const*/[ ]; export const REACTIVE_FORM_DIRECTIVES: Type[] = - /*@ts2dart_const*/[FormControlDirective, FormGroupDirective, FormControlName, FormGroupName]; \ No newline at end of file + /*@ts2dart_const*/[ + FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName + ]; \ No newline at end of file diff --git a/modules/@angular/forms/src/directives/ng_form.ts b/modules/@angular/forms/src/directives/ng_form.ts index afa025285b..201f1f5042 100644 --- a/modules/@angular/forms/src/directives/ng_form.ts +++ b/modules/@angular/forms/src/directives/ng_form.ts @@ -19,7 +19,7 @@ import {Form} from './form_interface'; import {NgControl} from './ng_control'; import {NgModel} from './ng_model'; import {NgModelGroup} from './ng_model_group'; -import {composeAsyncValidators, composeValidators, setUpControl, setUpFormGroup} from './shared'; +import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from './shared'; export const formDirectiveProvider: any = /*@ts2dart_const*/ {provide: ControlContainer, useExisting: forwardRef(() => NgForm)}; @@ -140,7 +140,7 @@ export class NgForm extends ControlContainer implements Form { PromiseWrapper.scheduleMicrotask(() => { var container = this._findContainer(dir.path); var group = new FormGroup({}); - setUpFormGroup(group, dir); + setUpFormContainer(group, dir); container.registerControl(dir.name, group); group.updateValueAndValidity({emitEvent: false}); }); 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 new file mode 100644 index 0000000000..48b57edf4b --- /dev/null +++ b/modules/@angular/forms/src/directives/reactive_directives/form_array_name.ts @@ -0,0 +1,96 @@ +/** + * @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 {FormArray} from '../../model'; +import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; +import {ControlContainer} from '../control_container'; +import {composeAsyncValidators, composeValidators, controlPath} from '../shared'; +import {AsyncValidatorFn, ValidatorFn} from '../validators'; + +import {FormGroupDirective} from './form_group_directive'; + +export const formArrayNameProvider: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + 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( + @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.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); } +} 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 a22284dcac..96e7f49a5c 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 @@ -12,13 +12,14 @@ import {EventEmitter, ObservableWrapper} from '../../facade/async'; import {ListWrapper, StringMapWrapper} from '../../facade/collection'; import {BaseException} from '../../facade/exceptions'; import {isBlank} from '../../facade/lang'; -import {FormControl, FormGroup} from '../../model'; +import {FormArray, FormControl, FormGroup} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators'; import {ControlContainer} from '../control_container'; import {Form} from '../form_interface'; import {NgControl} from '../ng_control'; -import {composeAsyncValidators, composeValidators, setUpControl, setUpFormGroup} from '../shared'; +import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared'; +import {FormArrayName} from './form_array_name'; import {FormGroupName} from './form_group_name'; export const formDirectiveProvider: any = @@ -155,16 +156,26 @@ export class FormGroupDirective extends ControlContainer implements Form, removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); } - addFormGroup(dir: FormGroupName) { + addFormGroup(dir: FormGroupName): void { var ctrl: any = this.form.find(dir.path); - setUpFormGroup(ctrl, dir); + setUpFormContainer(ctrl, dir); ctrl.updateValueAndValidity({emitEvent: false}); } - removeFormGroup(dir: FormGroupName) {} + removeFormGroup(dir: FormGroupName): void {} getFormGroup(dir: FormGroupName): FormGroup { return this.form.find(dir.path); } + addFormArray(dir: FormArrayName): void { + var ctrl: any = this.form.find(dir.path); + setUpFormContainer(ctrl, dir); + ctrl.updateValueAndValidity({emitEvent: false}); + } + + removeFormArray(dir: FormArrayName): void {} + + getFormArray(dir: FormArrayName): FormArray { return this.form.find(dir.path); } + updateModel(dir: NgControl, value: any): void { var ctrl  = this.form.find(dir.path); ctrl.updateValue(value); diff --git a/modules/@angular/forms/src/directives/shared.ts b/modules/@angular/forms/src/directives/shared.ts index 417dfe7a32..8bfd7b3f99 100644 --- a/modules/@angular/forms/src/directives/shared.ts +++ b/modules/@angular/forms/src/directives/shared.ts @@ -9,7 +9,7 @@ import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {BaseException} from '../facade/exceptions'; import {hasConstructor, isBlank, isPresent, looseIdentical} from '../facade/lang'; -import {FormControl, FormGroup} from '../model'; +import {FormArray, FormControl, FormGroup} from '../model'; import {Validators} from '../validators'; import {AbstractControlDirective} from './abstract_control_directive'; @@ -22,6 +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 {SelectControlValueAccessor} from './select_control_value_accessor'; import {AsyncValidatorFn, ValidatorFn} from './validators'; @@ -54,7 +55,8 @@ export function setUpControl(control: FormControl, dir: NgControl): void { dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); } -export function setUpFormGroup(control: FormGroup, dir: AbstractFormGroupDirective) { +export function setUpFormContainer( + control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) { if (isBlank(control)) _throwError(dir, 'Cannot find control'); control.validator = Validators.compose([control.validator, dir.validator]); control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]); diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index 26c48d14a0..c377402742 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/forms/src/forms.ts @@ -33,6 +33,7 @@ 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'; diff --git a/modules/@angular/forms/test/directives_spec.ts b/modules/@angular/forms/test/directives_spec.ts index 42883f26e2..849dd199aa 100644 --- a/modules/@angular/forms/test/directives_spec.ts +++ b/modules/@angular/forms/test/directives_spec.ts @@ -12,7 +12,7 @@ import {fakeAsync, flushMicrotasks, tick,} from '@angular/core/testing'; import {SpyNgControl, SpyValueAccessor} from './spies'; -import {FormGroup, FormControl, FormControlName, FormGroupName, NgModelGroup, FormGroupDirective, ControlValueAccessor, Validators, NgForm, NgModel, FormControlDirective, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/forms'; +import {FormGroup, FormControl, FormArray, FormArrayName, FormControlName, FormGroupName, NgModelGroup, FormGroupDirective, ControlValueAccessor, Validators, NgForm, NgModel, FormControlDirective, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/forms'; import {selectValueAccessor, composeValidators} from '@angular/forms/src/directives/shared'; import {TimerWrapper} from '../src/facade/async'; @@ -376,6 +376,30 @@ export function main() { }); }); + describe('FormArrayName', () => { + var formModel: FormArray; + var formArrayDir: FormArrayName; + + beforeEach(() => { + const parent = new FormGroupDirective([], []); + formModel = new FormArray([new FormControl('')]); + parent.form = new FormGroup({'array': formModel}); + formArrayDir = new FormArrayName(parent, [], []); + formArrayDir.name = 'array'; + }); + + it('should reexport control properties', () => { + expect(formArrayDir.control).toBe(formModel); + expect(formArrayDir.value).toBe(formModel.value); + expect(formArrayDir.valid).toBe(formModel.valid); + expect(formArrayDir.errors).toBe(formModel.errors); + expect(formArrayDir.pristine).toBe(formModel.pristine); + expect(formArrayDir.dirty).toBe(formModel.dirty); + expect(formArrayDir.touched).toBe(formModel.touched); + expect(formArrayDir.untouched).toBe(formModel.untouched); + }); + }); + describe('FormControlDirective', () => { var controlDir: any /** TODO #9100 */; var control: any /** TODO #9100 */; diff --git a/modules/@angular/forms/test/integration_spec.ts b/modules/@angular/forms/test/integration_spec.ts index b05e244800..864386c696 100644 --- a/modules/@angular/forms/test/integration_spec.ts +++ b/modules/@angular/forms/test/integration_spec.ts @@ -14,7 +14,7 @@ import {ComponentFixture} from '@angular/core/testing'; import {fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; -import {ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, FormControl, FormGroup, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, REACTIVE_FORM_DIRECTIVES, Validator, Validators, disableDeprecatedForms, provideForms} from '@angular/forms'; +import {ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, FormArray, FormControl, FormGroup, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, REACTIVE_FORM_DIRECTIVES, Validator, Validators, disableDeprecatedForms, provideForms} 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'; @@ -68,7 +68,7 @@ export function main() { }); })); - it('should update the control group values on DOM change', + it('should update the form group values on DOM change', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { @@ -221,7 +221,7 @@ export function main() { }); })); - it('should update DOM elements when rebinding the control group', + it('should update DOM elements when rebinding the form group', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { @@ -304,6 +304,77 @@ 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}); + + const t = `
+
+
+ +
+
+
`; + + tcb.overrideTemplate(MyComp8, t) + .overrideProviders(MyComp8, providerArr) + .createAsync(MyComp8) + .then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + tick(); + + 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'); + + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.form.value).toEqual({cities: ['LA', 'NY']}); + + }); + }))); + + it('should support pushing new controls to form arrays', + fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]); + const form = new FormGroup({cities: cityArray}); + + const t = `
+
+
+ +
+
+
`; + + tcb.overrideTemplate(MyComp8, t) + .overrideProviders(MyComp8, providerArr) + .createAsync(MyComp8) + .then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + tick(); + + cityArray.push(new FormControl('LA')); + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[2].nativeElement.value).toEqual('LA'); + expect(fixture.componentInstance.form.value).toEqual({cities: ['SF', 'NY', 'LA']}); + + }); + }))); + describe('different control types', () => { it('should support ', inject( diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 55d1e7bca5..663b5f6344 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -118,6 +118,18 @@ export declare class FormArray extends AbstractControl { removeAt(index: number): void; } +export declare class FormArrayName extends ControlContainer implements OnInit, OnDestroy { + asyncValidator: AsyncValidatorFn; + control: FormArray; + formDirective: FormGroupDirective; + name: string; + path: string[]; + validator: ValidatorFn; + constructor(parent: ControlContainer, validators: any[], asyncValidators: any[]); + ngOnDestroy(): void; + ngOnInit(): void; +} + export declare class FormBuilder { array(controlsConfig: any[], validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn): FormArray; control(value: Object, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormControl; @@ -194,12 +206,15 @@ export declare class FormGroupDirective extends ControlContainer implements Form submitted: boolean; constructor(_validators: any[], _asyncValidators: any[]); addControl(dir: NgControl): void; + addFormArray(dir: FormArrayName): void; addFormGroup(dir: FormGroupName): void; getControl(dir: NgControl): FormControl; + getFormArray(dir: FormArrayName): FormArray; getFormGroup(dir: FormGroupName): FormGroup; ngOnChanges(changes: SimpleChanges): void; onSubmit(): boolean; removeControl(dir: NgControl): void; + removeFormArray(dir: FormArrayName): void; removeFormGroup(dir: FormGroupName): void; updateModel(dir: NgControl, value: any): void; }