Search code examples
angularangular-forms

How do I restrict input in an Angular input box and keep the model correct?


I am having a hard time implementing the equivalent of what used to be parsers and formatters from AngularJS. My use case is not directly a parser/formatter, but here is what I am trying to accomplish.

I have a dynamic list of keys. For each of those keys, the user can enter a value 0-100. If the user inputs (keyboard or paste) any characters, they are stripped. If the user inputs any number greater than 100, it is capped to 100.

For that, I created a directive that filters out what I need. The directive works properly on the input element, but the underlying form is not getting the proper value as shown below.

bad form

The form seems to get my last input, no matter what.

How can I prevent the update of the model for invalid values?

I created a StackBlitz for this. The gist of the code is:

app.component.html

<form #form="ngForm">
  <div *ngFor="let key of list">
  <input name="{{key}}" ngModel appFilter >
  </div>
</form>

<pre>
form:
{{ form.value | json }}
</pre>

app.component.ts

export class AppComponent  {
  readonly list = ['foo', 'bar'];
}

filter.directive.ts

@Directive({
    selector: 'input[appFilter]',
})
export class FilterDirective {
    // TODO: remove directive?
    @Output()
    ngModelChange: EventEmitter<any> = new EventEmitter();

    value: string;

    @HostListener('input', ['$event'])
    onInputChange($event: TextInput) {
        const filtered = $event.target.value.replace(/[^0-9]/gi, '');
        if (filtered.match(/^[0-9]/)) {
            // starts with a number, must be a number
            const number = +filtered.replace(/\*/gi, '');
            $event.target.value = `${Math.min(100, number)}`;
        } else {
            // it must be empty string
            $event.target.value = '';
        }
        this.ngModelChange.emit($event.target.value);
    }
    constructor() {}
}

interface TextInput {
    target: {
        value: string;
    };
}

Solution

  • By using $event.target.value you adapt the input's value, but not the value of the underlying Angular NgControl. Luckily, NgControl can be injected into your directive whenever the input with the appFilter attribute also has an associated ngModel, formControl or formControlName attribute.

    Inject NgControl into your FilterDirective and use it to set the value:

    import { NgControl } from '@angular/forms';
    
    constructor(private ngControl: NgControl) { }
    
    @HostListener('input', ['$event'])
    onInputChange($event: TextInput) {
        const filtered = $event.target.value.replace(/[^0-9]/gi, '');
        if (filtered.match(/^[0-9]/)) {
            // starts with a number, must be a number
            const number = +filtered.replace(/\*/gi, '');
            this.ngControl.control.setValue(`${Math.min(100, number)}`);
        } else {
            // it must be empty string
            this.ngControl.control.setValue('');
        }
    }
    

    Should you expect cases where your FilterDirective is used without the ngModel, formControl or formControlName attribute; then make the injected NgControl @Optional().