2

I've noticed what seems to be a bug for me, but is probably more me misusing the $compile service in AngularJS : I have a directive called "dynamic" that compiles angularjs code and shows it into a div. The code that I compile in that case contains ng-controllers and those controllers are listening on events. The problem is that apparently controllers aren't "dead" after being replaced, because controllers that should be vanished still react to events (like $routeChangeSuccess or any other event). Here is a working plunkr that shows the problem. Let's see an example code of my problem :

The directive I'm using :

app.directive('dynamic', function ($compile) {
    return {
        restrict: 'A',
        replace: true,
        link: function (scope, element, attrs) {
            scope.$watch(attrs.dynamic, function(html) {
                element.html(html);
                $compile(element.contents())(scope);
            });
        }
    };
});

The main controller, followed by the controllers that I include:

app.controller('TestCtrl', function($scope) {
  $scope.dynamicContent = "Default content";

  $scope.firstButton = function() {
    $scope.dynamicContent = "<div ng-controller='FirstCtrl'>The div from first button</div>";
  }

  $scope.secondButton = function() {
    $scope.dynamicContent = "<div ng-controller='SecondCtrl'>The div from second button</div>";
  }

  $scope.checkButton = function() {
    $scope.$broadcast('checkEvent');
  }
});

app.controller('FirstCtrl', function($scope) {
  $scope.$on('checkEvent', function() {
    alert(1);
  });

});
app.controller('SecondCtrl', function($scope) {
  $scope.$on('checkEvent', function() {
    alert(2);
  });
});

Now if I call firstButton() then secondButton() then checkButton(), instead of receiving only the alert(2), I receive two alerts. If I hit buttons 1/2/1/2/1/2/1/2 it's going to show me as many alerts as buttons I've clicked.

What am I doing wrong here ?

Thanks, hilnius

4
  • It seems like your dynamic directive does what ng-include does ... except that ng-include (my assumption) is probably using $scope.$destroy() to get rid of scopes created by the dynamic templates it has loaded. Commented Mar 16, 2014 at 5:22
  • I cannot use ng-include because what I want to show is the result of a post request. Should I use manually $scope.$destroy in the right scope before changing the dynamic variable ? Commented Mar 16, 2014 at 5:45
  • Yes, I'd head in that direction. Admittedly, I've always relied on Angular disposing of controllers/scopes for me, so I don't know how successful this approach will be. Perhaps seeing what ng-include does while creating/destroying scopes might be helpful. Commented Mar 16, 2014 at 5:57
  • I realized after posting what your intentions were, so the $templateCache doesn't really apply, but it may help someone else in the future. Commented Mar 16, 2014 at 6:47

1 Answer 1

2

Your really close. First I'll give you what your probably looking to do since I do not know your intentions for the $compile service. Then I'll explain why you don't need the $compile service for this particular instance since your effectively duplicating ng-include.

What your probably looking to do:

The key to using directives (especially when attempting to "$compile" dynamic content is ensuring your know what scope is passed where. For most of the directives built into angularjs, angular automatically handles creation (via scope.$new() ) and destruction (via scope.$destroy() ). Since you are not explicitly '$destroy'-ing the scopes they will not be removed. Another problem is that your directly attaching the "dynamic" directive to the current scope without creating a child scope or an isolate scope in the directive (via $new):

Plunkr example

app.directive('dynamic', function ($compile) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
          var curScope = null,
              curEle = null;

          function removeOld(){
            if( curScope ){
              curScope.$destroy();
              curScope = null;
              curEle.remove();
              curEle = null;
            }
          }

            scope.$watch(attrs.dynamic, function(html) {
                removeOld();
                curScope = scope.$new(); //creates child scope (not isolate)
                //probably should do some proper escaping here see $sce service
                curEle = angular.element( html );
                if( !curEle.length ){
                  curEle = angular.element('<span>'+html+'</span>');
                }
                $compile( curEle )(curScope);
                element.append( curEle );
            });
        }
    };
});

What you probably should do:

For some small templates like this you probably should consider putting them into the $templateCache (via put as shown in the plunkr below) so that any request for the template can automatically load it. You also have to consider some other things like 'is the html properly sanitized?' or 'do I want my content animated properly?'. These things are automatically handled in ng-include which it almost seems like your trying to copy.

Plunkr example

app.run(function( $templateCache ){
  $templateCache.put("btn_default.html", "Default content");
  $templateCache.put("btn_one.html", "<div ng-controller='FirstCtrl'>The div from first button</div>");
  $templateCache.put("btn_two.html", "<div ng-controller='SecondCtrl'>The div from second button</div>");
})

Now all you have to do is use the pre-built ng-include directive like so:

<div ng-controller="TestCtrl">
      <div class="btn btn-default" ng-click="firstButton()">First button</div>
      <div class="btn btn-default" ng-click="secondButton()">Second button</div>
      <div class="btn btn-default" ng-click="checkButton()">Check events</div>
      <div ng-include="dynamicContent"></div>
</div>

ng-include source to help you out

Hope this helps with a better understanding.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks a lot, really helps. The second part also helps but is the cache being cleared automatically ? (to avoid memory leaks). Also, your first plunkr is the same as the second, you may want to correct that for later readers
Good to hear, the links are different for me, not sure what's going on there. You should be able to use the $cacheFactory methods and use $templateCache.remove('btn_default.html') for example. Also don't forgot to mark an answer as the best one here on SO :).
I have one more question : if I'm not using a controller but just directives and {{}} in my compiled code will it be garbage-collected automatically or should I destroy it manually ?
I'm not sure what you meant by {{}}, but if you call $destroy on the scope and .remove() on any jquery element it will be removed correctly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.