最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is $timeout the onlyrecommended way to avoid jQuery plugin rendering problems in AngularJS directives? - Stack Over

programmeradmin4浏览0评论

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/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 plete 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 ponents, even this does not fit nice with AngularJS' ambition.

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/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 plete 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 ponents, even this does not fit nice with AngularJS' ambition.

Share Improve this question edited Dec 12, 2013 at 17:34 hgoebl asked Dec 10, 2013 at 16:35 hgoeblhgoebl 13k9 gold badges51 silver badges74 bronze badges 3
  • 2 don't need long timeout, I often use 1. Concept is that $timeout pushes to next digest cycle, by which time ng-repeat will have been digested and those elements will then exist when code within $timeout is run – charlietfl Commented Dec 10, 2013 at 16:53
  • could not you just use some native angular implementation of slider? – artur grzesiak Commented Dec 12, 2013 at 19:21
  • I'd like to use a native implementation, but haven't found one fitting my needs. Most sliders are controls to select a value, bxSlider is more like a carousel, but grouping many items to one page. – hgoebl Commented Dec 12, 2013 at 20:15
Add a ment  | 

3 Answers 3

Reset to default 5 +50

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

  1. Angular piles 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 ponent 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 ponent 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 piles 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 ponent 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 ponents.

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 ponents 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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论