Skip to content

Commit 0dd95c5

Browse files
JeanMecheAndrewKushnir
authored andcommitted
feat(forms): Add FormArrayDirective (#55880)
The `FormArrayDirective` will allow to have a `FormArray` as a top-level form object. * `NgControlStatusGroup` directive will be applied to the `FormArrayDirective` * `NgForm` will still create a `FormGroup` Fixes #30264 BREAKING CHANGE: This new directive will conflict with existing FormArray directives or formArray inputs on the same element. PR Close #55880
1 parent 318718c commit 0dd95c5

File tree

13 files changed

+245
-20
lines changed

13 files changed

+245
-20
lines changed

goldens/public-api/forms/index.api.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@ export abstract class AbstractFormDirective extends ControlContainer implements
167167
removeControl(dir: FormControlName): void;
168168
removeFormArray(dir: FormArrayName): void;
169169
removeFormGroup(dir: FormGroupName): void;
170-
resetForm(value?: any): void;
170+
resetForm(value?: any, options?: {
171+
onlySelf?: boolean;
172+
emitEvent?: boolean;
173+
}): void;
171174
get submitted(): boolean;
172175
updateModel(dir: FormControlName, value: any): void;
173176
// (undocumented)
@@ -315,6 +318,17 @@ export class FormArray<TControl extends AbstractControl<any> = any> extends Abst
315318
}): void;
316319
}
317320

