11

I have an input field on my html that I need to limit the value of.

I am trying doing like this:

HTML:

<div *ngFor="let option of options">
    <input type="text" [ngModel]="option.value" (change)="onChangeValue($event.target.value, option)">
</div>

Typescript:

onChangeValue(valueString: string, option: OptionViewModel) {
    var value: number = parseInt(valueString);

    // Check some conditions. Simplified code just for example
    if (option.value != value && value > 5) {
        value = 5;
    }

    option.value = value;
}

OptionViewModel:

export class OptionViewModel {
    public optionId: number;
    public description: string;
    public pollId: number;
    public value?: number;
}

I tried using two-way binding, but my code relies on the previous value of option.value, and using two-way binding changes the variable before entering the function.

The problem is, sometimes the input field is not being updated. It looks like I just work the first time the value need to be changed, so for example, if input 6 the field is correctly changed to 5, but then if I add a 0 (making it 50), it doesn't correct to 5.

I debbuged the code and it is running the function and changing the option.value, and even using Augury to inspect the objects show the correct value.

2
  • Does OptionViewModel contain several fields? Can you show us that class (if not too large)? Commented Mar 15, 2018 at 14:19
  • @ConnorsFan I updated the question with the class Commented Mar 15, 2018 at 14:25

5 Answers 5

3

On your second edit, the content of the input element is not updated because Angular has not detected any change (option.value was 5, and it is still 5). Here are two methods to force the refresh of the element.


Method 1 - Replace the option item in the array

You can force the field to update by replacing the option in the array with a cloned copy, as shown in this stackblitz:

<div *ngFor="let option of options; let i=index">
  <input [ngModel]="option.value" (change)="onChangeValue($event.target.value, i)" type="text" >
</div>
onChangeValue(valueString: string, index: number) {
  var value: number = parseInt(valueString);
  if (value > 5) {
    value = 5;
  }
  this.options[index] = this.options[index].cloneWithValue(value);
}

export class OptionViewModel {
  public optionId: number;
  public description: string;
  public pollId: number;
  public value?: number;

  public cloneWithValue(value: number): OptionViewModel {
    let dest = new OptionViewModel();
    dest.optionId = this.optionId;
    dest.description = this.description;
    dest.pollId = this.pollId;
    dest.value = value;
    return dest;
  }
}

Method 2 - Use an additional field in the trackBy function

An alternative solution is to add an additional field to OptionViewModel (e.g. lastModified) and to use it in a trackBy method of the ngFor directive (see this stackblitz):

<div *ngFor="let option of options; trackBy: trackByFn">
  <input [ngModel]="option.value" (change)="onChangeValue($event.target.value, option)" type="text">
</div>
onChangeValue(valueString: string, option: OptionViewModel) {
  var value: number = parseInt(valueString);
  if (value > 5) {
    value = 5;
  }
  option.value = value;
  option.lastModified = Date.now();
}

trackByFn(index: number, option: OptionViewModel) {
  return `${index}___${option.lastModified}`;
}
export class OptionViewModel {
  public optionId: number;
  public description: string;
  public pollId: number;
  public value?: number;
  public lastModified: number = 0;
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you, both solutions worked. Do you know which one is better?
The first method recreates the options items as you edit the values. If you are referencing these objects in other parts of the code, you may not want to clone them. On the other hand, the trackBy method looks a little strange to me, but it keeps the option references intact. So, I don't know which one is really better...
What if option is primitive?
3

The simplest solution worked for me is to use ChangeDetectorRef.The change detection tree collects all views and updates the values of all components

  1. Make variable type of ChangeDetectorRef in contructor like below:-

     constructor(private changeDet:ChangeDetectorRef) { }
    
  2. Now just call changeDet.detectChanges() after setting/updating your value

     onChangeValue(valueString: string, option: OptionViewModel) {
     var value: number = parseInt(valueString);
    
     // Check some conditions. Simplified code just for example
     if (option.value != value && value > 5) {
         value = 5;
     }
    
     option.value = value;
     this.changeDet.detectChanges();//Detects changes and updates value
    }
    

Comments

2

Hi try to change ng model two way binding ,like that

    <div *ngFor="let option of options">
        <input type="text" [(ngModel)]="option.value"  #ctrl="ngModel"
 (change)="onChangeValue($event.target.value, option)">
    </div>

1 Comment

I tried using two-way binding, but my actual code relies on the previous value of option.value. I updated my question to reflect this.
1

Found a simpler workaround. Just use (keyup) instead of (change).

<input class="editable-field-input" type="number" value="editableFieldValue" [(ngModel)]="editableFieldValue" (keyup)="editableFieldValueChanged()" />

I haven't passed the event as argument because I already have the ngModel updating my variable.

editableFieldValueChanged() {
  if (this.editableFieldValue < -100) {
    this.editableFieldValue = -100;
  }
  if (this.editableFieldValue > 100) {
    this.editableFieldValue = 100;
  }
}

The TLDR version is, Angular considers this the same as choosing the same option in a dropdown over and over again, because the previous and current value is the same and hence it finds no reason to update the DOM element.

I believe this approach works because the (keyup) has a different execution order compared to the other events. I will leave the explaining to the people who are a lot more knowledgeable than I am.

Comments

0

You can try also creating a simple observable that will check the information for you.

import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

@Component({
  selector: 'my-app',
  template: `<input #inputName type="text" />`,
})
export class AppComponent implements OnInit {
  value: string = '';
  @ViewChild('inputName') inputName: ElementRef;

  ngOnInit() {
    const input = this.inputName.nativeElement as HTMLInputElement;
    Observable.fromEvent(input, 'input')
      .map((evt: Event) => evt.target as HTMLInputElement)
      .subscribe(target => target.value = Number(target.value) > 5 ? '5' : target.value);
  }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.