I have a common input component with the constructor
constructor(private fg: FormGroupDirective, @Self() public ngControl: NgControl) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
This is used within my angular component:
<!-- Email -->
<app-input
ngDefaultControl
formControlName="email"
[additionalClasses]="['mb-32']"
[idPrefix]="'email'"
[label]="'Email address'"
[maxLength]="100"
[size]="fullWidthInput">
</app-input>
The custom input component inherits from a BaseInput Component
@Component({ template: `` })
export abstract class BaseInputComponent implements ControlValueAccessor, OnDestroy {
@Input() additionalClasses: string[] = [];
@Input() additionalInputClasses: string[] = [];
@Input() additionalLabelClasses: string[] = [];
@Input() idPrefix: string;
@Input() label: string;
@Input() required = true;
@Input() size: InputSize = InputSize.Small;
protected ngUnsubscribe = new Subject<void>();
constructor(private fg: FormGroupDirective, @Self() protected ngControl: NgControl) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
get errorId(): string {
return `${this.idPrefix}-error`;
}
get errorMessage(): string {
return InputErrors.getErrorMessageForControl(this.ngControl, this.label);
}
get formControl(): AbstractControl | null {
return this.ngControl?.control;
}
get inputId(): string {
return `${this.idPrefix}-input`;
}
get labelClasses(): string {
return this.additionalLabelClasses.join(' ');
}
get isInvalid(): boolean {
return CustomValidation.isFormControlInvalid(this.ngControl, this.fg);
}
get labelId(): string {
return `${this.idPrefix}-label`;
}
get parentForm(): FormGroup {
return this.fg.form;
}
get value(): any {
return this.ngControl.value;
}
set value(v: any) {
this.propagateChange(v);
this.propagateTouched();
}
get wrapperClasses(): string[] {
const classes = ['input', this.size.toString(), ...this.additionalClasses];
if (this.isInvalid) {
classes.push('ft-invalid');
}
return classes;
}
// propagate changes to form control
propagateChange = (_: string) => {};
// propagate touched changes to form control
propagateTouched = () => {};
// From ControlValueAccessor interface
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
// From ControlValueAccessor interface
registerOnTouched(fn: any): void {
this.propagateTouched = fn;
}
// From ControlValueAccessor interface
writeValue(value: string): void {
if (value && value !== this.value) {
this.value = value;
}
}
ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
And the custom input component:
<mat-form-field [ngClass]="inputClasses" [floatLabel]="'never'" *ngIf="!readonly; else ReadonlyInput">
<mat-placeholder *ngIf="placeholder">{{ placeholder }}</mat-placeholder>
<input
matInput
[id]="inputId"
[formControl]="formControl"
[maxLength]="maxLength"
[type]="type"
[value]="value"
(input)="propagateChange($event.target.value)"
(keyup.enter)="onEnter()" />
<div *ngIf="isInvalid" [id]="errorId">
<mat-error>{{ errorMessage }}</mat-error>
</div>
</mat-form-field>
get value()
is what is causing the errors (I think), as the injected ngControl
is undefined.
I have a set of Karma/Jasmine tests. When running with the command ng test
locally, the browser opens, all tests pass and there are no errors or warnings in the console.
When running the command ran in DevOps, ng test --codeCoverage=true --watch=false --browsers ChromeHeadless
, (both in DevOps and locally), the below error is displayed:
TypeError: Cannot read property 'value' of undefined
at <Jasmine>
at InputComponent.get value [as value] (http://localhost:9877/_karma_webpack_/src/app/shared/components/form/base-input.component.ts:72:4)
at InputComponent_mat_form_field_2_Template (ng:///InputComponent.js:83:62)
at executeTemplate (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:7447:1)
at refreshView (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:7316:1)
at refreshEmbeddedViews (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:8408:1)
at refreshView (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:7340:1)
at refreshComponent (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:8454:1)
at refreshChildComponents (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:7109:1)
at refreshView (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:7366:1)
at refreshComponent (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:8454:1)
TypeError: Cannot read property 'getCheckedValue' of undefined
at <Jasmine>
at http://localhost:9877/_karma_webpack_/src/app/users/components/new-user-dialog/new-user-dialog.component.spec.ts:101:55
at <Jasmine>
at http://localhost:9877/_karma_webpack_/main.js:364670:71
at new ZoneAwarePromise (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:960:1)
at ./src/app/users/components/new-user-dialog/new-user-dialog.component.spec.ts.__awaiter (http://localhost:9877/_karma_webpack_/main.js:364666:12)
at UserContext.<anonymous> (http://localhost:9877/_karma_webpack_/src/app/users/components/new-user-dialog/new-user-dialog.component.spec.ts:100:47)
at ZoneDelegate.invoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)
at ZoneDelegate.invoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:363:1)
Can anyone help me with a solution? The fact this works with the browser is what's throwing me - all dependency injection etc seems to be configured correctly.
For anyone seeing this, seeing the same issue, the problem was with a constructor in a base component.
In the base component:
constructor(private fg: FormGroupDirective, @Self() protected ngControl: NgControl) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
And in the input component, no constructor at all. For the headless tests, this seems to be required.
constructor(fg: FormGroupDirective, @Self() ngControl: NgControl) {
super(fg, ngControl);
}