I am working on creating reusable form input components using Angular's reactive forms.
For my FormArray input component, I am facing challenges.
I found I needed to use double-type casting:
get arrayGroup(): FormGroup {
return this.formArray as AbstractControl as FormGroup;
}
Because unless I wrapped the input HTML in something like this:
<div [formGroup]="arrayGroup"></div>
I got this error:
NG01053: formGroupName must be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it to an existing FormGroup instance (you can create one in your class).
My solution works and does the job, but there must be a better way.
I have shared two components, a form, and an input.
** The components **
// FILENAME: forms.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormArray,
FormBuilder,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { ArrayInputComponent } from './array-input/array-input.component';
@Component({
selector: 'app-forms',
standalone: true,
imports: [ArrayInputComponent, CommonModule, ReactiveFormsModule],
template: `
<div>
<h1>Forms</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<app-array-input [formArray]="formArray"></app-array-input>
<button>Submit</button>
</form>
</div>
`,
})
export class FormsComponent implements OnInit {
form: FormGroup = this.fb.group({});
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form.addControl('formArray', this.fb.array([]));
}
get formArray() {
return this.form.get('formArray') as FormArray;
}
onSubmit() {
console.log('Form valid: ', this.form.valid);
console.log('Form values: ', this.form.value);
}
}
// FILENAME: ./array-input/array-input.component
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import {
AbstractControl,
FormArray,
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
@Component({
selector: 'app-array-input',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<div [formGroup]="arrayGroup">
<h1>Array Input</h1>
<button type="button" (click)="add()">Add</button>
<ng-container *ngFor="let item of this.formArray.controls; let i = index">
<div [formGroupName]="i">
<input full placeholder="name" formControlName="name" />
<input placeholder="relation" formControlName="relation" />
</div>
</ng-container>
</div>
`,
})
export class ArrayInputComponent {
@Input() formArray!: FormArray;
constructor(private fb: FormBuilder) {}
add() {
const item = this.fb.group({
name: ['', [Validators.required]],
relation: [],
});
this.formArray.push(item);
}
get arrayGroup(): FormGroup {
return this.formArray as AbstractControl as FormGroup;
}
}
Angular version 17
The ControlContainer
DI can be used to fetch the parent FormGroup
using
this.formGroup = this.controlContainer!.control as FormGroup;
Then we can use this formGroup
to get the formArray
.
get formArray(): FormArray {
return this.formGroup.get('formArray') as FormArray;
}
After this, we configure the formArrayName
and formGroup
which have their own separate DIV.
<div [formGroup]="formGroup">
<div formArrayName="formArray">
<h1>Array Input</h1>
<button type="button" (click)="add()">Add</button>
<ng-container *ngFor="let item of this.formArray.controls; let i = index">
<div [formGroupName]="i">
<input full placeholder="name" formControlName="name" />
<input placeholder="relation" formControlName="relation" />
</div>
</ng-container>
</div>
</div>
After this your component, should work fine.
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import {
AbstractControl,
ControlContainer,
FormArray,
FormBuilder,
FormGroup,
FormGroupName,
NgForm,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
@Component({
selector: 'app-array-input',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<div [formGroup]="formGroup">
<div formArrayName="formArray">
<h1>Array Input</h1>
<button type="button" (click)="add()">Add</button>
<ng-container *ngFor="let item of this.formArray.controls; let i = index">
<div [formGroupName]="i">
<input full placeholder="name" formControlName="name" />
<input placeholder="relation" formControlName="relation" />
</div>
</ng-container>
</div>
</div>
`,
})
export class ArrayInputComponent {
formGroup: FormGroup = new FormGroup({});
constructor(
private fb: FormBuilder,
private controlContainer: ControlContainer
) {}
ngOnInit() {
console.log(this.controlContainer.control);
this.formGroup = this.controlContainer!.control as FormGroup;
}
add() {
const item = this.fb.group({
name: ['', [Validators.required]],
relation: [],
});
this.formArray.push(item);
}
get formArray(): FormArray {
return this.formGroup.get('formArray') as FormArray;
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [ArrayInputComponent, CommonModule, ReactiveFormsModule],
template: `
<div>
<h1>Forms</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<app-array-input></app-array-input>
<button>Submit</button>
</form>
</div>
`,
})
export class App {
form: FormGroup = this.fb.group({});
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form.addControl('formArray', this.fb.array([]));
}
get formArray() {
return this.form.get('formArray') as FormArray;
}
onSubmit() {
console.log('Form valid: ', this.form.valid);
console.log('Form values: ', this.form.value);
}
}
bootstrapApplication(App);
If you want something even more reusable, you can give the array name as an @Input
working example below