321+
// @public
322+
export class FormArrayDirective extends AbstractFormDirective {
323+
get control(): FormArray;
324+
form: FormArray;
325+
ngSubmit: EventEmitter<any>;
326+
// (undocumented)
327+
static ɵdir: i0.ɵɵDirectiveDeclaration<FormArrayDirective, "[formArray]", ["ngForm"], { "form": { "alias": "formArray"; "required": false; }; }, { "ngSubmit": "ngSubmit"; }, never, never, false, never>;
328+
// (undocumented)
329+
static ɵfac: i0.ɵɵFactoryDeclaration<FormArrayDirective, never>;
330+
}
331+
318332
// @public
319333
export class FormArrayName extends ControlContainer implements OnInit, OnDestroy {
320334
constructor(parent: ControlContainer, validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[]);
@@ -678,7 +692,7 @@ export class NgControlStatus extends AbstractControlStatus {
678692
export class NgControlStatusGroup extends AbstractControlStatus {
679693
constructor(cd: ControlContainer);
680694
// (undocumented)
681-
static ɵdir: i0.ɵɵDirectiveDeclaration<NgControlStatusGroup, "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]", never, {}, {}, never, never, false, never>;
695+
static ɵdir: i0.ɵɵDirectiveDeclaration<NgControlStatusGroup, "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]", never, {}, {}, never, never, false, never>;
682696
// (undocumented)
683697
static ɵfac: i0.ɵɵFactoryDeclaration<NgControlStatusGroup, [{ optional: true; self: true; }]>;
684698
}
@@ -713,7 +727,7 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit {
713727
get submitted(): boolean;
714728
updateModel(dir: NgControl, value: any): void;
715729
// (undocumented)
716-
static ɵdir: i0.ɵɵDirectiveDeclaration<NgForm, "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", ["ngForm"], { "options": { "alias": "ngFormOptions"; "required": false; }; }, { "ngSubmit": "ngSubmit"; }, never, never, false, never>;
730+
static ɵdir: i0.ɵɵDirectiveDeclaration<NgForm, "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", ["ngForm"], { "options": { "alias": "ngFormOptions"; "required": false; }; }, { "ngSubmit": "ngSubmit"; }, never, never, false, never>;
717731
// (undocumented)
718732
static ɵfac: i0.ɵɵFactoryDeclaration<NgForm, [{ optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }]>;
719733
}
@@ -850,7 +864,7 @@ export class ReactiveFormsModule {
850864
// (undocumented)
851865
static ɵinj: i0.ɵɵInjectorDeclaration<ReactiveFormsModule>;
852866
// (undocumented)
853-
static ɵmod: i0.ɵɵNgModuleDeclaration<ReactiveFormsModule, [typeof FormControlDirective, typeof FormGroupDirective, typeof FormControlName, typeof FormGroupName, typeof FormArrayName], never, [typeof ɵInternalFormsSharedModule, typeof FormControlDirective, typeof FormGroupDirective, typeof FormControlName, typeof FormGroupName, typeof FormArrayName]>;
867+
static ɵmod: i0.ɵɵNgModuleDeclaration<ReactiveFormsModule, [typeof FormControlDirective, typeof FormGroupDirective, typeof FormArrayDirective, typeof FormControlName, typeof FormGroupName, typeof FormArrayName], never, [typeof ɵInternalFormsSharedModule, typeof FormControlDirective, typeof FormGroupDirective, typeof FormArrayDirective, typeof FormControlName, typeof FormGroupName, typeof FormArrayName]>;
854868
}
855869

856870
// @public

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@
381381
"createTextNode",
382382
"createViewBlueprint",
383383
"cyclicDependencyError",
384-
"declareDirectiveHostTemplate",
385384
"declareNoDirectiveHostTemplate",
386385
"decreaseElementDepthCount",
387386
"deepForEach",
@@ -734,7 +733,6 @@
734733
"stringifyForError",
735734
"syncViewWithBlueprint",
736735
"templateCreate",
737-
"throwCyclicDependencyError",
738736
"throwError",
739737
"throwInvalidWriteToSignalErrorFn",
740738
"throwProviderNotFoundError",

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,6 @@
10051005
"syncPendingControls",
10061006
"syncViewWithBlueprint",
10071007
"templateCreate",
1008-
"throwCyclicDependencyError",
10091008
"throwError",
10101009
"throwInvalidWriteToSignalError",
10111010
"throwInvalidWriteToSignalErrorFn",

packages/forms/src/directives.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {NgNoValidate} from './directives/ng_no_validate_directive';
1818
import {NumberValueAccessor} from './directives/number_value_accessor';
1919
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
2020
import {RangeValueAccessor} from './directives/range_value_accessor';
21+
import {FormArrayDirective} from './directives/reactive_directives/form_array_directive';
2122
import {FormControlDirective} from './directives/reactive_directives/form_control_directive';
2223
import {FormControlName} from './directives/reactive_directives/form_control_name';
2324
import {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
@@ -52,6 +53,7 @@ export {NgModelGroup} from './directives/ng_model_group';
5253
export {NumberValueAccessor} from './directives/number_value_accessor';
5354
export {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
5455
export {RangeValueAccessor} from './directives/range_value_accessor';
56+
export {FormArrayDirective} from './directives/reactive_directives/form_array_directive';
5557
export {
5658
FormControlDirective,
5759
NG_MODEL_WITH_FORM_CONTROL_WARNING,
@@ -97,6 +99,7 @@ export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, N
9799
export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] = [
98100
FormControlDirective,
99101
FormGroupDirective,
102+
FormArrayDirective,
100103
FormControlName,
101104
FormGroupName,
102105
FormArrayName,

packages/forms/src/directives/ng_control_status.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class NgControlStatus extends AbstractControlStatus {
134134
*/
135135
@Directive({
136136
selector:
137-
'[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]',
137+
'[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]',
138138
host: ngGroupStatusHost,
139139
standalone: false,
140140
})

packages/forms/src/directives/ng_form.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const resolvedPromise = (() => Promise.resolve())();
123123
* @publicApi
124124
*/
125125
@Directive({
126-
selector: 'form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]',
126+
selector: 'form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]',
127127
providers: [formDirectiveProvider],
128128
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
129129
outputs: ['ngSubmit'],
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, EventEmitter, forwardRef, Input, Output, Provider} from '@angular/core';
10+
11+
import {FormArray} from '../../model/form_array';
12+
import {ControlContainer} from '../control_container';
13+
14+
import {AbstractFormDirective} from './abstract_form.directive';
15+
16+
const formDirectiveProvider: Provider = {
17+
provide: ControlContainer,
18+
useExisting: forwardRef(() => FormArrayDirective),
19+
};
20+
21+
/**
22+
* @description
23+
*
24+
* Binds an existing `FormArray` to a DOM element.
25+
*
26+
* This directive accepts an existing `FormArray` instance. It will then use this
27+
* `FormArray` instance to match any child `FormControl`, `FormGroup`/`FormRecord`,
28+
* and `FormArray` instances to child `FormControlName`, `FormGroupName`,
29+
* and `FormArrayName` directives.
30+
*
31+
* @see [Reactive Forms Guide](guide/reactive-forms)
32+
* @see {@link AbstractControl}
33+
*
34+
* @usageNotes
35+
* ### Register Form Array
36+
*
37+
* The following example registers a `FormArray` with first name and last name controls,
38+
* and listens for the *ngSubmit* event when the button is clicked.
39+
*
40+
* @ngModule ReactiveFormsModule
41+
* @publicApi
42+
*/
43+
@Directive({
44+
selector: '[formArray]',
45+
providers: [formDirectiveProvider],
46+
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
47+
exportAs: 'ngForm',
48+
standalone: false,
49+
})
50+
export class FormArrayDirective extends AbstractFormDirective {
51+
/**
52+
* @description
53+
* Tracks the `FormArray` bound to this directive.
54+
*/
55+
@Input('formArray') override form: FormArray = null!;
56+
57+
/**
58+
* @description
59+
* Emits an event when the form submission has been triggered.
60+
*/
61+
@Output() override ngSubmit = new EventEmitter();
62+
63+
/**
64+
* @description
65+
* Returns the `FormArray` bound to this directive.
66+
*/
67+
override get control(): FormArray {
68+
return this.form;
69+
}
70+
}

packages/forms/src/directives/reactive_directives/form_control_name.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../valid
4040

4141
import {NG_MODEL_WITH_FORM_CONTROL_WARNING} from './form_control_directive';
4242
import {FormArrayName, FormGroupName} from './form_group_name';
43-
import { AbstractFormDirective } from './abstract_form.directive';
43+
import {AbstractFormDirective} from './abstract_form.directive';
4444

4545
const controlNameBinding: Provider = {
4646
provide: NgControl,

packages/forms/src/directives/reactive_directives/form_group_directive.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {FormGroup} from '../../model/form_group';
1212
import {ControlContainer} from '../control_container';
1313

1414
import {AbstractFormDirective} from './abstract_form.directive';
15-
import {AbstractControl} from '../../forms';
1615

1716
const formDirectiveProvider: Provider = {
1817
provide: ControlContainer,

packages/forms/src/directives/reactive_directives/form_group_name.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ export const formArrayNameProvider: any = {
131131
*
132132
* Syncs a nested `FormArray` to a DOM element.
133133
*
134-
* This directive is designed to be used with a parent `FormGroupDirective` (selector:
135-
* `[formGroup]`).
134+
* This directive is designed to be used with a parent `FormGroupDirective`/`FormGroupArray` (selector:
135+
* `[formGroup]`/`[formArray]`).
136136
*
137137
* It accepts the string name of the nested `FormArray` you want to link, and
138138
* will look for a `FormArray` registered with that name in the parent
139-
* `FormGroup` instance you passed into `FormGroupDirective`.
139+
* `FormGroup`/`FormArray` instance you passed into `FormGroupDirective`/`FormGroupArray`.
140140
*
141141
* @see [Reactive Forms Guide](guide/forms/reactive-forms)
142142
* @see {@link AbstractControl}

0 commit comments

Comments
 (0)