213

I have a very boiled down version of what I am doing that gets the problem across.

I have a simple directive. Whenever you click an element, it adds another one. However, it needs to be compiled first in order to render it correctly.

My research led me to $compile. But all the examples use a complicated structure that I don't really know how to apply here.

Fiddles are here: http://jsfiddle.net/paulocoelho/fBjbP/1/

And the JS is here:

var module = angular.module('testApp', [])
    .directive('test', function () {
    return {
        restrict: 'E',
        template: '<p>{{text}}</p>',
        scope: {
            text: '@text'
        },
        link:function(scope,element){
            $( element ).click(function(){
                // TODO: This does not do what it's supposed to :(
                $(this).parent().append("<test text='n'></test>");
            });
        }
    };
});

Solution by Josh David Miller: http://jsfiddle.net/paulocoelho/fBjbP/2/

Flip
  • 6,233
  • 7
  • 46
  • 75
PCoelho
  • 7,850
  • 11
  • 31
  • 36

7 Answers7

262

You have a lot of pointless jQuery in there, but the $compile service is actually super simple in this case:

.directive( 'test', function ( $compile ) {
  return {
    restrict: 'E',
    scope: { text: '@' },
    template: '<p ng-click="add()">{{text}}</p>',
    controller: function ( $scope, $element ) {
      $scope.add = function () {
        var el = $compile( "<test text='n'></test>" )( $scope );
        $element.parent().append( el );
      };
    }
  };
});

You'll notice I refactored your directive too in order to follow some best practices. Let me know if you have questions about any of those.

Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • 35
    Awesome. It works. See, these simple and basic examples are the ones that should be shown in angulars' docs. They start off with complicated examples. – PCoelho Mar 07 '13 at 20:48
  • `$compile` often isn't used for cases this simple as there are usually easier ways to accomplish this than reaching for `$compile`. – Josh David Miller Mar 07 '13 at 20:56
  • 1
    Thanks, Josh, this was really useful. I made a tool in Plnkr that we are using in a new CoderDojo to help kids learn how to code, and I just extended it so that I can now use Angular Bootstrap directives like datepicker, alert, tabs, etc. Apparently I msssed something up and right now it's only working in Chrome though: http://embed.plnkr.co/WI16H7Rsa5adejXSmyNj/preview – JoshGough Jun 30 '13 at 19:31
  • 3
    Josh - what's an easier way to accomplish this without using `$compile`? Thanks for your answer by the way! – doubleswirve Jan 28 '14 at 00:25
  • 3
    @doubleswirve In this case, it would be far easier to just use ngRepeat. :-) But I assume you mean adding new directives dynamically to the page, in which case the answer is no - there's no simpler way because the `$compile` service is what wires directives up and hooks them into the event cycle. There's no way around `$compile`ing in a situation like this, but in most cases another directive like ngRepeat can accomplish the same job (so ngRepeat is doing the compiling for us). Do you have a specific use case? – Josh David Miller Jan 29 '14 at 19:09
  • Thanks Josh! I'm really just trying to get a handle on the best practices so to speak. For a current project we have a variable set of form controls that are dependent upon user permissions. Instead of loading them all on page load we wanted them to be selectable (e.g., search by name would add a custom input directive to the form) so `$compile` seemed like a good option. I thought about using the `ng-show` directive but it seems that loading all the possible control directives might be a waste. Thanks again! – doubleswirve Jan 29 '14 at 19:19
  • Hi, would you please provide ideas on my new proposed API to make programmatically adding directives a simpler process? https://github.com/angular/angular.js/issues/6950 Thanks! – trusktr Apr 05 '14 at 04:18
  • 2
    Shouldn't the compile happen in the prelink stage? I think that the controller should only contain non-DOM, unit-testable code, but I'm new to the link/controller concept so I'm unsure myself. Also, one basic alternative is ng-include + partial + ng-controller since it will act as a directive with *inherited* scope. – Marcus Rådell Oct 07 '14 at 15:15
  • 1
    Could you elaborate why you change link function to a controller in this case? – Downhillski Mar 17 '16 at 13:29
  • How would this solution change if you needed to add a DOM element without one being there already? As in, could you use just JavaScript to create every directive from scratch? – sahilkmr78 Jul 29 '16 at 13:53
  • @sahilkmr78 That's exactly what we did above; we created an element that had a directive, compiled it, and attached it to the DOM. If you mean arbitrarily adding DOM nodes anywhere in DOM tree like you do with jQuery, that would be very un-angular. Check out my other post: http://stackoverflow.com/questions/14994391/thinking-in-angularjs-if-i-have-a-jquery-background/15012542#15012542 – Josh David Miller Jul 29 '16 at 17:51
  • Similar problem i am facing, Can you help me here http://stackoverflow.com/questions/38821980/how-to-write-directive-on-class-in-angular-js – pandu das Aug 11 '16 at 07:07
  • @JoshDavidMiller yes I do have a specific use case: http://stackoverflow.com/questions/41004055/angular-dynamically-fetch-and-include-html-templates. Should I go with `ng-repeat`? If so, I don't know how to dynamically append a `canvas` element to an element not yet created in `ng-repeat` that it is within – user3871 Dec 06 '16 at 20:56
