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.
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.