Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the oninput event behave differently in Angular than it does in JavaScript?

I'm learning Angular with TypeScript, and a few inconsistencies with JavaScript are troubling me. Here's a function which works perfectly with pure JavaScript (it programmatically sets min and max attributes to an input - please don't ask me why I'd want to do that programmatically):

function change_number(input,min,max) {
  if(input.value > max)
    input.value = max;
  else if(input.value < min)
    input.value = min;
}
<input type='number' oninput='change_number(this,1,5)' placeholder='Enter a number' />

In Angular and TypeScript, the function behaves strangely: If I enter 10 into the input field, it doesn't reset to 5, until I enter another digit, meaning the value is only read after the input event (and not on-input - don't know if that makes sense). What's more strange is it works fine with the keydown and keyup events. Why this behaviour? Is it linked to the way Angular binds events to inputs? I have a feeling understanding this would help me better understand Angular's binding mechanism with NgModel

FYI, here's how I called the function in Angular (using the Ionic Framework) - I used the bound quantity in [(ngModel)] as the value of the input:

<ion-input type='number' (input)='change_numbers(1,5)' [(ngModel)]='quantity'></ion-input>
like image 401
Cedric Ipkiss Avatar asked Jul 30 '20 08:07

Cedric Ipkiss


People also ask

What is Oninput in Angular?

The oninput event occurs when an element gets user input. This event occurs when the value of an <input> or <textarea> element is changed.

What is the difference between ngModel and ngModelChange?

The NgModel class has the update property with an EventEmitter instance bound to it. This means we can't use (ngModelChange) without ngModel . Whereas the (change) event can be used anywhere, and I've demonstrated that above with the [value] property binding instead.

What does Oninput do in Javascript?

The oninput attribute fires when the value of an <input> or <textarea> element is changed. Tip: This event is similar to the onchange event. The difference is that the oninput event occurs immediately after the value of an element has changed, while onchange occurs when the element loses focus.

What is the difference between browsers and react JS on change event?

Unlike React, the browser fires onchange event after focus from input element is taken off. So when focus is set on an input element and something is typed, onchange won't be fired until and unless the input element is out of focus.


2 Answers

To understand why it behaves like that, first you need to be familiar with two basic concepts:

  1. A "banana in the box" syntax [(ngModel)]="quantity" is nothing more than sugar syntax which is translated to [ngModel]="quantity" (ngModelChange)="quantity = $event"
  2. In angular, change detection is by default triggered at the end of the call stack (this is possible with zone.js).

With this knowledge, you can understand what really happens here:

stackblitz

  1. You type 4 into the input.
  2. onNgModelChange is triggered, quantity is set to 4. The inner model value of input (more specifically, ngModel directive) is still equal to undefined.
  3. change_numbers is called, quantity is not changed.
  4. This is the end of the callstack (note that ngModelChange and change_numbers all called in a single callstack due to how angular handles events). Change detection is performed. Because quantity (4 now) is bound to the ngModel (undefined now), angular detects a need to update the UI (because undefined != 4 of course).
  5. Update is performed, everything works as expected.

  1. You type the next number - 8.
  2. onNgModelChange is triggered, quantity is set to 48. The inner model value of input (more specifically, ngModel directive) is still equal to 4.
  3. change_numbers is called, quantity is changed to 5.
  4. This is the end of the callstack. Change detection is performed. Because quantity (5 now) is bound to the ngModel (4 now), angular detects a need to update the UI (because 5 != 4 of course).
  5. Update is performed, everything works as expected.

  1. You type the next number - 7.
  2. onNgModelChange is triggered, quantity is set to 57. The inner model value of input (more specifically, ngModel directive) is still equal to 5.
  3. change_numbers is called, quantity is changed to 5.
  4. This is the end of the callstack. Change detection is performed. Because quantity (5 now) is bound to the ngModel (also 5 now), angular doesn't detect a need to update the UI (because 5 == 5 of course).
  5. Update is not performed, UI remains stale.

A solution to that might be calling detectChanges in onNgModelChange method. It will work, because in this moment we will update the inner model value of ngModel (even if it is much higher than 5). So even if we next decrease the value to 5, angular still will detect a need to update the UI.

I hope that explains everything. If you'll have some questions, don't hesitate to let me know. Cheers!

like image 23
Krzysztof Grzybek Avatar answered Oct 24 '22 05:10

Krzysztof Grzybek


Angular framework is behaving perfectly the way it has to. It is us who are a bit confused.

Angular handles forms in a bit different manner. There are two approaches to build forms in angular -

  • Reactive (Synchronous)
  • Template-driven (Asynchronous)

The Key difference between them is the above Sync/async behaviour. In simple terms we can say that.

  • Synchronous - during the data change.
  • Asynchronous - after the data has changed.

x

<ion-input type='number' (input)='change_numbers(1,5)' [(ngModel)]='quantity'></ion-input>

Above one is a Template-driven approach. Here we are using [(ngModel)], this updates only after the data has changed.

In order to make a reactive input field, use below code :-

in app.component.ts file

import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'dd-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  form: FormGroup;
  min = 0;
  max = 5;

  constructor(private fb: FormBuilder) {
    this.buildForm();
  }

  buildForm(): void {
    this.form = this.fb.group({
      numberInput: ''
    });

    this.form.get('numberInput').valueChanges.pipe(distinctUntilChanged()).subscribe(_ => {
      if (_) {

        if (_ > this.max) {
          this.form.get('numberInput').setValue(5);
        }
        if (_ < this.min) {
          this.form.get('numberInput').setValue(0);
        }

      } else {
        this.form.get('numberInput').reset('');
      }
    });
  }
}

In app.html (Here i am posting 2 way to express it.)

  <form [formGroup]="form">
    <!-- <input type='number' [min]='0' [max]='5' formControlName="numberInput" /> we can also this one, more easy just set value dynamically -->
    <input type='number' formControlName="numberInput" />
  </form>

NOTE:- DONT FORGET TO IMPORT ReactiveFormsModule

More about forms can found in the official docs : -https://angular.io/guide/forms-overview

like image 69
abhay tripathi Avatar answered Oct 24 '22 05:10

abhay tripathi