3

My sample code (down there) raises two issues:

  1. the "renderthis" function is called twice per-item when simply loading the page

  2. the main issue - when clicking the button I assume that after eval'ing the doit() expression, ng-click calls $apply on the scope, which causes the two DOM elements for each item to be re-rendered.. now, i understand that the scope for each item here is somehow bound to the parent scope or related to it by some weird isolation or transclusion or whatnot which are things i have yet to master.. is there a way to make ng-click only call $digest on the child scope of each item or something of this sort?

here is my code:

html:

<body ng-controller="MainCtrl">
     <ul>
        <li ng-repeat="item in items">
        <div>
         <span>{{item.title}}</span> 
         <span>{{renderthis()}}</span>
         <span>{{item.number}}</span>
         <button ng-click="doit()">CLIQUE!</button>
        </div>
        </li>
      </ul>
  </body>

JS:

var app = angular.module('angularjs-starter', []);

app.controller('MainCtrl', function($scope) {
  $scope.doit = function(){
    console.log('doing it for item with title --> ', this.item.title);
  }
  $scope.renderthis = function(){
    console.log('rendering this for item with title -->', this.item.title);
    return '|';
  }
  $scope.items = [{title: 'hello', number: 1}, {title: 'shalom', number: 42}];
});

Or see this plnkr:

http://plnkr.co/edit/VkR82lbJ0Mi2z12RqBfW

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
ayal gelles
  • 2,829
  • 1
  • 21
  • 18

3 Answers3

3

After some ping-pong with @Mark regarding this (see below) it was suggested that I rephrase the question to something of this sort: "How can I make a click event inside an ng-repeat scope only re-render the affected item (i.e., not run $apply) " This is to emphasize, as Mark explained, that it's not so much possible to work around using "ng-click" itself since it issues a hard-coded "$apply" and this runs a digest loop etc.. (see more below)

I will accept my own answer which includes my own version of ng-click that uses $digest instead of $apply and thus does not trigger a re-rendering of all ng-repeat'ed items.

Here is the revised ng-click code (xng-click):

var xngEventDirectives = {};

angular.forEach(
  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup'.split(' '),
  function(name) {
    var directiveName = directiveNormalize('xng-' + name);
    xngEventDirectives[directiveName] = ['$parse', function($parse) {
      return function(scope, element, attr) {

        var fn = $parse(attr[directiveName]);
        element.bind(name.toLowerCase(), function(event) {
        fn(scope, {$event:event});
        scope.$digest();
        });
      };
    }];
  }
);

And here is a plnkr to demo it:

http://plnkr.co/edit/3i7TCEkBtxCFw2ESeyaU.

ayal gelles
  • 2,829
  • 1
  • 21
  • 18
2

is there a way to make ng-click only call $digest on the child scope of each item or something of this sort?

No. To achieve this functionality, you'd have to create your own directive.

ng-click triggers a $digest loop (see section "Runtime") which will (among other things) examine all $watches. Each set of {{}}s in a view sets up a $watch. This is why when you click you see the log()s from the renderthis function.

I don't think anything is re-rendered unless the $watches detect a change.

Regarding the "called twice per-item when the page loads":

(Since watchExpression can execute multiple times per $digest cycle when a change is detected, be prepared for multiple calls to your listener.) -- Scope#$watch API doc

@Artem has a nice explanation about Angular's digest cycle and dirty checking. To summarize, because Angular found something that changed on the first $digest cycle, it runs another one to make sure all models have stabilized (not changed).

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • please see this plnkr: http://embed.plnkr.co/VkR82lbJ0Mi2z12RqBfW. a click on one of the buttons changes nothing in the scope and still the "renderthis" function is called for both items' scopes.. does this not mean that a re-render just happened? what if "renderthis" is a function that should run per-item and does some heavy lifting and i have 10000 items in my list.. if bet if i bound the click myself and called scope.$digest this would not have happened, but i was looking for a more angulary solution to this. – ayal gelles Jan 30 '13 at 21:40
  • (@ayal, I had tested your plnkr before I posted.) Because of the {{}}s around `renderthis()` in your view, Angular automatically sets up a $watch for that. Anytime $apply is called, that watch will be examined, and hence your renderthis() method is called. If that means "render" to you, then rendering is happening. (To me "render" means the browser has to repaint part of the visible page, because something in the DOM changed -- nothing in the DOM changes when you click the button. So to me, there is no rendering). scope.$digest will still cause all $watches in your view to be examined. – Mark Rajcok Jan 30 '13 at 21:58
  • whether "render" is the right word here, the fact remains that if I had 1000000 items on that list, a click in the scope of *one of them* will have triggered 1000000 function calls.. Also please see this forked plnkr (http://plnkr.co/edit/1gCzOWIpr2mLD4Ad1CIa) in which i no longer use ng-click - i bind the click event myself and call $digest myself, this time even with an actual change to the scope. In this case there is no "rendering" of the other elements in the list (though the double call for "renderthis" for the first item is still super-weird to me) – ayal gelles Jan 31 '13 at 06:37
  • This is more or less (probably less) what I expected ng-click would do: http://plnkr.co/edit/3i7TCEkBtxCFw2ESeyaU. (The code is almost the same, only it calls $digest) – ayal gelles Jan 31 '13 at 10:12
  • @ayal, I like your last fiddle. Although I think I answered the question as posed (i.e., no, ng-click can't just run $digest on the child scope where the event happened), I suggest 1) reword the question more like the following: How can I make a click event inside an ng-repeat scope only re-render the affected item (i.e., not run $apply)? 2) add your last fiddle as an answer and accept it. I have at least two other SO questions in mind that I'd like to refer to such an answer. – Mark Rajcok Feb 01 '13 at 17:22
  • @ayal, I modified my answer to include a link about how dirty checking works. Hopefully it will help you understand the double call for "renderthis". – Mark Rajcok Feb 01 '13 at 17:22
  • @ayal, FYI, above, when I suggested rewording the question, I meant the title -- in case that wasn't clear. – Mark Rajcok Feb 01 '13 at 17:50
0

You can try ngp-local-click instead of ng-click -> https://github.com/ansukla/ng-perf

Ambika Sukla
  • 244
  • 4
  • 3