6

I'm trying to solve a major problem I'm experiencing with angularJS in respect to using dynamically-added html that contains ng-controller.

Lets say that I want to add a div to the DOM that is a ng-controller, that blurts it's bound data out to the display. I can achieve this successfully as follows:

<!DOCTYPE html>
<html ng-app="myApp">
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script>
        var demoData = {
            'test1': 'one',
            'test2': 'two'
        };

        var myApp = angular.module('myApp', []);
        myApp.controller('TestCtrl', function ($scope) {
            $scope.demo = demoData;
        });
    </script>
</head>
<body>
    <div ng-controller="TestCtrl">
        {{demo}}
    </div>            
</body>
</html>

which outputs the following, as expected:

{"test1":"one","test2":"two"}

However, now lets say that the div actually has to be loaded dynamically, perhaps when a user presses a button. In this case, I'd replace the tag in the above example with the following:

<body>
    <button onclick="addDiv();">Click to add Div!</button>
    <script>
        function addDiv() {
            var newDiv = $('<div ng-controller="TestCtrl">{{demo}}</div>');
            $(document.body).append(newDiv);
        }
    </script>
</body>

which outputs the following when I click the button:

Click to add Div!
{{demo}}

So far, this makes sense; angular has already worked its way through the DOM, done it's thing, and finished. It's not been told about new stuff being added. So, if we look at the AngularJS manual, right at the bottom of this page, we find out how to tell it we've just added some stuff:

Sometimes you want to get access to the injector of a currently running Angular app from outside Angular. Perhaps, you want to inject and compile some markup after the application has been bootstrapped. You can do this using the extra injector() added to JQuery/jqLite elements. See angular.element.

This is fairly rare but could be the case if a third party library is injecting the markup.

In the following example a new block of HTML containing a ng-controller directive is added to the end of the document body by JQuery. We then compile and link it into the current AngularJS scope. var $div = $('{{content.label}}'); $(document.body).append($div);

angular.element(document).injector().invoke(function($compile) {
  var scope = angular.element($div).scope();
  $compile($div)(scope);
});

So... with this in mind, we update addDiv() function in our example as follows:

function addDiv() {
    var $newDiv = $('<div ng-controller="TestCtrl">{{demo}}</div>');
    $(document.body).append($newDiv);

    angular.element(document).injector().invoke(function ($compile) {
        var scope = angular.element($newDiv).scope();
        $compile($newDiv)(scope);
    });
}

And now when we run it, we should be golden right?
Nope.. we still get the following:

Click to add Div!
{{demo}}

sad panda is sad

Can you point out what I'm doing wrong, as no end of googling and reading the manual is helping, as everything seems to suggest I've got the code right!

Sk93
  • 3,676
  • 3
  • 37
  • 67
  • 4
    All your problems come from the fact that you're trying to use AngularJS as if it were jQuery. You shouldn't generate or load HTML dynamically with AngularJS. You should load data, as JSON. And depending on what the data contains, the static template, using ng-show, ng-if and other directives, should show/hide sections of the page. Navigating between pages should be done with a router. – JB Nizet Oct 19 '15 at 12:35
  • 2
    In an AngularJS app, an addDiv() function should not add a div. It should add an element to an array stored in the scope. An ng-repeat directive showing all the elements of this array would then detect the new element, and refresh the view accordingly. – JB Nizet Oct 19 '15 at 12:36
  • I understand what you're saying, but that doesn't really help me here, as I cannot really redesign our entire web app to use angular in that manner. All I am trying to achieve is to use angular within a handful of dynamically loaded pages within a much larger web app. If you look at the referenced AngularJS manual, it does actually state this should be possible? – Sk93 Oct 19 '15 at 12:42
  • It would work. Just use an ng-repeat iterating over urls, and ng-include each url in the view. When you want to add a div, add a URL to the array of URLs. – JB Nizet Oct 19 '15 at 12:43
  • @JBNizet - sorry - edited my earlier comment to better explain the problem I face. – Sk93 Oct 19 '15 at 12:44
  • Here's a plunkr showing a demo: http://plnkr.co/edit/kF20xMAXBVcEmPEZYgP8?p=preview – JB Nizet Oct 19 '15 at 12:52
  • @JBNizet Thanks, but lets assume I cannot make such drastic changes to the overarching product. If this was greenfield, then of course, I'd use Angular the 'right way', but in this particular instance, I'm constrained by pre-existing conditions I cannot change. With that (my content WILL be dynamically loaded) in mind, can you suggest how I can achieve this or what is wrong with my particular example as apposed to that offered by the angularJS manual? – Sk93 Oct 19 '15 at 12:56
  • My demo DOES dynamically load div1.html and div2.html, and they both use an angular controller. I wrote that working demo in 5 minutes, and you have a solution that doesn't work, so I don't see how it is a drastic change. – JB Nizet Oct 19 '15 at 12:59
  • @JBNizet your demo requires that an ng-repeat is added as a container for all the dynamic code and requires the MainCtrl to be responsible for loading the dynamic code. As mentioned, I cannot make changes to HOW the dynamic code is added. There is a very large, very complex "panelManager" javascript object that deals with loading, unloading, focus flow, update chaining and change control that would need to be completely rewritten to implement your suggestion. As I mentioned, this is outside of my control. I can run javascript when my dynamic html is loaded, but I cannot change HOW it's loaded. – Sk93 Oct 19 '15 at 13:06

2 Answers2

8

Your code works as is when invoked from a function called from an Angular ng-click.

Since you seem to really want to avoid using Angular, and thus want a traditional onclick event handler, you need to wrap the changes into a call to $scope.$apply(): http://plnkr.co/edit/EeuXf7fEJsbBmMRRMi5n?p=preview

angular.element(document).injector().invoke(function ($compile, $rootScope) {
    $rootScope.$apply(function() {
        var scope = angular.element($newDiv).scope();
        $compile($newDiv)(scope);
    });
});
ahsan ayub
  • 286
  • 2
  • 17
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • It's not that I want to avoid using Angular, it's that I cannot justify weeks of coding and testing of a core part of an existing product, to then implement functionality that will take maybe a half a day to write. The inclusion of $rootScope was the missing key. simply adding that made everything work perfectly. Thanks for the answer and help. – Sk93 Oct 19 '15 at 13:49
3

Are you trying to compile newDiv, when the variable is $newDiv?

Edit: I think this is because you are using the injector and compile outside of an angular controller.

I have setup a plunkr at http://plnkr.co/edit/utQvgyR7d0jBgx5aEpJa?p=preview to demonstrate this.

The example uses a controller on the body of the page:

app.controller('InitialController', function ($scope, $injector){
    $scope.newBtn = function () {
      var $newDiv = $('<div ng-controller="TestCtrl">{{demo}}</div>');

      $injector.invoke(function ($compile) {
          var div = $compile($newDiv);
          var content = div($scope);
          $(document.body).append(content);
      });
    };
});

This is responsible for dynamically adding content and compiling that content with a second controller:

app.controller('TestCtrl', function($scope) {
  $scope.demo = "This is the div contents";
});

The second controller is responsible for the logic related to that content.

Matt Waldron
  • 324
  • 1
  • 8
  • add this as a clarification question to the original question, but not as an answer. – Diana R Oct 19 '15 at 12:38
  • @Matt sadly not. That was just a typo in the question, rather than in my code. Updated the question to correct that :) – Sk93 Oct 19 '15 at 12:38