Wednesday, August 14, 2013

Angular - $compile: How it works, How to use it.

What is a "View"?

A view is HTML, plain and simple. A view is a DOM element and it's children. Views will contain markup and directives in that markup.

What Views are "Compiled"? 

All views are compiled by Angular. This is done by code that lives in the $compile service area of Angular.

How does view compilation work?

View compilation in Angular is some of the most ingenious functional programming I've seen in JavaScript. It works (very, very roughly) like this:

  1. Step into a DOM node.
  2. Loop through registered directives to see if this node contains any.
  3. For each directive found:
    1. Determine if the current scope is to be used, or if a child or isolated scope needs to be created.
    2. Create a function that, when called, will execute the directive's linking function against the appropriate scope from step 3.1.
    3. Append that function to a list of functions to be executed later.
  4. Does the current DOM node have children?
    1. YES: Step into each one and go to step 1 above.
    2. NO: That's the end of this branch.
  5. Return a function that will execute all functions created in Step 3.2. This is your compiled view.

Binding a View to a Scope

This is done when the compiled view is called and a scope is passed to it.

When does this occur?

  • When you angular app is initially bootstrapped.
  • During the processing of some directives, such as ng-repeat, it will be called on a subsection of your views.
  • When ng-view is updated after a routing event it's called on the incoming view.
  • When ng-include changes, it's called on the incoming view.
  • I'm sure there are many more places I'm not thinking of, but those are the big ones I can think of that happen OOTB.

When should I use $compile?


Truthfully? Almost never. It's going to be pretty rare that you should have to compile some HTML and process it as a view. Generally, you can just use directives that already exist to do whatever it is you think you need to do with $compile. But I suppose there are some edge cases where you may want to use it. Such as creating a completely custom repeater or something like that.

If you do find yourself needing to use compile. It should almost always be in a directive. Think about it: $compile is creating a function to wire up directives to a scope... in essence setting up all interactions between your app and the DOM. When you see "DOM", it's a directive. Directives are where we should be manipulating the DOM and nowhere else.

An example of compiling a view manually

/**
 * A weird directive that takes a space-seprated list of property names,
 * and prints them out as JSON.
 */
app.directive('whatIsInThese', ['$compile', function($compile) {
    return function(scope, elem, attrs) {
        //getting a list of space-separated property names 
        //from the attribute.
        var these = attrs.whatIsInThese.split(' '),

        //start creating an html string for our "view".
            html = '';

        //append a bunch of bound values from the list.
        angular.forEach(these, function(item) {
            html += '{{' + item + '| json}}\n\n';
        });

        //create an angular element. (this is still our "view")
        var el = angular.element(html),

        //compile the view into a function.
            compiled = $compile(el);

        //append our view to the element of the directive.
        elem.append(el);

        //bind our view to the scope!
        //(try commenting out this line to see what happens!)
        compiled(scope);
    };
}]);

Here's a plunker showing the above directive at work. Play around with it if you like. It's a strange example, and the same thing could be done in better, more maintainable ways, for sure. But it gets the idea accross, I think.

TL;DR Version

View compilation in Angular basically traverses the entire DOM tree of whatever node you give it, creating a single function, that when called will execute every linking function from every directive it finds in that DOM tree, with the appropriate scope, element, attributes and (optionally) controllers.

Let's not forget those those linking functions are what are setting up your $watches and event bindings and all of your ties to the DOM.


And as always: READ THE SOURCE!

Remember, Angular is open source, if you want to know more about it, don't read my stupid blog, read the source!!! It can be found on GitHub.

18 comments:

  1. Thanks, now I know how it works! :-)

    ReplyDelete
    Replies
    1. ... I'll think of something witty to say when I pick myself back up off the floor.

      I discovered the awesomeness in most of Angular's source while I was working on this: https://github.com/blesh/VolcanoJS

      It helped me learn how $compile works, why $digest exists, and a dozen other things. I tried to be original... but instead I just ended up learning things about Angular.

      ... feel free to fork (lol I kid, I kid)

      Delete
    2. lol! Misko - you should put Ben on the team. He's great with this stuff :D

      Ben - so volcanojs is a bust? Decidedly?

      Delete
    3. For now, yes. It started off like "Hey, I'm going to make my own framework, and it's going to be different. I want DI, plain JS object models, to use HTML mark up for binding between models and the DOM..." then after a while I was like "oh, that's why Angular did X this way..." and "Oh, I guess I can't do X, looks like that's why Angular did Y." ... so it was turning into a really small, buggy version of Angular.

      Delete
    4. Yeah -when we talked about it at Patrick's pub you said things were looking that way, but this was the first I saw of a conclusion.

      Delete
  2. Thanks for the in depth post. So if I don't need to $compile normal templates in Angular, when / how do they get compiled in the framework before they are rendered?

    ReplyDelete
  3. There are a few places where views are compiled. One is when your app starts up, the "bootstrap" if you will. One is when you change a route and the new view is loaded. The others occur when directives are processed, either if they have a template, or the do it explicitly. A templated directive will take it's template, either from a url or a string, and compile it for you as a view and append it to the view it's in. Other directives, like ng-repeat for example, will use a transclusion function along with $compile to take it's inner contents and create child-scoped views while iterating through items in an array or dictionary. I'm sure there are plenty more examples, but those are the ones that come to mind first.

    ReplyDelete
  4. So what if you have a service that needs to show a popup, for example. The service might be named Popup and relies on having a div class="popup" in the DOM for the lifecycle of the popup.

    Wouldn't it make the most sense to have that Popup service handle inserting and creating any directives needed in the DOM (i.e. by using $compile)? That way your controllers can avoid having to know anything about the DOM, but you can still instantiate directives on the fly.

    Or would there be a better way to do that?

    ReplyDelete
    Replies
    1. If you have to show a popup, you can just use scope and ng-show or ng-hide, or even ng-class to make the popup show. It gets a little more advanced when you have a popup you need to reuse that is going to return a value, like say a custom confirm directive or something... for that you're looking at possible a service and a directive working in concert.. but that would be a whole blog post in an of itself.

      Delete
  5. Hello, I have this jquery datatable and I put a button for every row that the source generates, the thing is that I try to use ng-click for the event of that button but nothing happens, what would be the right way to implement $compile in this case, if it's not a lot of a bother, thanks

    ReplyDelete
    Replies
    1. It's hard to say without seeing some code. You should post the question on StackOverflow. It's really a better format for that sort of thing.

      Delete
  6. Clear, concise, accurate. Love it! Many, many thanks!

    ReplyDelete
  7. This is by far the best explanation I've read so far. And it's pretty simple too. thx!

    ReplyDelete
  8. Good post, but in your example, you don't have to use the the $complie service at all, plus that, there is issue that, the json content displayed will not be updated when the model changed. You can simply change the content in the element in the compile function, but the compile function does not return a link function, it return nothing! $compile service will automatically compile the content for you. Here is the code


    app.directive('whatIsInThese', ['$compile', function($compile) {

    return {
    compile: function ($elem, attrs, transclude) {
    //getting a list of space-separated property names
    //from the attribute.
    var these = attrs.whatIsInThese.split(' '),

    //start creating an html string.
    html = '<pre>';

    //append a bunch of bound values from the list.
    angular.forEach(these, function(item) {
    html += '{{' + item + '| json}}\n\n';
    });

    html += '</pre>';

    $elem.html(html);
    }
    };

    }]);

    check this http://plnkr.co/edit/yS6Gac?p=preview

    ReplyDelete
    Replies
    1. I know I don't have to use the $compile function here, I was just creating a contrived example to show it being used. Maybe it's not the best example, but it's *an* example, which is all I was going for.

      Also, I'm not quite sure what you mean by "the json content displayed will not be updated when the model is changed". Here you can see it does update: http://plnkr.co/edit/76KvRveswe2v7QcjIaHl?p=preview

      Your example obviously works as well, but wouldn't have helped my explanation of the $compile service. It also might not be as apparent to the next developer what was happening, as the compile option in directives is a confusing and little-used thing. Generally, I only use that if I need to get to the transclusion function for some reason.

      Anyway, thanks for the feedback.

      Delete
  9. Ben, As a beginner to Angularjs in a hurry to make my project requirements to meet, I did the coding in controller as shown below :


    var chartName = "xyz1";

    template = ;

    angular.element(document.querySelectorAll('.snap-content')).append($compile(template)($scope));

    This template compilation works fine and generates c3 charts on the UI, everything is working fine for me in my project perspective. But after reading some of the blogs of this kind, some doubt is arising in me, that the way I am compiling the template is a bad practice. What is the best practice that you suggest for me to replace my above compile template code. Right now I am using angular js 1.2.16, if I migrate to angularjs1.3.X, will it cause any problems. Can safely switch to angularjs1.3.X with the above I have right now? Thanks for the nice article.

    ReplyDelete
    Replies
    1. To answer your question about migration: you should be fine, but just try it. It's literally one script tag you'll be changing to see what breaks. Ideally you have unit tests to run too.

      For the "best practice" question. You should ask this in Stack Overflow, these comments aren't great for answering technical questions. But generally, any practice that works that isn't creating tech debt is a "best practice". That said, it doesn't mean there isn't a better way. Throw it up on Stack Overflow and see what you learn. Link the question here if you like.

      Delete
  10. Very insightful post, thanks for sharing.

    ReplyDelete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)