7

I want to filter user input when they type in an HTML text input.

I can do that in native HTML/JavaScript as shown in the following demo:

<form>
    <label for="tracking">Tracking Number:</label>
    <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2}\d{9}[A-Z]{2}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
    />
</form>

<script>
    const input = document.getElementById('tracking');

    input.addEventListener('input', () => {
        console.log('input fired');

        // Remove non-alphanumeric chars and force uppercase
        input.value = input.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
    });
</script>

filtering user input with HTML/JS

In the image above, I'm typing a, b, +, - (filtering works like I want). StackBlitz demo: Filter user input (native)

Now, I've done the same thing using Angular (with a template-driven form) as shown in the following demo:

@Component({
  selector: 'app-root',
  template: `
    <form>
      <label for="tracking">Tracking Number:</label>
      <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2}\d{9}[A-Z]{2}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
        [ngModel]="trackingNumber"
        (ngModelChange)="onTrackingChange($event)"
      />
    </form>
  `,
  imports: [FormsModule],
})
export class App {
  trackingNumber = '';

  onTrackingChange(value: string) {
    console.log('input fired');

    // Remove non-alphanumeric characters and force uppercase
    this.trackingNumber = value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
  }
}

filtering user input with Angular

In the image above, I'm typing a, b, +, - (filtering does NOT work like I want). StackBlitz demo: Filter user input (Angular)

As far as my Angular knowledge goes, this happens when the current ngModel value is the same as the new/filtered value, thus Angular does not trigger a change on the HTML text input.

How can I overcome this behavior in Angular?

Can I force Angular to trigger a change?

1
  • 1
    You can remove [ngModel] and (ngModelChange) and use (input)="onTrackingChange($event)" on input element like this: onTrackingChange(event: Event) { console.log('input fired'); const input = event.target as HTMLInputElement; input.value = input.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); } Commented May 15 at 15:53

5 Answers 5

1

The user "novative" on the Angular discord found a solution by replacing (ngModelChange) with (input) + manually updating the input value.

@Component({
  selector: 'app-root',
  template: `
    <form>
      <label for="tracking">Tracking Number:</label>
      <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2}\d{9}[A-Z]{2}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
        [ngModel]="trackingNumber"
        (input)="onTrackingChange(box)" 
        #box
      />
    </form>
  `,
  imports: [FormsModule],
})
export class App {
  trackingNumber = '';

  onTrackingChange(box: HTMLInputElement) {
    console.log('input fired');

    // Remove non-alphanumeric characters and force uppercase
    this.trackingNumber = box.value = box.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
  }
}

Thanks to anyone who tried to help!

Sign up to request clarification or add additional context in comments.

2 Comments

This seems essentially the same as what Milos Stojanovic suggested, which you weren't happy with -- ?
Why did you bind input with ngModel? I don't think it's necessary for your problem.
1

When using two-way binding [(ngModel)] with ngModelChange, Angular's change detection can cause timing issues where:

  • The view doesn't reflect the updated model value immediately
  • Input validation might not trigger properly
  • The displayed value might not match the model value

Solution 1: Using setTimeout (Quick Fix)

@Component({
  selector: 'app-root',
  template: `
    <form>
      <label for="tracking">Tracking Number:</label>
      <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2,}\d{9,}[A-Z]{2,}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
        [(ngModel)]="trackingNumber"
        (ngModelChange)="onTrackingChange($event)"
      />
    </form>
  `,
  imports: [FormsModule],
  standalone: true
})
export class App {
  trackingNumber = '';

  onTrackingChange(value: string) {
    setTimeout(() => {
      this.trackingNumber = value.replace(/[^a-zA-Z0-9]/g,'').toUpperCase();
    });
  }
}

Option 2: Reactive Forms (Recommended)

For more robust solutions, especially with complex validation:

@Component({
  selector: 'app-root',
  template: `
    <form [formGroup]="form">
      <label for="tracking">Tracking Number:</label>
      <input
        type="text"
        id="tracking"
        formControlName="trackingNumber"
        pattern="^[A-Z]{2,}\d{9,}[A-Z]{2,}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
      />
    </form>
  `,
  imports: [ReactiveFormsModule],
})
export class App {
  form = new FormGroup({
    trackingNumber: new FormControl('')
  });

  constructor() {
    this.form.get('trackingNumber')?.valueChanges.subscribe((value:any) => {
      if (value) {
        const cleanValue = value.replace(/[^a-zA-Z0-9]/g,'').toUpperCase();
        this.form.get('trackingNumber')?.setValue(cleanValue, { emitEvent: false });
      }
    });
  }
}

