0

I'm creating two Angular directives, fooContainer and foo, where the fooContainer will load one or more foo elements and render them inside its container.

Now I also want to attach some JavaScript events to the foo directives compiled HTML from the fooContainer directives link function since the container is supposed to be responsible for some things like dragging the compiled foo elements around.

Since $compile is asynchronous when compiling directives with templateUrl the HTML isn't available directly after calling $compile(html)(scope), however if I use a timeout for about a second the HTML is rendered in and I can interact with it.

This isn't optimal. Does $compile expose an event or promise I can use to be notified when all HTML is rendered?

Here is a Plunker that describes my problem, http://plnkr.co/edit/coYdRqCsysV4txSFZ6DI?p=preview

Viktor Elofsson
  • 1,581
  • 2
  • 13
  • 20
  • 1
    Why don't you attach the events in the compile/link functions of `foo`? If you need to provide some logic from `fooContainer` as a callback, you can `require` `fooContainer`'s controller. – link Jun 04 '14 at 12:57
  • You can safely attach events to the element in the directive's link function, but only for the directive's children. The directive's parent haven't been linked yet. – Michael Kang Jun 04 '14 at 13:21

4 Answers4

1

Approaches in order of preference:

1) Follow pattern of simmi simmi whenever you can and use angular (ng-...) approach. This is most reliable.

1.5) UPDATE: Liquinaut 'attribute directive' approach below seems valid - I've only used it in a quick demo POC and it worked fine. Assuming this survives more complex usage I would prefer over the watch/jquery option 2 below. Please note however that the $last is specifically for ng-repeat. If you are injecting a $compile chunk of non-repeating markup as per the OP then you need to drop the $last check. And to be clear this requires you to add the attribute post-render to the element you are waiting to render i.e. (per OP plunker)

var el = $compile('<foo><div class="hide" post-render>...

with directive:

.directive('postRender', function() {
    return function(scope, element, attrs) {
        //your code here
        element.hide();
    };
});

I've forked the original plunkr and generalized the approach to allow passing a generic callback: http://plnkr.co/edit/iUdbn6zAuiX7bPMwwB81?p=preview

NOTE: This approach only works for basic activities on the element compiled in. If your compile string has angular variable interpolation e.g. {{value}} and you rely on these being resolved in the callback it won't work. They are not resolved at this time. This is also true if the postRender directive is rewritten with an explicit link function. Option 2 below works fine for these cases as it pushes resolution to at least the next digest.

2) I've found watching the DOM very reliable even in very complex apps (although performance should as always be monitored). Replace your el.find('.hide').hide(); line with this block:

scope.$watch(
  function() { return element.find('.hide').length > 0;},
  function(newVal, oldVal) {
    if (newVal) {
      //DO STUFF
      el.find('.hide').hide();
    }
  }
);

I wouldn't be comfortable using this in a tight loop but for one off usage on directive instantiation (assuming you aren't creating a 1000 of them!) it seems reasonable - I used this approach for ng/ui-slider integration etc

3) pixelbits approach also good architectural approach if you are building something more complex (and for reusable components) but beware the extra scope that gets created if you are using transclude (e.g. nested directives) it will be $$nextSibling that gets the 'emit'. See: here

BTW: if just want a quick way to do drag and drop see: http://codef0rmer.github.io/angular-dragdrop/#/

Community
  • 1
  • 1
nempnett
  • 225
  • 2
  • 7
0

The directive fires a postRender event:

fooContainer.directive('postRender', function() {
    return function(scope, element, attrs) {
        if (scope.$last){
            //your code here
        }
    };
});

Hope that helps!

Liquinaut
  • 3,759
  • 1
  • 21
  • 17
  • Can you elaborate on usage here (example?). I would be delighted if this actually works and we could ditch jQuery based watches... – nempnett Jun 05 '14 at 08:25
  • I've created a fiddle here: http://jsfiddle.net/8Gjgu/1/ In this case we use angularjs for a live faq search and filtering including text highlighting while typing. Since this is a complete used case, I didn't remove much of the functionality. The faqs are shown after angularjs has finished rendering. On the live site this is done to replace the markup that is rendered by the cms for SEO's sake. – Liquinaut Jun 24 '14 at 14:18
  • This seems like a good approach - I've only used it in a quick demo POC and it worked fine. I've updated my answer to add this approach which I think I prefer over the watch/jquery option 2 in my answer above if it survives more complex usage. Please note however that the $last is specifically for ng-repeat. If you are injecting a $compiled chunk of non-repeating markup as per the OP then you need to drop the $last check. – nempnett Jun 30 '14 at 22:59
0

http://plnkr.co/edit/f4924y6GW7rAMItqVR0L?p=preview

    .directive('fooContainer', function($compile, $timeout) {
  return {
    link: function(scope, element, attributes) {
     console.log('link called');
        var el = $compile('<foo><div class="hide" >I should always be hidden.</div><div class="hideDelay" ng-show="visiblity">I should be hidden after 1 second.</div></foo>')(scope);
       element.append(el);

         scope.visiblity=false;


    },
    restrict: 'E',
    template: '<div class="fooContainer"></div>'
  }
});

why Dont you try using ng-show/ng-hide

anam
  • 3,905
  • 16
  • 45
  • 85
0

You can safely attach events to the element in the directive's link function, but only for the directive's children. The directive's parent haven't been linked yet.

So within fooContainer's link function, you know that foo has already been compiled and linked, and it's safe to attach events.

If foo needs to be notified once fooContainer is linked, you can use some form of inter-directive communication. i.e. $scope.$emit. If you want to attach events to foo, you can also use a jquery selector inside fooContainer's link function.

According to Angular documentation:

templateUrl

Same as template but the template is loaded from the specified URL. 
Because the template loading is asynchronous the compilation/linking
is suspended until the template is loaded

This means your template is already loaded by the time your link function executes. A promise object is not needed.

Michael Kang
  • 52,003
  • 16
  • 103
  • 135