I am creating a form using Angular Reactive forms, and for UX reasons I am trying to disable the form's submit when certain invalid values are inserted in an <input type="number">
field, for example negative numbers or non-integer numbers.
The problem is that when I insert a value that even if allowed by <input type="number">
would usually evaluate to NaN
, like, for example, the letter e
alone, the minus or plus sign without a number following it, or even a number too big to be represented by a TypeScript number
, instead evaluates to null
when accessing the value via control.value
or control.getRawValue()
.
EDIT: The form also needs to be submittable when the input field is empty. This is sadly a non-negotiable requirement, and I also cannot use a default value like 0
for the field.
This is a minimum reproducible example for what I tried (it only tries to block NaN values).
app.component.ts
import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, ValidatorFn } from '@angular/forms';
@Component({
selector: 'app-root',
template: `
<form [formGroup]="formGroup" (ngSubmit)="print()">
<input type="number" name="control" id="control" formControlName="control" >
<button type="submit" [disabled] ="formGroup.invalid">print</button>
</form>
`
})
export class AppComponent {
title = 'ReactiveFormNumberControl';
formGroup
constructor(private _fb: FormBuilder) {
this.formGroup = this._fb.group({
control: this._fb.control<number | null>(null, {
validators: [noNaNValidator]
})
})
}
print = () => {
console.log(this.formGroup.value)
}
}
const noNaNValidator: ValidatorFn = (
control: AbstractControl<number | null>,
) => {
const value = control.value
if (Number.isNaN(value)) {
// Value is null and not NaN, so this is never reached because Number.isNaN(null) is false,
// the control (and group) is marked valid and the submit button is not disabled
return {
invalidValue: {
value: value
},
}
}
return null
}
While looking at Angular issues related to this on GitHub, more precisely Issue #2962, I found a workaround for this, leveraging HTML5's badInput
property present present in some types of <input>
elements, number
included.
I've wrote a custom validator directive, similar to the one shown in this comment on Angular Issue #2962, but unlike that one the directive I've wrote has to be explicitly applied, and for now can only be applied to <input type="number">
fields (but this should be easy to change if needed, by changing the directive's selector
).
bad-input-validator.directive.ts
import { Directive, ElementRef } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
@Directive({
selector: 'input[type=number][appBadInputValidator]',
standalone: true,
providers: [{
provide: NG_VALIDATORS,
useExisting: BadInputValidatorDirective,
multi: true,
}]
})
export class BadInputValidatorDirective implements Validator {
private onChangeCallback?: () => void;
constructor(private _element: ElementRef) { }
validate(_: AbstractControl): ValidationErrors | null {
const inputElement = this._element.nativeElement
return inputElement.validity.badInput ? { 'badInput': true } : null
}
registerOnValidatorChange?(fn: () => void): void {
this.onChangeCallback = fn
}
}
This can probably be improved somehow, but for now it does what I need to. When I apply the appBadInputValidator
to an <input type="number">
element, and I input a value that would parse to NaN
, the FormControl
and FormGroup
get marked as invalid and the submit button is correctly disabled.
This directive works similar Angular.JS's form validation, that kept track of badInput
, but this behavior is currently not present in Angular, at least as of version 16.2.6.
Big thanks to amc1804 on GitHub for posting a solution.