angulartypescriptmultidimensional-arrayangular-reactive-formsformarray

Accessing Angular Typed Form Property from a FormArray in the HTML Template


I’m working on a form that manages currencies, with fields for code and label. I’m using the latest Angular features and trying to leverage typed forms.

I have a FormArray inside a FormGroup, which is populated with some pre-filled currency data. The reactive form works as expected—I can add and remove currencies with buttons.

Currency form working fine

However, I’m facing an issue when trying to access the form control properties in my HTML template to apply validation classes (e.g., showing a valid or invalid state for the input fields). I receive the following error:

NG9: Property 'code' does not exist on type 'AbstractControl<any, any>'. [plugin angular-compiler]

src/app/countries.component.html:34:58:
  34 │ [class.is-valid]="currency.code.valid"

Problem:

Here’s the code that triggers the error:

[class.is-valid]="currency.code.valid"

I’ve also tried changing it to:

[class.is-valid]="currency.controls['code'].valid"

Or:

[class.is-valid]="currency.get('code').valid"

But I still get the same error.

My TypeScript Code:

import { CommonModule } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import {
    FormArray,
    FormBuilder,
    FormGroup,
    ReactiveFormsModule,
    Validators
} from '@angular/forms';

export interface Item {
    code: string;
    label: string;
}

@Component({
    selector: 'app-countries',
    standalone: true,
    imports: [ReactiveFormsModule, CommonModule],
    templateUrl: './countries.component.html',
    styleUrl: './countries.component.scss'
})
export class CountriesComponent {
    currencyForm: FormGroup;
    private formBuilder = inject(FormBuilder);

    constructor() {
        this.currencyForm = this.formBuilder.group({
            currencies: this.formBuilder.array<FormGroup>(
                [
                    {code: 'CAD', label: 'Canadian Dollar'},
                    {code: 'USD', label: 'United States Dollar'},
                    {code: 'EUR', label: 'Euro'}
                ].map(currency => this.createCurrencyFormGroup(currency))
            )
        });
    }

    createCurrencyFormGroup(currency: Item) {
        return this.formBuilder.group({
            code: [currency.code, [Validators.required, Validators.maxLength(3), Validators.minLength(3)]],
            label: [currency.label, Validators.required]
        });
    }

    get currencies(): FormArray {
        return this.currencyForm.get('currencies') as FormArray;
    }

    addCurrency() {
        const currencyControl = this.createCurrencyFormGroup({code: '', label: ''});
        this.currencies.push(currencyControl);
    }

    removeCurrency(index: number) {
        this.currencies.removeAt(index);
    }
}

My HTML Code:

<div class="container mt-3">
    <form [formGroup]="currencyForm" class="row g-3">
        <div class="col-md-5" formArrayName="currencies">
            <button type="button" class="btn btn-sm btn-secondary" (click)="addCurrency()">Add Currency</button>

            <div *ngFor="let currency of currencies.controls; let idx = index" [formGroupName]="idx">
                <div class="row">
                    <div class="col-4">
                        <input type="text"
                               class="form-control"
                               placeholder="Currency Code"
                               formControlName="code"
                               maxlength="3"
                               [class.is-valid]="currency.get('code').valid"
                               [class.is-invalid]="currency.get('code').invalid" />
                    </div>
                    <div class="col-6">
                        <input type="text"
                               class="form-control"
                               placeholder="Currency Label"
                               formControlName="label"
                               [class.is-valid]="currency.get('label').valid"
                               [class.is-invalid]="currency.get('label').invalid" />
                    </div>
                    <div class="col-2">
                        <button type="button" class="btn btn-danger" (click)="removeCurrency(idx)">Remove</button>
                    </div>
                </div>
            </div>
        </div>
    </form>
</div>

What I've Tried:

Question:

How can I properly access the form control properties in my FormArray to apply validation classes in the template? What am I doing wrong with my approach to accessing the code and label properties?

Thank you so much


Solution

  • TRY

    Use (see that it's not only a FormArray else a FormArray of FormGroup

    get currencies(): FormArray {
       return this.currencyForm.get('currencies') as FormArray<FormGroup>;
    }
    

    And use (see the '?')

    [class.is-valid]="currency.get('code')?.valid"
    [class.is-invalid]="currency.get('code')?.invalid"
    

    If not work, Always can use the "dot" notation to reach the control

    [class.is-valid]="currencyForm.get('currency.'+idx+'.code)?.valid"
    [class.is-invalid]="currencyForm.get('currency.'+idx+'.code)?.invalid"
    

    NOTE: You needn't use always a FormArray inside a FormGroup

    You can use, e.g.

      currencies = this.formBuilder.array<FormGroup>(
        [
          { code: 'CAD', label: 'Canadian Dollar' },
          { code: 'USD', label: 'United States Dollar' },
          { code: 'EUR', label: 'Euro' },
        ].map((currency) => this.createCurrencyFormGroup(currency))
      );
    

    And

    <div *ngFor="let currency of currencies.controls; let idx = index" 
           [formGroup]="currency">
          <input ..  formControlName="code"/>
          <input ..  formControlName="label"/>
    </div>