Hi,
In this post we'll learn how to enforce some kind of behavior on input by using Angular core.
Our goal:
Create an input which can get only numbers (or empty value).
Create an input which can get only numbers (or empty value).
If any character other than a number is typed - input should not be updated!
Here is how angular performs when using an input:
1 + 2 - Anonymous function inside registerOnChange (1) sent to fn which is set to onChange (2).
3 + 4 - input event handler is defined to handleInput which just executes onChange.
Directive:
It's possible to solve the issue by writing a behavior directive.
This directive gets angular defaultValueAccessor from injector and override onChange so it won't trigger fn immediately - only after it validates input value.
Step-by-step explanation of app.ts :
@Directive({ selector: '[ngModel][numbersOnlyInput]'})
In order to use this directive - an html DOM element should have ngModel and numbersOnlyInput attributes.
constructor(private valueAccessor: DefaultValueAccessor, private model: NgModel)
Constructor gets 2 references from Angular's injector:
DefaultValueAccessor - which is actually Angular default wrapper that write and listen to changes of some form elements (https://angular.io/api/forms/DefaultValueAccessor).
NgModel - will be used in to extract and override the value in case it isn't valid.
valueAccessor.registerOnChange = (fn: (val: any) => void) => { valueAccessor.onChange = (val) => { }
Overriding default onChange functionality.
Please note that actual change implementation should be written inside onChange method. Inside registerOnChange one should define the onChange method.
let regexp = new RegExp('\\d+(?:[\.]\\d{0,})?$'); let isNumber : boolean = regexp.test(val); let isEmpty : boolean = val == '';
This is onChange implementation. val is the value that was now written to the input.
val is tested against a regex that enforces only numbers and isNumber gets the boolean result.
Input can also be empty - so val is also tested against an empty value and isEmpty gets the boolean result.
if(!isNumber && !isEmpty) { model.control.setValue(model.value); return; } return fn(val);
First, remember that onChange signature is (_: any) => {} which means that it's a function that gets a value of any type and returns a function.
- If val is not number and is not empty - model.value has the input value before something changed because input model hasn't updated yet.
model.control.setValue(model.value); sets previous input text into the input element.
After setting the old value - change cycle is stopped so no function is returned to onChange.
- If val is a number or empty - change cycle continues regularly by return default fn with val.
Note: If this part isn't that clear - take a look again at the flow diagram above.
<input type="text" [(ngModel)]="val" numbersOnlyInput>
As mentioned, by writing ngModel and numbersOnlyInput - NumbersOnlyDirective is activated on input.
Note: If you're using directives in your app - it's recommended to define each one on a separate file.
Custom Input Component:
Suppose you don't need that kind of functionality across all application and you need to write a custom input which behaves differently than Angular's input.
For instance: PrimeNG autocomplete component.
Step-by-step explanation of custom-input.ts:
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true };
NG_VALUE_ACCESSOR is an angular token which tells angular dependency injection (DI) core that this class implements ControlValueAccessor. Now Angular DI system can grab an instance of it. Using this token allows to use this input in a dynamic form as angular would recognize it as a form control (In this awesome custom form component blogpost refer to the paragraph that starts with "The beauty of this is that we never have to tell NgForm what our inputs are").
@Component({ selector: 'custom-input', template: `<div class="form-group"> <label><ng-content></ng-content> <input (input)="handleInput($event.target)" (blur)="onBlur()" > </label> </div>`, providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] })
In contrast to the directive solution - now all events must be handled by custom-input implementation.
//From ControlValueAccessor interface registerOnChange(fn: any) { this.onChangeCallback = fn; }
onChange gets fn as is. fn is the same anonymous function from the diagram above. Overriding that function won't help like it did before. This is a new input (with a new ControlValueAccessor) which doesn't trigger onChange. The component's developer should state when or where to execute it.
handleInput(target: any): void { const val = target.value; let regexp = new RegExp('\\d+(?:[\.]\\d{0,})?$'); let isNumber = regexp.test(val); let isEmpty = val == ''; if(!isNumber && !isEmpty) { let model = this._inj.get(NgControl); // get NgModel model.control.setValue(model.value); target.value = model.value; return; } this.onChangeCallback(val); }
In this case, handleInput which has "numbers-only" input functionality decides if to trigger onChange
or not (continue change cycle or stop).