简体   繁体   中英

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.

Your problem is a racing condition problem, so you can't just remove the $timeout . Pretty much what happens is:

  1. Angular compiles both elements;
  2. Angular links you bx-slider element;
  3. bx-slider looks for <li> elements (none at this time) and create the list;
  4. Angular links the ng-repeat and build the <li> list and resolve the bindings.

So, to solve the first aspect of racing condition (build the component only after all <li> are ready), you should expose a update method at bxSlider directive and create a sub-directive that would call a update function in the bxSlider controller, using the $scope.$last trick :

.directive('bxSlider', function () {
    var BX_SLIDER_OPTIONS = {
        minSlides: 2,
        maxSlides: 7,
        slideWidth: 120
    };

    return {
        restrict: 'A',
        require: 'bxSlider',
        priority: 0,
        controller: function() {},
        link: function (scope, element, attrs, ctrl) {
            var slider;
            ctrl.update = function() {
                slider && slider.destroySlider();
                slider = element.bxSlider(BX_SLIDER_OPTIONS);
            };
        }
    }
}])
.directive('bxSliderItem', function($timeout) {
    return {
        require: '^bxSlider',
        link: function(scope, elm, attr, bxSliderCtrl) {
            if (scope.$last) {
                bxSliderCtrl.update();
            }
        }
    }
})

This solution would even give you the ability to add new itens to the model, for everytime you have a new $last item, the bxSlider would be built. But again, you would run into another racing condition. During step 3 , the slider component duplicates the last element, in order to show it just before the first, to create a 'continuity' impression (take a look at the fiddle to understand what I mean). So now your flow is like:

  1. Angular compiles both elements;
  2. Angular links you bx-slider element;
  3. Angular links the ng-repeat and build the <li> list;
  4. Your code calls the parent update function, that invokes your component building process, that duplicates the last element;
  5. Angular resolves the bindings.

So now, your problem is that the duplications made by the slider, carries only the templates of the elements, as Angular hadn't yet resolved it bindings. So, whenever you loop the list, you gonna see a broken content. To solve it, simply adding a $timeout of 1 millisecond is enough, because you gonna swap the order of steps 4 and 5, as Angular binding resolution happens in the same stack as the $digest cycle, so you should have no problem with it:

.directive('bxSliderItem', function($timeout) {
    return {
        require: '^bxSlider',
        link: function(scope, elm, attr, bxSliderCtrl) {
            if (scope.$last) {
                $timeout(bxSliderCtrl.update, 1);
            }
        }
    }
})

But you have a new problem with that, as the Slider duplicates the boundaries elements, these duplications are not overviewed by AngularJs digest cycle, so you lost the capability of model binding inside these components.

After all of this, what I suggest you is to use a already-adapted-angularjs-only slide solution .

So, summarizing:

  1. you can use 1 millisecond delay in your solution, because Angular digest cycle is synchronous; - but you lost the capability to add new items to your list
  2. you can use a $scope.$last trick with the $timeout as well - but you lost Angular bindings and if this components have any instance (selected, hover), you gonna have problem
  3. you could use an already written solution (like the one I suggested).
  4. you could write your own AngularJs native solution.

My answer seems round-about but might remove your need for $timeout. Try making another directive and attaching it to the li element. Something like the following pseudo code:

angular.module('myApp').directive('pdfClick', function() {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            $element.bxSlider().delegate('a', 'click', pdfClicked);
        }
    }
});

<li class="doc-thumbnail" ng-repeat="doc in docs" pdfClick>

It should attach the click event to every list item's anchor generated by ng repeat.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM