85

I am working on a Ajax app using both jQuery and AngularJS.

When I update content (which contains AngularJS bindings) of a div using jQuery's html function, the AngularJS bindings doesn't work.

Following is code of what I am trying to do:

$(document).ready(function() {
  $("#refreshButton").click(function() {
    $("#dynamicContent").html("<button ng-click='count = count + 1' ng-init='count=0'>Increment</button><span>count: {{count}} </span>")
  });
});
</style><script src="http://docs.angularjs.org/angular-1.0.1.min.js"></script><style>.ng-invalid {
  border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="">
  <div id='dynamicContent'>
    <button ng-click="count = count + 1" ng-init="count=0">
        Increment
      </button>
    <span>count: {{count}} </span>
  </div>


  <button id='refreshButton'>
    Refresh
  </button>
</div>

I have dynamic content inside a div with the ID #dynamicContent, and I have a refresh button that would update contents of this div when refresh is clicked. Increment works as expected if I don't refresh the content, but after I refresh, the AngularJS binding stops working.

This may not be valid in AngularJS, but I initially built application with jQuery and started using AngularJS later on so I can't migrate everything to AngularJS. Any help with getting this working in AngularJS is greatly appreciated.

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Raja
  • 1,317
  • 3
  • 16
  • 22
  • 1
    Is there any particular reason for using JQuery for this functionality? As this is nicely and easily covered by angular: – pkozlowski.opensource Aug 02 '12 at 05:48
  • 3
    This is just a simplified version of my real use case to show the problem. In actual application dynamic content is generated by grails taglib which is passed over to jquery as html. So I can't port all logic in grails taglib over to angularjs to make it pure angularjs. – Raja Aug 02 '12 at 14:57
  • 1
    @pkozlowski.opensource, that link is dead? Also you should join http://outdoors.stackexchange.com/ :) – Liam Feb 14 '14 at 11:09

3 Answers3

116

You need to call $compile on the HTML string before inserting it into the DOM so that angular gets a chance to perform the binding.

In your fiddle, it would look something like this.

$("#dynamicContent").html(
  $compile(
    "<button ng-click='count = count + 1' ng-init='count=0'>Increment</button><span>count: {{count}} </span>"
  )(scope)
);

Obviously, $compile must be injected into your controller for this to work.

Read more in the $compile documentation.

Noah Freitas
  • 17,240
  • 10
  • 50
  • 67
  • 2
    Thanks Noah, compile is working when I do it in a controller method and directly bind that controller method to click event of button. Here is working [example](http://jsfiddle.net/fABdD/10/). But I in my actual app, I had to call the controller method from a different jquery function. so I grabbed reference to scope object and called refresh method to recompile which doesn't work. Here is [example](http://jsfiddle.net/fABdD/12/). Can you help with getting this example to work. – Raja Aug 02 '12 at 20:13
  • 1
    @Raja When you call angular methods/expression outside of the angular framework you should call [$scope.$apply()](http://docs.angularjs.org/api/ng.$rootScope.Scope) to perform proper scope life-cycle of exception handling, executing watches, etc. http://jsfiddle.net/fABdD/14/ – Artem Andreev Aug 03 '12 at 04:59
  • 1
    @ArtemAndreev exactly right. @Raja you could also move the call to `$scope.$apply()` into the `refresh()` function itself if it makes sense for your architecture: http://jsfiddle.net/nfreitas/8xkeh/1/ – Noah Freitas Aug 03 '12 at 15:21
  • 3
    The link to `angular-1.0.1.min.js` in the original examples was 404. Here are updated working versions of @ArtemAndreev-s example: http://jsfiddle.net/pmorch/fABdD/186/ and @NoahFreitas example: http://jsfiddle.net/pmorch/8xkeh/43/ – Peter V. Mørch Dec 19 '13 at 00:21
  • Works great when you use loaded ajax content. The directive execute successfully with compile. – Misters May 02 '14 at 01:54
  • @NoahFreitas How do you get access to the $compile if your not using a directive to load the dynamic content? – jwize Jun 05 '14 at 10:56
  • @jwize You can inject it into the Angular component where you are processing the dynamic content (it doesn't have to be a directive.) Or if you are processing the content outside of angular, you can get a reference to the injector like you did in your second example. – Noah Freitas Jun 06 '14 at 18:49
  • @NoahFreitas. I was able to get my content to render from the template file but the {{ fields }} didn't get rendered when I tested. – jwize Jun 07 '14 at 08:13
  • 1
    Using $compile and manipulating DOM within a controller will lead you down a path of having untestable code. Directives should be used for changing the DOM never a controller. – Enzey Dec 03 '14 at 16:08
56

Another Solution in Case You Don't Have Control Over Dynamic Content

This works if you didn't load your element through a directive (ie. like in the example in the commented jsfiddles).

Wrap up Your Content

Wrap your content in a div so that you can select it if you are using JQuery. You an also opt to use native javascript to get your element.

<div class="selector">
    <grid-filter columnname="LastNameFirstName" gridname="HomeGrid"></grid-filter>
</div>

Use Angular Injector

You can use the following code to get a reference to $compile if you don't have one.

$(".selector").each(function () {
    var content = $(this);
    angular.element(document).injector().invoke(function($compile) {
        var scope = angular.element(content).scope();
        $compile(content)(scope);
    });
});

Summary

The original post seemed to assume you had a $compile reference handy. It is obviously easy when you have the reference, but I didn't so this was the answer for me.

One Caveat of the previous code

If you are using a asp.net/mvc bundle with minify scenario you will get in trouble when you deploy in release mode. The trouble comes in the form of Uncaught Error: [$injector:unpr] which is caused by the minifier messing with the angular javascript code.

Here is the way to remedy it:

Replace the prevous code snippet with the following overload.

...
angular.element(document).injector().invoke(
[
    "$compile", function($compile) {
        var scope = angular.element(content).scope();
        $compile(content)(scope);
    }
]);
...

This caused a lot of grief for me before I pieced it together.

jwize
  • 4,230
  • 1
  • 33
  • 51
  • 1
    Thanks as the above code worked for me scope and the explanation about compile being available. sometimes you miss the easy parts :) – Abs Aug 12 '14 at 11:52
  • I had to $digest after the compile. – Knu Sep 12 '14 at 13:14
  • 2
    Absolute lifesaver, I added a conditional check for angular in my code which allows it to use all the angular goodness when plugged into an angular app – LiamRyan Dec 03 '14 at 10:43
18

Addition to @jwize's answer

Because angular.element(document).injector() was giving error injector is not defined So, I have created function that you can run after AJAX call or when DOM is changed using jQuery.

  function compileAngularElement( elSelector) {

        var elSelector = (typeof elSelector == 'string') ? elSelector : null ;  
            // The new element to be added
        if (elSelector != null ) {
            var $div = $( elSelector );

                // The parent of the new element
                var $target = $("[ng-app]");

              angular.element($target).injector().invoke(['$compile', function ($compile) {
                        var $scope = angular.element($target).scope();
                        $compile($div)($scope);
                        // Finally, refresh the watch expressions in the new element
                        $scope.$apply();
                    }]);
            }

        }

use it by passing just new element's selector. like this

compileAngularElement( '.user' ) ; 
shyammakwana.me
  • 5,562
  • 2
  • 29
  • 50
  • Thanks, this saved my life with a package that used UIKit for its dialogs, with HTML generated on the fly inside a $scope function. As a side note, I had to change the selector of the $target to suit my needs, in my case $["data-ng-controller]"), and comment out $scope.apply(); since this function was called already in an angular callback (so apply was already taking place) – zontar May 06 '17 at 09:25
  • @zontar you can also make `target` dynamic via taking it from argument. – shyammakwana.me Apr 11 '18 at 18:51