3

I have an angular2 component 'my-tree' which I am using in my parent 'my-app' component. 'my-app' is as below:

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <my-tree *ngFor="#node of nodes" [title]="node">
      <my-tree *ngFor="#subNode of getSubNodes(node)" [title]="subNode">
      </my-tree>
    </my-tree>  
  `,
  directives: [MyTree]
})
export class App {
  constructor() {
    this.nodes = ['Angular2', 'typescript', 'js']    
  }

  getSubNodes( node: string ) {
    if( node === 'Angular2') {
      return ['2.0.0', '1.4.2']
    }
    if ( node === 'typescript' ) {
      return ['1.7.3'];
    }
    if ( node === 'js' ) {
      return ['es-6'];
    }    
  }  
}

my-tree is a simple component -

@Component({
  selector: 'my-tree',
  providers: [],
  inputs: ['title'],
  template: `
    <ul>
      <li><span>{{title}}</span></li>
      <ng-content></ng-content>
    </ul>
  `,
  directives: []
})
export class MyTree {
    private title: string;

}

When this is executed, the console is logged with following errors - Expression 'getSubNodes(node) in App@2:15' has changed after it was checked. Previous value: '2.0.0,1.4.2'. Current value: '2.0.0,1.4.2'.

See this plunk for actual code.

The idea in my code is to create a tree( just an example), the first levels come from an array(hard coded values), the second level come from a function that returns the next set given the current node(or value) from the first level. And its the call to this function where angular complains for the expression being changed after it was checked. Though the value is reported exactly same as it was earlier in the error message. I searched for this error on SO, and found few references, but mostly they suggest to invoke change detection. I am unable to understand why this is required and also how to do that. I also read that this is a diagnostic message only and it is not thrown in production mode.

Is it not possible to call a function within an *ngFor? What should be done to get rid of this error?

SnareChops
  • 13,175
  • 9
  • 69
  • 91
tyrion
  • 714
  • 2
  • 7
  • 27

2 Answers2

3

The problem is

 getSubNodes( node: string ) {
    if( node === 'Angular2') {
      return ['2.0.0', '1.4.2']
    }
    if ( node === 'typescript' ) {
      return ['1.7.3'];
    }
    if ( node === 'js' ) {
      return ['es-6'];
    }    
  }  

here each time Angular checks the value it gets a new array instance, thus they are never the same. Angular doesn't compare the values only instance equality of the array.

This check is also only done in development mode. See also What is difference between production and development mode in Angular2?

This way Angular should be satisfied

 angular2 = ['2.0.0', '1.4.2'];
 typeScript = ['1.7.3'];
 js = ['es-6'];

 getSubNodes( node: string ) {
    if( node === 'Angular2') {
      return this.angular2;
    }
    if ( node === 'typescript' ) {
      return this.typeScript;
    }
    if ( node === 'js' ) {
      return this.js;
    }    
  }  
Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Ok, now I understand the reason for the error message. But it may not be always possible to define return values which always satisfy this criteria. a function call may not always return this way always. so basically does it mean that functions should not be used? – tyrion Jan 23 '16 at 17:15
  • 4
    You could opt for `@Component({changeDetection:ChangeDetectionStrategy.OnPush})` and notify Angular about changes. See https://github.com/angular/angular/issues/4746 or implement `doCheck` https://angular.io/docs/js/latest/api/core/DoCheck-interface.html – Günter Zöchbauer Jan 23 '16 at 17:19
  • 1
    That was really helpful. – tyrion Jan 23 '16 at 17:32
1

The problem is that getSubNodes(node) violates the idempontent rule because a new array is returned each time the method is called.

From the Template Expressions section of the Template Syntax dev guide:

Template expressions can make or break an application. Please follow these guidelines unless you have an exceptionally good reason to break them in specific circumstances that you thoroughly understand.
....

Idempotent Expressions

In Angular terms, an idempotent expression always returns exactly the same thing until one of its dependent values changes.

Dependent values should not change during a single turn of the JavaScript virtual machine. If an idempotent expression returns a string or a number, it returns the same string or number when called twice in a row. If the expression returns an object (including a Date or Array), it returns the same object reference when called twice in a row.

In development mode, Angular will complain if the idempotent requirement is violated, because in development mode, template bindings are checked twice, to find these types of violations. Angular is trying to be helpful, because if you were doing something different, like modifying application state that is visible in a parent component, that parent component's view wouldn't get updated due to the single pass through the component tree during change detection. I.e., once a parent component is checked for changes, it is not checked again, even if a descendant component changes some data that the parent component has bound in its view/template.

To fix the problem, rework the code to not violate the rule.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492