5

Pretty standard situation.

There is one parent component <item-list>. Inside its template with *ngFor generated 20 child components <item-block>. Child component styles set with [ngStyle] directive and template expression that calls function setStyles().

The problem (or maybe not) is that when any event emitted on one specific child element, expression setStyles() executed twice for each of child components.

So if we click on one specific item in our example, and we have 20 <item-block> components - setStyles() will be executed 20+20 times.

The questions are:

  1. Why its happening and is it expected behavior.
  2. How it affect performance
  3. How it could be avoided - only one call per child component/detection change.

Example & plnkr:

plnkr (click on item - open console for debug output)

import {Component} from '@angular/core'

@Component({
  selector: 'item-list',
  template: `
    <item-block
        [item]="item"
        *ngFor="let item of items"
    ></item-block>
  `,
})
export class ItemListComponent {

  items: any[] = [];

  constructor() {}

  ngOnInit() {
     // generate dummy empty items
    for (let i = 0; i < 20; i++) {
      this.items.push(
        {
          value: 'item #' + i; 
        }
      )
    }
  }
}

import {Component, Input} from '@angular/core'

@Component({
  selector: 'item-block',
  template: `
    <div
      class="item"
      [ngStyle]="setStyles()"
      (click)="testClick($event)"
    >{{item.value}}</div>
  `,
})
export class ItemBlockComponent {

  @Input() item: any;

  constructor() {}

  testClick(): void{
      console.log('item clicked');
  }

  setStyles(){
      console.log('seting styles...');
      return {
          'background': '#ccc'
      };
  }
}
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Alex Kalmikov
  • 1,865
  • 19
  • 20

2 Answers2

3
[ngStyle]="setStyles()"

causes setStyles to be called every time change detection is run (which can be quite often and will hurt performance). Also because setStyles() returns a different object instance every time, it should cause an exception. "Expression changed since it was last checked" or similar.

Calling methods from the view this way is discouraged.

Instead assign the value to a property and bind to that property:

[ngStyle]="myStyles"
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • i thought so aswell, but angular2 seems a bit different. are you sure the watcher is as simpe as in version 1? VErsion 2 works with definedproperties which have getter and sdetter and can run different actions when setting or getting values – Mephiztopheles Jan 04 '17 at 19:08
  • If it's discouraged. Why its described in official docs as one of the options: https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ngStyle – Alex Kalmikov Jan 04 '17 at 19:10
  • Sorry, I don't understand your comment. I also only know Angular2 well and almost nothing about Angular1. – Günter Zöchbauer Jan 04 '17 at 19:11
  • I think that's quite a bad example. I'll create a bug report. – Günter Zöchbauer Jan 04 '17 at 19:11
  • ah ok.. in angular 1 every angular own function calls apply-function on scope. the apply will run a digest and any digest will check each watcher for changes. so this behaviour was typical for angular 1 to run some functions twice or more. In angular 2 i saw they are using definedProperties and i thought they would use the setter to run a digest – Mephiztopheles Jan 04 '17 at 19:13
  • In Angular2 digest is "change detection". Change detection is run when zone.js detects that an async callback (event handler, observable subscription, setTimeout, ... was completed). – Günter Zöchbauer Jan 04 '17 at 19:16
  • so pretty same as angular 1.. do you know why they changed their syntax so hard? – Mephiztopheles Jan 04 '17 at 19:17
  • One reason was, because in Angular2 they needed a concrete implementation for every event they wanted to support. In Angular2 event binding is generic and every event is supported by default. There was a very looong discussion about the new syntax about 2 years ago. Angular1 syntax just didn't work for them in general for all kinds of reasons - don't remember details. Angular2 is an entirely new framework and they didn't see strong arguments about dropping anything they were not entirely happy with. – Günter Zöchbauer Jan 04 '17 at 19:19
  • events like input or click? and why this new template syntax? [ngStyle] is that html-conform? – Mephiztopheles Jan 04 '17 at 19:22
  • Yes, events like inputs and DOM events and custom events. `[ngStyle]` is HTML conform but it doesn't matter because it's never added to the DOM this way. It's processed by Angular and only the result is added to the DOM. – Günter Zöchbauer Jan 04 '17 at 19:23
  • @Günter Zöchbauer, So why change detection runs on each of components and not on one that clicked ? Is it by design ? – Alex Kalmikov Jan 04 '17 at 19:26
  • you NEED to detect in both directions which can be accesed by the current scope to get all changes – Mephiztopheles Jan 04 '17 at 19:27
  • 1
    Because a click event bubbles and Angular doesn't know what components actually have an event handler registered. – Günter Zöchbauer Jan 04 '17 at 19:28
1

In default (development mode) Angular run Detect Changes mechanism twice.
In production mode it is reduce to single change.

How to switch Detect Changes mechanism to production?

In main.ts file try to add:

import { enableProdMode } from '@angular/core';
// ...
enableProdMode();
// ...
platformBrowserDynamic().bootstrapModule(AppModule)

and reload application.

piecioshka
  • 4,752
  • 2
  • 20
  • 29