hgoebl hgoebl - 6 months ago 32
Javascript Question

Is $timeout the only/recommended way to avoid jQuery plugin rendering problems in AngularJS directives?

I'm porting a jQuery webapp to AngularJS (<- beginner!).

To integrate bxSlider along with some templating stuff, I wrote following directive:

[Edit] better have a look at jsFiddle jsfiddle.net/Q5AcH/2/ [/Edit].

angular.module('myApp')
.directive('docListWrapper', ['$timeout', function ($timeout) {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {

$timeout(function () {
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider(); // <-- jQuery plugin doing heavy DOM manipulation
}, 100); // <-------------- timeout in millis
}
};
}]);


Without
$timeout
there is the problem that bxSlider cannot calculate sizes of the freshly created elements or doesn't find them at all.

I'm a bit concerned that using a long timeout-value might cause flickering while using a short value could cause problems on slow machines.

In my real application (of course with more data and more sections than in the jsFiddle) I observed something strange:

When I play around with the timeout value, using 10 or more milliseconds is enough so the jQuery plugin bxSlider finds a complete DOM. With less time waiting (9 millis or less), the plugin is not able to wrap the
<ul>
as it should.

But the problem of a very nasty flickering is still present.

In the fiddle, probably due to a smaller DOM, the flickering is not visible in Chrome + Firefox, only with Internet Explorer 10.

I don't want to rely on empiric values for
$timeout
which could be highly dependent on machine, os, rendering engine, angular version, blood preasure, ...

Is there a robust workaround?

I've found some examples with event listeners (
$on
,
$emit
) and with some magic done with ng-repeat
$scope.$last
. If I can remove flickering, I'd accept some coupling between components, even this does not fit nice with AngularJS' ambition.

Answer

Data hasn't yet arrived at scope at rendering time!

It turned out the problem was that the data has not been present at the time the directive was executed (linked).

In the fiddle, data was accessible in the scope very fast. In my application it took more time since it was loaded via $http. This is the reason why a $timeout of < 10ms was not enough in most cases.

So the solution in my case was, instead of

angular.module('myApp')
    .directive('docListWrapper', ['$timeout', function ($timeout) {
        return {
            restrict: 'C',
            templateUrl: 'partials/doc-list-wrapper.html',
            scope: { docs: '=docs'},
            link: function (scope, element, attrs) {

                $timeout(function () { // <-------------------- $timeout
                    element
                        .children('.doc-list')
                        .not('.ng-hide')
                        .bxSlider();
                }, 10);
            }
        };
    }]);

I now have this:

angular.module('myApp')
    .directive('docListWrapper', [function () {
        return {
            restrict: 'C',
            templateUrl: 'partials/doc-list-wrapper.html',
            scope: { docs: '=docs'},
            link: function (scope, element, attrs) {

                scope.$watch('docs', function () { // <---------- $watch
                    element
                        .children('.doc-list')
                        .not('.ng-hide')
                        .bxSlider();
                });
            }
        };
    }]);

Maybe there is a more elegant solution for this problem, but for now I'm happy that it works.

I hope this helps other AngularJS beginners.