1

I have created a simple pipe which I apply to a text input. The regex works - but something is strange. The console log CORRECTLY shows the new value (non-alphanumeric removed), yet in the browser the input field will not update until AFTER i have type a good character. So typing '123!!!!A' will show exclamation points until A is typed then they dissappear. Why?

I use the pipe like this:

<input type="text" class="form-control" [ngModel]="name | inputFormat" (ngModelChange)="name=$event">

and the pipe is

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'inputFormat'
})
export class InputFormatPipe implements PipeTransform {

    transform(value: any): any {
    value = value.replace(/[^a-z0-9]/gi, ''); 
    console.log('new value: '+value);
    return value;
  }

}

I suspect change detection is not working - but not sure how to fix this.

TSG
  • 4,242
  • 9
  • 61
  • 121
  • What task are you trying to solve with this code? I'd say it should not work. If you want your value formatted in some particular way in the input then you'll probably need to implement ControlValueAccessor... but it's just my guess. Please provide details of what you want to achieve. – Alexander Leonov Sep 19 '17 at 02:48
  • I am writing a generic pipe that will remove restricted chars, change case, replace some chars with others, etc. To keep it simple I've reduced my problem to the simplest case possible to demonstrate the bug – TSG Sep 19 '17 at 02:55
  • That's not the way to go anyways. If you need to perform such things then ControlValueAccessor is your best friend. It will allow you to subscribe to different events on the input directly and process them **before** your value gets to the model. For example, you could avoid correcting your value by refusing unwanted characters in keydown event even before they are taken by the input. Just take a look at the DefaultValueAccessor in the angular sources and add some creative thinkng. It is not a rocket science, though it will require some code from you, but it will definitely be better approach. – Alexander Leonov Sep 19 '17 at 03:07
  • Is there an example you can point to? Earlier someone told me that PIPE was the right way to achieve this (I was using directive with onchange) at first. Aside from that, I'm curious why this doesn't work and how to fix it! – TSG Sep 19 '17 at 03:15
  • Pipes are only intended to format values in order to display them as static text, i.e. in span/div or something of that sort. Not for inputs. I can't tell you why it does not work and honestly I don't want to dig into this just because I don't see any value in the knowledge why something does not work when used improperly. :) As for example, I'll try to craft something for you, just give me few minutes. – Alexander Leonov Sep 19 '17 at 03:23
  • I have to wonder if using a input+pipe is wrong as SO is full of examples like this (https://stackoverflow.com/questions/40346676/angular-2-using-pipes-with-ngmodel) which makes me wonder why those work and mine dont – TSG Sep 19 '17 at 03:30

1 Answers1

0

Ok, here is an example of how you can process user input in the place where it ought to be processed by design.

test-input.directive.ts

import { Directive, ElementRef, Renderer2, forwardRef, HostListener } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const TEST_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => TestInputDirective),
  multi: true
};

@Directive({
  selector: 'input[appTestInput]',
  providers: [TEST_VALUE_ACCESSOR]
})
export class TestInputDirective implements ControlValueAccessor {

  onChange = (_: any) => { };
  onTouched = () => { };

  constructor(private _renderer: Renderer2, private _elementRef: ElementRef) { }

  // this gets called when the value gets changed from the code outside
  writeValue(value: any): void {
    const normalizedValue = value == null ? '' : value;
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  }

  registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }

  setDisabledState(isDisabled: boolean): void {
    this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
  }

  // just as an example - let's make this field accept only numbers
  @HostListener('keydown', ['$event'])
  _handleKeyDown($event: KeyboardEvent): void {
    if (($event.keyCode < 48 || $event.keyCode > 57) && $event.keyCode !== 8) {
      // not number or backspace, killing event
      $event.preventDefault();
      $event.stopPropagation();
    }
  }

  @HostListener('input', ['$event'])
  _handleInput($event: KeyboardEvent): void {
    // this is what we should call to inform others that our value has changed
    this.onChange((<any>$event.target).value);
  }
}

Just add this directive declaration to your module:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { TestInputDirective } from './test-input.directive';

@NgModule({
  declarations: [
    AppComponent,
    TestInputDirective // <-- you'll need this
  ],
  imports: [
    BrowserModule,
    FormsModule        // <-- and this
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

And then you can use it in the template like this:

app.component.html

<input type="text" appTestInput [(ngModel)]="name">
<div>{{name}}</div>

That's a bit more code than with pipes, true, but this is proper way of handling user input, especially when it should be pre-processed in some way.

Alexander Leonov
  • 4,694
  • 1
  • 17
  • 25
  • This works for simply killing a keystroke, but what if I want to change key 'a' to 'A' ? – TSG Sep 19 '17 at 04:22
  • Then pre-process the value that you pass to the onChange(). You can process whatever events you want on the input. For example, I omitted 'blur' event which I use sometimes to re-format the value that user typed in, particularly to add zeroes to the time (something like making '09:00' out of '9') or to convert string to upper case. There's whole bunch of things you can do here. I even change data type which this input is bound to. Date inputs in one of my projects are bound directly to the Date values, I don't have to have separate conversion from string value in the input. Very convenient. – Alexander Leonov Sep 19 '17 at 04:30
  • But I need to change characters as the user types (for example to replace / and \ with . ) so that's why I went with a pipe. I don't want to change the value on blur since that may frustrate the user. There must be a way to change and/or delete keys as the user types... – TSG Sep 19 '17 at 13:01
  • Well, implementing it with pipes will actually reset cursor position to the end of the string after each keystroke regardless of where it is typed in... so if user tries to input something in the middle they will become just furious. ;) Apart from that, there's no easy way to just "change" event. What you need to do is to cancel original event, setTimeout() to wait for its processing to completely finish, then generate your own event, fire it on the input... and pray this all works because browser can easily consider such tricks as something suspicious and block it. – Alexander Leonov Sep 19 '17 at 14:23
  • So, what you actually want to do is to make sure that you get correct value in the model and on the screen. ControlValueAccessor is specifically designed for this kind of stuff. With pipes you have no control over input element and its original events and will get intermittent incorrect values in the model in between the moment when keystroke happens and the moment when your pipe kicks in. ControlValueAccessor gives you abililty to **guarantee** than model value will **always** be correct. – Alexander Leonov Sep 19 '17 at 14:27