Comments

0

[ngModelChange] is not equivalent to the javascript onChange because the javascript one is more akin to a keyup event and the angular ngModelChange depends on the update to the ngModel which creates almost a cyclic redundancy when you update the value during the value update.

so for your issue here is my suggestion:

in your template, change ngModelChange to keyup and then your onTrackingChange function should now receive an actual event object, see below for the code and stack blitz demo

@Component({
  selector: 'app-root',
  template: `
    <form>
      <label for="tracking">Tracking Number:</label>
      <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2,}\d{9,}[A-Z]{2,}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
        [(ngModel)]="trackingNumber" // changed here
        (keyup)="onTrackingChange($event)" // changed here
      />
      
    </form>
  `,
  imports: [FormsModule],
})
export class App {
  trackingNumber = '';

  onTrackingChange(value: any) { //changed expected parameter to any
    console.log('input fired');

    // Remove non-alphanumeric characters and force uppercase
    this.trackingNumber = this.trackingNumber.replace(/[^a-zA-Z0-9]/g, '')
                                             .toUpperCase(); // changed here
  }
}

Stack Blitz Demo

3 Comments

In your stackblitz demo you are using input event instead of keyup as you provided in answer. Using input event as in demo won't do the trick, however keyup will, although you can still enter invalid char which is visibly immediately deleted.
hmm might have changed it to test, but yeah keyup was used as seen here in the code excerpt, I modified the stack blitz demo thanks for the heads up
This works better, but still fails to filter sometimes or some specific characters. A solution that works is using one-way data binding [ngModel], replacing (ngModelChange) with (input), and updating the input value manually. But thanks for trying to help!
0

You can combine those two ways. You can remove ngModel and use input event and update input's value when event is fired.

<input
  type="text"
  id="tracking"
  name="tracking"
  pattern="^[A-Z]{2,}\d{9,}[A-Z]{2,}$"
  title="Format must be like AB123456789CD"
  required
  minlength="13"
  maxlength="13"
  (input)="onTrackingChange($event)" // change here
>

with:

onTrackingChange(event: Event) {     
  console.log('input fired');     
  const input = event.target as HTMLInputElement;     
  input.value = input.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();   
}

This way you shouldn't be able to enter invalid char. Basically, you can enter it but it will be immediately replaced with '' which shouldn't be visible.

If you want, you can introduce component's variable as you did with trackingNumber which you will update every time you update input's value.

2 Comments

That's doing things natively and completely ditching ngModel and template-driven forms. But thanks for trying to help!
@nunoarruda No problem. Reading you question I didn't think that you needed to use ngModel. As I said, you can introduce variable to follow input's value if you want, but I don't see why you would need to bind it with ngModel?
-1

Indeed, [ngModel] only updates the DOM upon detecting a change in the bound property - if the bound property hasn't changed, ngModel will not write to the DOM. This is by design, because writing to the DOM can have side effects, such as resetting cursor position and text selection.

If you want to do it anyway, you therefore have to do it yourself, for instance like this:

      <input
        type="text"
        id="tracking"
        name="tracking"
        pattern="^[A-Z]{2}\d{9}[A-Z]{2}$"
        title="Format must be like AB123456789CD"
        required
        minlength="13"
        maxlength="13"
        [ngModel]="trackingNumber"
        (ngModelChange)="onTrackingChange(trackingInput, $event)"
        #trackingInput
      />

and

  onTrackingChange(inputElement: HTMLInputElement, value: string) {
    const fixedValue = value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
    this.trackingNumber = fixedValue;
    if (value != fixedValue) { 
      inputElement.value = fixedValue;
    }
  }

Note however, that this has the same usability artifacts as your plain DOM solution, in that any input will move the cursor to the end of the (fixed) text, which may be surprising if the cursor was previously elsewhere ...

4 Comments

I did play around with manually setting the input value. While it works better or filters more, it still fails to filter some characters sometimes. But thanks for trying to help!
Did this "playing around" actually use the code I posted? Because I tested my code before posting, it worked reliably. Can you describe a case where it does not?
Yes, I tested your code and the code of others who suggested the same thing. It fails to filter a tilde (~) and an acute accent (´), for example. Thanks for trying to help anyway.
can not reproduce: ~, ´, and even é are filtered correctly, i.e. neither the input field nor the component property contain these characters after the user presses these keys, or pastes a value containing such characters.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.