I've successfully migrated my Angular 18 project to Angular 19 and replaced all @Input decorators with the new input() signals. The code works but the test for a directive with input() signals breaks.
Here is my directive, it just multiplies values of a form group.
import { Directive, OnInit, input } from "@angular/core";
import { FormGroup, FormGroupDirective } from "@angular/forms";
import { FormItem } from "../model/form.model";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
@UntilDestroy()
@Directive({
selector: "[appMultiply]",
standalone: true,
})
export class MultiplyDirective implements OnInit {
constructor(formGroupDirective: FormGroupDirective) {
this.form = formGroupDirective.form;
}
readonly appMultiply = input<FormItem>();
readonly formDefinition = input<Array<FormItem>>();
private form: FormGroup;
ngOnInit(): void {
const appMultiply = this.appMultiply();
if (!appMultiply?.multiply || !this.formDefinition()) {
return;
}
const sourceControl = this.form.get(appMultiply.formControlName);
if (!sourceControl) {
return;
}
//initial calculation
this.calculateTargetControlValues(appMultiply, sourceControl.value);
//wait for source changes
sourceControl.valueChanges
.pipe(untilDestroyed(this))
.subscribe((value) =>
this.calculateTargetControlValues(this.appMultiply()!, value),
);
}
private calculateTargetControlValues = (
appMultiply: FormItem,
sourceValue: number,
) => {
const multiplyByControl = this.form.get(
appMultiply.multiply!.multiplyByControlName,
);
if (!multiplyByControl) {
return;
}
this.formDefinition()!
.filter((formItem) =>
this.appMultiply()!.multiply!.targetFormControlNames.includes(
formItem.formControlName,
),
)
.forEach((formItem) => {
const targetControl = this.form.get(formItem.formControlName);
if (!targetControl) {
return;
}
const result = (sourceValue ?? 0) * (multiplyByControl.value ?? 0);
targetControl.patchValue(result, { emitEvent: false });
});
};
}
This is the basic test of the directive:
import { FormGroupDirective } from "@angular/forms";
import { MultiplyDirective } from "./multiply.directive";
describe("MultiplyDirective", () => {
it("should create an instance", () => {
const directive = new MultiplyDirective(new FormGroupDirective([], []));
expect(directive).toBeTruthy();
});
});
And I get this error from Karma : "Error: NG0203: inputFunction() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext
. Find more at https://angular.dev/errors/NG0203"
If I replace the 2 input signals with @Input decorator inputs, the test runs successfully. Like this :
@Input() appMultiply!: FormItem
@Input() formDefinition!: Array<FormItem>
What do i do wrong?
You can use the runInInjectionContext
context method to make the directive initialize. We have to provide the first argument as the EnvironmentInjector
to the method.
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(AppComponent);
injector = TestBed.inject(EnvironmentInjector);
// injector = TestBed.inject(Injector);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
const directive = runInInjectionContext(injector, () => {
return new MultiplyDirective(new FormGroupDirective([], []));
});
expect(directive).toBeTruthy();
});
import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
import { AppComponent, MultiplyDirective } from './app.component';
import { AppModule } from './app.module';
import { FormGroupDirective } from '@angular/forms';
import {
runInInjectionContext,
Injector,
EnvironmentInjector,
} from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let injector: EnvironmentInjector;
// let injector: Injector;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppModule],
}).compileComponents();
});
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(AppComponent);
injector = TestBed.inject(EnvironmentInjector);
// injector = TestBed.inject(Injector);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
const directive = runInInjectionContext(injector, () => {
return new MultiplyDirective(new FormGroupDirective([], []));
});
expect(directive).toBeTruthy();
});
});