78

In addition to perfect Riceball LEE's example of adding a new element-directive

newElement = $compile("<div my-directive='n'></div>")($scope)
$element.parent().append(newElement)

Adding a new attribute-directive to existed element could be done using this way:

Let's say you wish to add on-the-fly my-directive to the span element.

template: '<div>Hello <span>World</span></div>'

link: ($scope, $element, $attrs) ->

  span = $element.find('span').clone()
  span.attr('my-directive', 'my-directive')
  span = $compile(span)($scope)
  $element.find('span').replaceWith span

Hope that helps.

Sharikov Vladislav
  • 7,049
  • 9
  • 50
  • 87
deadrunk
  • 13,861
  • 4
  • 29
  • 29
  • 4
    Don't forget to remove the original directive in order to prevent Maximum call stack size exceeded error. – SRachamim Mar 26 '14 at 13:27
  • Hi, would you please provide ideas on my new proposed API to make programmatically adding directives a simpler process? https://github.com/angular/angular.js/issues/6950 Thanks! – trusktr Apr 05 '14 at 04:14
  • I wish in 2015 we wouldn't have limits in call stack size. :( – psycho brm Jul 31 '15 at 13:57
  • 4
    The `Maximum call stack size exceeded` error always happens because of infinite recursion. I've never seen an instance where increasing the stack size would solve it. – Gunchars Jul 31 '15 at 18:13
  • Similar problem i am facing, Can you help me here http://stackoverflow.com/questions/38821980/how-to-write-directive-on-class-in-angular-js – pandu das Aug 11 '16 at 07:07
45

Dynamically adding directives on angularjs has two styles:

Add an angularjs directive into another directive

  • inserting a new element(directive)
  • inserting a new attribute(directive) to element

inserting a new element(directive)

it's simple. And u can use in "link" or "compile".

var newElement = $compile( "<div my-diretive='n'></div>" )( $scope );
$element.parent().append( newElement );

inserting a new attribute to element

It's hard, and make me headache within two days.

Using "$compile" will raise critical recursive error!! Maybe it should ignore the current directive when re-compiling element.

$element.$set("myDirective", "expression");
var newElement = $compile( $element )( $scope ); // critical recursive error.
var newElement = angular.copy(element);          // the same error too.
$element.replaceWith( newElement );

So, I have to find a way to call the directive "link" function. It's very hard to get the useful methods which are hidden deeply inside closures.

compile: (tElement, tAttrs, transclude) ->
   links = []
   myDirectiveLink = $injector.get('myDirective'+'Directive')[0] #this is the way
   links.push myDirectiveLink
   myAnotherDirectiveLink = ($scope, $element, attrs) ->
       #....
   links.push myAnotherDirectiveLink
   return (scope, elm, attrs, ctrl) ->
       for link in links
           link(scope, elm, attrs, ctrl)       

Now, It's work well.

