1

I have the following component:

.ts:

import {Component, Inject} from '@angular/core';
import {FormBuilder, Validators} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {IValueAndCurrency} from '@model/IValueAndCurrency';

@Component({
  selector: 'app-top-up-amount-change-dialog',
  templateUrl: './top-up-amount-change-dialog.component.html',
  styleUrls: ['./top-up-amount-change-dialog.component.scss'],
})
export class TopUpAmountChangeDialogComponent {
  topupAmountFormGroup = this.fb.group({
    topupAmount: [this.data.topupAmount, {
      validators: [
        Validators.required,
        Validators.min(0),
      ]
    }],
  })

  constructor(
    private dialogRef: MatDialogRef<TopUpAmountChangeDialogComponent>,
    private fb: FormBuilder,
    @Inject(MAT_DIALOG_DATA)
    public data: {
      topupAmount: IValueAndCurrency;
      possibleAmounts: IValueAndCurrency[];
    },
  ) {
  }

}

Unit test:

import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {TopUpAmountChangeDialogComponent} from './top-up-amount-change-dialog.component';
import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {MATERIAL_MODULES} from 'src/app/strong textapp.consts';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {APP_CONFIG} from "../../../../../../../appConfig.injectortoken";
import {TestAppConfigJson} from "../../../../../../app.component.spec";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {IValueAndCurrency} from "../../../../../../common/models/IValueAndCurrency";
import {Currency} from "../../../../../../common/models/enums/currency.enum";

fdescribe('TopUpAmountChangeDialogComponent', () => {
  let component: TopUpAmountChangeDialogComponent;
  let fixture: ComponentFixture<TopUpAmountChangeDialogComponent>;
  const matRefSpy = jasmine.createSpyObj('matRefSpy', ['close']);
  const dialogData: { topupAmount: IValueAndCurrency, possibleAmounts: IValueAndCurrency[] } = {
    topupAmount: {
      value: 10,
      currencyCode: Currency.EUR
    },
    possibleAmounts: [
      {
        value: 0,
        currencyCode: Currency.EUR
      },
      {
        value: 0,
        currencyCode: Currency.USD
      }
    ]
  };

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        BrowserAnimationsModule,
        MATERIAL_MODULES,
        ReactiveFormsModule,
        HttpClientModule,
      ],
      declarations: [TopUpAmountChangeDialogComponent],
      providers: [
        FormBuilder,
        {provide: APP_CONFIG, useValue: TestAppConfigJson},
        {provide: MatDialogRef, useValue: matRefSpy},
        {provide: MAT_DIALOG_DATA, useValue: dialogData},
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TopUpAmountChangeDialogComponent);
    component = fixture.componentInstance;
    component.data = dialogData;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

If I try to run it, I get the following error:

hrome 110.0.0.0 (Linux x86_64) TopUpAmountChangeDialogComponent should create FAILED
        TypeError: Cannot read properties of undefined (reading 'group')
            at new TopUpAmountChangeDialogComponent (src/app/modules/.../top-up-amount-change-dialog/top-up-amount-change-dialog.component.ts:12:34)

If I move the formControl initialization to the constructor, it works properly. But I don't want to do that: that case I would have to declare my form as an untyped FormGroup<any> and therefore I would loose the type safety this provides (more about this here, "Avoid This Common Typed Forms Pitfall".

Edit: A correction: I could keep the type information with moving the initialization to the constructor or to ngOnInit, however, for that I would need to explicitly describe the interface which I feel like is a code duplication: the formBuilder could get the type implicitly. So you either do the solution I posted below, or follow this recommendation and add the type information manually.

2
  • 1
    Moving it in the constructor does not mean you would have to change the typings of your form. The "clean" way would be to move the initialization into the constructor, or, even better, to ngOnInit. Commented Feb 17, 2023 at 7:16
  • @PhilippMeissner I see your point, but moving it to NgOnInit or to the Constructor would need me giving explicit typings for the declaration, which I would like to avoid (a simple topupAmountFormGroup: FormGroup would result in an any type, so I would have to add a new Interface instead, with a lot of boilerplate code) - my solution below allows you to declare and implicitly give the type information at the same time Commented Feb 17, 2023 at 7:20

2 Answers 2

1

You can use the Angular inject function:

private fb = inject(FormBuilder);
topupAmountFormGroup = this.fb.group...
Sign up to request clarification or add additional context in comments.

Comments

0

The solution is to not to use the injected FormBuilder instance, but in fact, create a new one:

export class TopUpAmountChangeDialogComponent {
  topupAmountFormGroup = new FormBuilder().group({
    topupAmount: [this.data.topupAmount, {
      validators: [
        Validators.required,
        Validators.min(0),
      ]
    }],
  })
...

It is only implicitly described in the documentation and I thought that it should be more clear that in this case we should do this.

1 Comment

When using FormBuilder direclty in the constructor itself it seems to work me. Does that make sense? constructor (private fb: FormBuilder){this.formGroup = this.fb.group({................

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.