4

Take a look at this simple Angular2 component:

@Component({
  selector: 'status-area',
  directives: [],
  template: require('./status-area.tpl.jade')
})

export class StatusAreaComponent {

  constructor(private _settings: SettingsProvider) {}

  get items() {
     return _.filter(this._settings.features, (feature: any) => {
      return feature.inStatusBar && feature.isEnabled;
    });
  }
}

SettingsProvider has a property features, which is modified by another component (for example SettingsPanel). This components linked only by service (no input params).

So, as you have seen I need to fetch only enabled and marked as status-bar compatible features from _settings.features property.

For this purpose I use lodash method find.

However, as you might guess, angular throws an exception in development mode:

Expression has changed after it was checked

It happens because _.filter() for each pass returns new instance of array, but collection is same.

My question is, how you solve this situation? Or may be there is another solution/approach to handle this case.

In my opinion it would be very useful if angular2 had special decorator like that:

@checkIterable()
get items() {
     return _.filter(this._settings.features, (feature: any) => {
      return feature.inStatusBar && feature.isEnabled;
    });
  }

Of course I can solve this by using another approach, with events and events listeners, but it produced a lot of boilerplate code.

Take a look at this example:

export class StatusAreaComponent implements OnInit, OnDestroy {

  protected _items;
  protected _removeWatcherFn;

  constructor(private settings: SettingsProvider) {}

  ngOnInit() {
    this._items =  this._fetchItems();

    this._removeWatcherFn = this.settings.onChange.bind(() => {
      this._items =  this._fetchItems();
    });
  }

  ngOnDestroy() {
    this._removeWatcherFn();
  }

  _fetchItems() {
    return _.filter(this.settings.features, (feature: any) => {
      return feature.inStatusBar;
    });
  }


  get items() {
    return this._items;
  }
}

In this way Angular is happy, but i don't.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Timofey Yatsenko
  • 758
  • 10
  • 14

4 Answers4

1

You could try to the 'ChangeDetectorRef' class and its 'detectChanges' method.

Perhaps this question could help you:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
1

Since you are already using lodash you can make use of the convenient _.memoize method to cache result of filtering. It is usually used to avoid expensive calculations, but in your case you can benefit from its side-effect because Angular will not have this problem with filtered arrays being different objects for each check.

Here is possible solution for the problem:

@Component({
  selector: 'status-area',
  directives: [],
  template: require('./status-area.tpl.jade')
})
export class StatusAreaComponent {

  constructor(private _settings: SettingsProvider) {
    this.initFilter();
  }

  initFilter() {

    function filterItems(items) {
      return _.filter(items, (feature: any) => {
        return feature.inStatusBar && feature.isEnabled;
      })
    }

    this.filterFeatures = _.memoize(filterItems, function(items) {
      return filterItems(items).map(item => item.id).join();
    });
  }

  get items() {
    return this.filterFeatures(this._settings.features);
  }

}

Demo: http://plnkr.co/edit/MuYUO3neJdTs0DHluu0S?p=info

dfsq
  • 191,768
  • 25
  • 236
  • 258
1

Return the same array (reference) each time.

export class StatusAreaComponent {
  filteredItems = [];
  constructor(private _settings: SettingsProvider) {}
  get items() {
    let filteredItems = this._settings.features.filter( (feature: any) => {
      return feature.inStatusBar && feature.isEnabled;
    });
    this.filteredItems.length = 0;
    this.filteredItems.push(...filteredItems);
    return this.filteredItems;
  }
}

Note that ... is an ES2015 feature.

I assume you have something like *ngFor="#item of items">{{#item}} in your template. If so, your items() getter function will be called for every change detection cycle. This could get (CPU) expensive. You might be better off (re)generating the filtered list in the service whenever a feature is added/removed/changed. Then your StatusAreaComponent could simply get a reference to that filtered array once in ngOnInit(). That's probably how I would implement this.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • This solution will break a change detection. Because in each check detector will be think that this proprty isn't changed and component view will not be updated. – Timofey Yatsenko Feb 14 '16 at 13:05
  • @TimofeyYatsenko, I'm assuming there is an `NgFor` loop in the template. If so, change detection will check the bindings for each item in the array, so the view will be updated, even though the array (reference) is not changing. There is definitely some common confusion around this... see [this answer](http://stackoverflow.com/a/35106215/215945) for more info. – Mark Rajcok Feb 15 '16 at 17:13
0

Store the filtered array and the filter criteria and recreate the filtered array only when the filter criteria or the array items changed and always return the stored array.

Another way is to change the change detection to onPush and notify Angular explicitly about changes by injecting ApplicationRef and calling its .tick()

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567