Riceball LEE
  • 1,541
  • 18
  • 18
  • 1
    Would love to see a demo of inserting a new attribute to element, in vanilla JS if possible - I'm missing something... – Patrick Nov 21 '13 at 12:08
  • the real example of inserting a new attribute to element is here(see my github): https://github.com/snowyu/angular-reactable/blob/master/src/i-reactable.coffee – Riceball LEE Dec 06 '13 at 08:45
  • 1
    Doesn't help honestly. This is how I ended up solving my problem though: http://stackoverflow.com/a/20137542/1455709 – Patrick Dec 06 '13 at 10:52
  • Yes, this case is the inserting an attribute directive into another directive, not the inserting element in template. – Riceball LEE Dec 08 '13 at 03:03
  • What's the reasoning behind doing it outside of the template? – Patrick Dec 08 '13 at 05:28
  • Hi, would you please provide ideas on my new proposed API to make programmatically adding directives a simpler process? https://github.com/angular/angular.js/issues/6950 Thanks! – trusktr Apr 05 '14 at 04:21
  • This worked in angular 1.2, but stopped in angular 1.3. – httpete Mar 07 '16 at 22:27
9
function addAttr(scope, el, attrName, attrValue) {
  el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
user1212212
  • 1,311
  • 10
  • 6
5

Josh David Miller is correct.

PCoelho, In case you're wondering what $compile does behind the scenes and how HTML output is generated from the directive, please take a look below

The $compile service compiles the fragment of HTML("< test text='n' >< / test >") that includes the directive("test" as an element) and produces a function. This function can then be executed with a scope to get the "HTML output from a directive".

var compileFunction = $compile("< test text='n' > < / test >");
var HtmlOutputFromDirective = compileFunction($scope);

More details with full code samples here: http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs

Muhammad Hassaan
  • 7,296
  • 6
  • 30
  • 50
Danial Lokman
  • 131
  • 1
  • 5
5

The accepted answer by Josh David Miller works great if you are trying to dynamically add a directive that uses an inline template. However if your directive takes advantage of templateUrl his answer will not work. Here is what worked for me:

.directive('helperModal', [, "$compile", "$timeout", function ($compile, $timeout) {
    return {
        restrict: 'E',
        replace: true,
        scope: {}, 
        templateUrl: "app/views/modal.html",
        link: function (scope, element, attrs) {
            scope.modalTitle = attrs.modaltitle;
            scope.modalContentDirective = attrs.modalcontentdirective;
        },
        controller: function ($scope, $element, $attrs) {
            if ($attrs.modalcontentdirective != undefined && $attrs.modalcontentdirective != '') {
                var el = $compile($attrs.modalcontentdirective)($scope);
                $timeout(function () {
                    $scope.$digest();
                    $element.find('.modal-body').append(el);
                }, 0);
            }
        }
    }
}]);
ferics2
  • 5,241
  • 7
  • 30
  • 46
4

Inspired from many of the previous answers I have came up with the following "stroman" directive that will replace itself with any other directives.

app.directive('stroman', function($compile) {
  return {
    link: function(scope, el, attrName) {
      var newElem = angular.element('<div></div>');
      // Copying all of the attributes
      for (let prop in attrName.$attr) {
        newElem.attr(prop, attrName[prop]);
      }
      el.replaceWith($compile(newElem)(scope)); // Replacing
    }
  };
});

Important: Register the directives that you want to use with restrict: 'C'. Like this:

app.directive('my-directive', function() {
  return {
    restrict: 'C',
    template: 'Hi there',
  };
});

You can use like this:

<stroman class="my-directive other-class" randomProperty="8"></stroman>

To get this:

<div class="my-directive other-class" randomProperty="8">Hi there</div>

Protip. If you don't want to use directives based on classes then you can change '<div></div>' to something what you like. E.g. have a fixed attribute that contains the name of the desired directive instead of class.

Gábor Imre
  • 5,899
  • 2
  • 35
  • 48
  • Similar problem i am facing, Can you help me here http://stackoverflow.com/questions/38821980/how-to-write-directive-on-class-in-angular-js – pandu das Aug 11 '16 at 07:08
  • OMG. it took 2 days to find this $compile... thanks friends.. it works best... AJS you rock.... – Srinivasan Jun 16 '17 at 07:57