Friday, June 6, 2014

Loading dynamic Angular content via JQuery

Sometimes you cannot start a project from scratch. Sometimes you're just stuck with crap created by others. And often you want to improve that.

Say, you're stuck with a web application full with JQuery (created in some not to be named country in South Asia starting with 'I' and ending with 'ndia') and you are really trying to make something better of it. You don't understand anything of this Indian JQuery magic, so you gradually replace it with some clear, tested! and short Angular code.

You created a nice Angular component, added it to your application, but somehow it works in one page but not on another. After some debugging you notice that when the component doesn't work, it is in some html fragment which is dynamically loaded using 'ajaxSubmit' function from JQuery's Form plugin. This dynamic loading is outside of Angular's scope, so Angular does not know this is happening and therefore cannot do any Angular stuff on the new fragment.

The trick to solve this, is to 'compile' the new html fragment using Angular and then insert it into the DOM. This gives Angular the opportunity to activate any Angular components in the fragment. Now, the big question is, how to do this 'compiling' ? Let's go through it step by step.

Before

This was how it was before
$(this).ajaxSubmit({target:'#body',url:'/my/mvc/url'});
This replaced the content of the html tag with id 'body' (so not the contents '<body>' tag itself!) with the html returned by the url '/my/mvc/url'.

Calling an Angular function from plain JavaScript

Somehow the returned html fragment must be compiled, so we need a function which call's Angular's compiler and pass the new html. But, JQuery cannot access Angular's providers or services, so the function must be in Angular. For JQuery to be able to access the function, you must put it somewhere where JQuery can reach it. At first I put the function on Angular's $rootScope like this:
app.run(function($rootScope) {

    $rootScope.refresh = function() {
        console.log('refreshing body');
    }
});
You can then reach this function by getting the rootScope like this:
var rootScope = angular.element(document).scope();
rootScope.refresh();
You can even get the scope of an element by adding a find('') before the 'scope()':
var elementScope = angular.element(document).find('elemId').scope();
elementScope.refresh();
This also works because an Angular Scope inherit from it's parent Scope. The 'app.run' function is called when the page is loaded so it creates the function on the object at that time.

I did not like this solution, because you have to get the scope first before you're able to call the wanted function. So, I opted for another solution to put the function on the Window object so the function would be globally available. In Angular you can just add the $window argument to a function and then create a new function on that object which can then be called from anywhere in JavaScript.
app.run(function($window) {

    $window.refresh = function() {
        console.log('refreshing body');
    }
});

Compile the new html fragment

Angular has a $compile provider with which you can compile the html fragment.
Using it is as easy as adding the '$compile' argument to the run function. The html can be passed as an argument and the return value is the compiled html which you can then put into the dom.

Adding it to the dom

Somehow adding the html to the dom correctly wasn't very straight forward. An element supports both a 'replaceWith' and a 'html' method to modify is contents, but using:
elem.replaceWith( $compile( newHtml )(elemScope))
displayed the html wrong.
elem.html( $compile( newHtml )(elemScope))
This did not work at all.
Finally:
$(target)['html']( $compile( newHtml )(targetScope));
this seemed to work fine.
For me, it all looked like the same code, just written differently, but apparently it makes a difference.

Calling compile function from ajaxSubmit

Initially, the 'ajaxSubmit' call would 1) call the url and 2) replace the content of the provided target with the new html. Because we have to compile the html before it can be injected, a success handler is required to capture the retrieved data. Then it can be compiled and added to the dom.
$(this).ajaxSubmit({
  // no target! Angularfy function will put code into dom.
  url:'&lt;@core.basePath/&gt;my/mvc/url',
  success: function(data) {
    // call the compile function and add compiled html to dom.
  }
});

The full solution

Putting everything together, this is the solution I ended up with. The 'angularfy' function compiles the html and adds it to the provided target.
app.run(function($window, $compile) {

    /*
     * Function to 'Angular-fy' dynamically loaded content
     * by JQuery. This compiles the new html code and injects it
     * into the DOM so Angular 'knows' about the new code.
     */
    $window.angularfy = function(target, newHtml) {
        // must use JQuery to query for element by id.
        // must wrap as angular element otherwise 'scope' function is not defined.
        var targetScope = angular.element($(target)).scope();

//        elem.replaceWith( $compile( newHtml )(elemScope)); Displays html wrong.
//        elem.html( $compile( newHtml )(elemScope)); Does not work.
        $(target)['html']( $compile( newHtml )(targetScope)); // Does work!
        targetScope.$apply();
    }
});
In the web page, the ajaxSubmit calls the 'angularfy' function from the success handler.
$(this).ajaxSubmit({
  // no target! Angularfy function will put code into dom.
  url:'&lt;@core.basePath/&gt;my/mvc/url',
  success: function(data) {
    angularfy('#body', data); // to notify Angular of new html code in dom.
  }
});

Happy Angular-JQuery-ing ! ;-)

Btw: I am a hardcore Java/Scala backender and do not pretend to be a frontend/Angular guru. This solution might not be the nicest one, but for me, at this moment, it works. If you know any improvements, let me know.