1

I have an defined a directive:


$(function () {
    angular.module(['myApp']).directive('rootSlider', function () {
        return {
            restrict: 'E',
            template: '<ul><li ng-repeat="item in items"><img ng-src="{{item.url}}" /></li></ul>',           
            scope: {
                items: '='
            },
            replace: true,
            compile: function ($templateElement, $templateAttributes) {
                return function ($scope, $element, $attrs) {

                    var $scope.items.length //expect 2, get 2
                    var numChildren $element.children().length //expect 2, get 0
                };
            }
        };
    });
});

Although the $scope.items property has 2 items, and in the final rendered DOM there are two <li> elements, in the link function $element does not yet have children.

At what point in the angular lifecycle can I get the completely rendered element (my intention is to use a jQuery slider plugin here).

The markup is

<root-slider items="ModelPropertyWithTwoItems"></root-slider>

UPDATE:

I was able to get this to work correctly with a combination of $watchCollection and $evalAsync.

$(function () {
    angular.module(['myApp']).directive('rootSlider', ['$timeout', function ($timeout) {
        return {
            restrict: 'E',
            template: '<ul class="bxslider"><li ng-repeat="item in items"><img ng-src="{{item.url}}" /></li></ul>',
            scope: {
                items: '='
            },
            compile: function ($templateElement, $templateAttributes) {
                return function ($scope, $element, $attrs) {
                    $scope.$watchCollection('items', function (newCollection, oldCollection) {
                        if ($scope.slider) {
                            $scope.$evalAsync(function () {
                                $scope.slider.reloadSlider();
                            });
                        }
                        else {

                            $scope.$evalAsync(function () {
                                $scope.slider = $element.find('.bxslider').bxSlider();
                            });
                        }

                    });

                };
            }
        };
    } ]);
});

Watch collection fires once on directive initialization (since it is a collection, you can't compare the newValue and oldValue, so I ended up adding the slider object to the $scope that gets created for the instance of the directive.

I use $evalAsync to defer execution of the jQuery code which (so far) is proving to avoid flicker on all browsers (it seems to run before $timeout(function(){}, 0).

The solution above could probably be simplified by just returning a link function (rather than a compile function, which turned out to be unneccessary).

Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75

2 Answers2

2

A common way to wait for the DOM to finish rendering in Angular is to use $timeout. Here's an explanation of the various options and here's another look at this.

So this should resolve this for you. Include $timeout in your directive:

angular.module(['myApp']).directive('rootSlider', function ($timeout) {

And then wrap your

$timeout(function() {
     var numChildren $element.children().length;
},0);

There has been a request for a more official feature supporting this and here's Angular's own Misko Hevery's comment before he closed the issue:

So that is a very complex question. The issue is that with data-binding there is no clear start/end to the update cycle, it is continues. I am sure that is not the answer you are looking for, but angular is not like other frameworks, and so it lives by different rules.

How would you imagine this would work? What kind of and when should you receive these notifications.

So far it looks like nobody has come up with a response to this question posted near the end by 3emad:

I'm just curious about $timeout down side

I've taken it as encouraging for this approach that nobody has yet posted a downside.

Community
  • 1
  • 1
KayakDave
  • 24,636
  • 3
  • 65
  • 68
  • This works, but it creates a situation where the unformatted elements are displayed temporarily. Seems like there would be a more framework specific hook for this. – Joe Enzminger Dec 19 '13 at 01:53
  • $timeout is used pretty commonly for this- but I totally get that it seems kludgy. I'm puzzled that you're seeing a flash that you didn't before as this should not delay loading of the image/template. Is something else waiting on length? Here's a demo with a 2 second delay in the timeout, but the template loads normally: jsfiddle.net/csnGL/2 – KayakDave Dec 19 '13 at 02:18
  • Marking this as the answer. The $watch solution didn't specifically address the problem (although it was helpful), and link to the feature request you posted led me to $evalAsync. Thanks! – Joe Enzminger Dec 19 '13 at 18:37
1

You might want to use $watch to catch up the change of model.

$scope.$watch('items', function(newValue) {
    // logic about items here...
}, true); // pass true since it should be an array
Banana-In-Black
  • 2,558
  • 1
  • 27
  • 33
  • Thanks. I had figured out something similar. I will update the question shortly, but I now use $watchCollection, and when it fires on initialization children().length is 2 (as expected). It still flickers on IE(9), but it looks like it works with other browsers. – Joe Enzminger Dec 19 '13 at 02:50