1

I'm trying to convert an application from pure JS to AngularJS and have run into a problem that I've extracted into the following code snipet.

I have two controllers that each call an SSE server and each has its own callback function. My understanding is that $scope for each controller would be different and modifying one would not effect the other.

However, whenever eventBCallBack() in eventBCtrl is executed, it seems to effect $scope of eventACtrl. My filter which is being called in eventACtrl is executed whenever eventBCallBack() is executed. Even if eventBCallBack() is an empty function, it makes no difference.

I suspect it has something to do with $scope.$apply.

Following is the HTML file:

<!DOCTYPE html>
<html ng-app="testApp">
<body>
  <div ng-controller="eventACtrl">
  <div>{{day}}</div>
  <lable for="filteredName">Filter:</label>
  <input type="text" name="filteredName" ng-model="filteredName"/>
  <table>
    <tbody>
        <tr ng-repeat="module in modules | matchFilter:filteredName | orderBy: 'name'">
            <td>{{$index+1}}</td>
            <td>{{module.name}}</td>
        </tr>
    </tbody>
  </table>
  </div>
  <div ng-controller="eventBCtrl">
  {{cpu}}
  </div>
  <script src="js/angular.min.js"></script>
  <script src="js/chaos.js"></script>
</body>
</html>

Following is the javascript code:

var testApp = angular.module("testApp", []);
testApp.controller("eventACtrl", function($scope) {

    var eventACallback = function(e) {
        $scope.$apply(function() {
        var pData = JSON.parse(e.data);
        var sDate = new Date(Number(pData.date));
        $scope.day = sDate.toDateString() + " " + sDate.toLocaleTimeString();
        $scope.modules = pData.modules;
        console.log("EVENTA");
        });
    }

    var source = new EventSource("http://" + location.host +"/EVENTS:A");
    source.addEventListener("EVENTA", eventACallback, false);
});

testApp.controller("eventBCtrl", function($scope) {

    var eventBCallback = function(e) {
        $scope.$apply(function() {
            var pData = JSON.parse(e.data);
            $scope.cpu = pData.cpu;
            console.log("EVENTB");
        });     
    }

    var source = new EventSource("http://" + location.host + "/EVENTS:B");
    source.addEventListener("EVENTB", eventBCallback, false);
});


testApp.filter("matchFilter", function() {
    return function(modules, filteredName) {
        console.log("filter: " + filteredName);
        var newModules = [];
        for (var i in modules) {
            if (modules[i].name.search(filteredName) != -1) {
                newModules.push(modules[i]);
            } else
                continue;
        }
        return newModules;
    };
});
Mysticial
  • 464,885
  • 45
  • 335
  • 332
mr emmech
  • 9
  • 1
  • Why don't you make use of event using a single connection not two connections e.g. event, 'EVENTA' and 'EVENTB' through http://location.host/event? – Donghwan Kim Apr 02 '15 at 14:30
  • One of the events repeats at a high frequency (10s to 100s of times per second). The other event not that much, but updates a very (very) large table. I can't afford to fire the large table event every time the high frequency event fires. Any way, I've now understood the root of the problem. $scope.$apply isn't confined to a controller's scope. It operates at the root scope. I think I need to some how call $digest instead of $apply to limit things to a particular controller's scope. Problem is, $digest doesn't seem to be working so far! – mr emmech Apr 02 '15 at 22:38
  • It seems that it is because of your server logic. Though, it is not desired (but not critical) considering that [browser limits the number of simultaneous connections](http://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser). – Donghwan Kim Apr 03 '15 at 04:04

2 Answers2

0

From what you are describing, the scope that controller A has is not "modified" in any way, right? There are no new models assigned to the scope or values of these models changed.

All that is happening is that your filter is executed. That is to be expected - an array filter is executed on every digest cycle, regardless of how the digest was initiated.

Here's a smaller way to reproduce your issue without any controllers or async functions with $scope.$apply (ng-if creates a new child scope)

<div ng-if="true" ng-init="items = [1, 2, 3]>
  <div ng-repeat="item in items | doNothing">{{item}}</div>
</div>
<button ng-click="">clicking me triggers digest</button>

doNothing here is:

.filter("doNothing", function(){
  return function(arr){
    console.log(arr);
    return arr;
  }
});
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • So in the code I presented, when will a digest cycle be normally executed? It seems that every time the async function is executed, the filter is called. My async function may be executed many times a second and every time the filter gets called. The filter is never called (except initially) if the async function isn't executed. – mr emmech Apr 02 '15 at 11:50
  • @mremmech, the digest cycle is executed when something that calls `$scope.$digest`. Typically, this is done internally by Angular (directives like `ng-click` do that), or when async functions are handled with `$q`, or built-in Angular services, like `$http` and `$timeout`. When you handle an async function manually, then `$scope.$apply` invokes a digest from the root. The filter function, as well as any $watched expression, is re-evaluated on every digest. – New Dev Apr 02 '15 at 13:36
-1

Posting the question here and on other forums was really helpful. I now clearly understand the problem and have worked out the solution.

First of all, $scope.$apply works on root scope. This means that even if I had separate controllers, $apply would trigger watchers for all $scope objects, not just on the controller from where $apply was called.

The solution was to remove $scope from the callback functions and call $digest from within the function. This way, $digest would only effect the current scope.

As follows:

testApp.controller("eventBCtrl", function($scope) {

    eventBCallback = function(e) {
            var pData = JSON.parse(e.data);
            $scope.cpu = pData.cpu;
            $scope.$digest();
            console.log("EVENTB");
    }

I'v tested this and it works fine.

mr emmech
  • 9
  • 1
  • That would help but that's not "the" solution. Anything, including unrelated `ng-click` or something else in the app will trigger a digest cycle and will cause your filter function to run. The way to solve it is to prefilter in the controller - not in the view. – New Dev Apr 06 '15 at 01:10