angularangular2-docheck

How to watch for changes in an array of objects without causing massive lag?


I feel there are some things that are missing in Angular 2 that Angular 1 had, one of those things being the $watch function.

OnChanges and DoCheck are supposed to replace the $watch but I haven't been able to use either to get the desired result. OnChanges can't listen to objects so that's of no use in this scenario, and while DoCheck does the job, it causes massive lag since it seems to go bananas as soon as a change happens.

Here I've got a component which has a bunch of checkboxes, each checkbox toggle causes a value to be added to an array, rather than the default true/false functionality.

What I need to watch for in this component is the isDisabled state of a checkbox, so each time it changes I need to update the isChecked state of the checkbox.

Like I said, it works, but it has 2-3 seconds of delay on every checkbox toggle, despite only having 5 checkboxes. Preferably I wouldn't want the loop to run every time you toggle a checkbox, but instead only if the isDisabled state of a checkbox changes.

EDIT: I was told an Observable could do the job here, but I can't find any examples of this being applied to an array of objects like this. It also needs to trigger without calling its next() method manually, since it has to listen for outside changes to the array that is being sent in.

EDIT 2: I created a plunkr which doesn't show the lag, probably because in my app I have other things that might have an affect on the delay as well. However, the plunkr clearly shows that there seems to be something wrong because I put a console.log(true) within the ngDoCheck() function and it runs a looot of times. Which can't be intended, if it is, it's really odd.

Here's the plunkr: https://plnkr.co/edit/dJVnqbrredDyWTXNDzDK?p=preview

How can I do this?

export class FormCheckboxMultipleComponent implements OnInit, DoCheck {
  @Input() model: Array<any>;
  @Output('modelChange') onModelChange: EventEmitter<any> = new EventEmitter();
  @Output() callback: EventEmitter<any> = new EventEmitter();

  constructor(private _globals: GlobalVariablesService) {
    this.ns = _globals.ns;
  }

  ngOnInit() {
    this.model = this.model || [];
  }

  ngDoCheck() {

    for (let checkbox of this.checkboxes) {

      if (checkbox.isDisabled) {
        this.untoggle(checkbox);
      }
    }
  }

  findIndex(checkbox) {

    return this.model.reduce(function(cur, val, index) {

      if (val.name === checkbox.name && cur === -1) {
        return index;
      }

      return cur;
    }, -1);
  }

  addToModel(checkbox) {
    this.model.push(checkbox);
  }

  removeFromModel(i) {
    this.model.splice(i, 1);
  }

  toggle(checkbox) {

    if (checkbox.isDisabled) {
      return false;
    }

    let existingIndex = this.findIndex(checkbox);

    if (existingIndex === -1) {
      this.addToModel(checkbox);
    }
    else {
      this.removeFromModel(existingIndex);
    }

    checkbox.isChecked = !checkbox.isChecked;

    this.onModelChange.emit(this.model);

    this.callback ? this.callback.emit() : false;
  }

  untoggle(checkbox) {

    let existingIndex = this.findIndex(checkbox);

    this.removeFromModel(existingIndex);

    checkbox.isChecked = false;

    this.onModelChange.emit(this.model);
  }
}

Solution

  • That's caused by this.onModelChange.emit(this.model); in this.untoggle(checkbox) in ngDoCheck. This is a perfect endless loop. this.onModelChange.emit(this.model); causes change detection and therefore ngDoCheck() to run and the loop begins again.

    Ensure that change detection doesn't cause change detection. ngDoCheck is called by change detection and must not invoke another change detection otherwise you get an endless loop.

    Plunker