Reputation: 13007
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.
Upvotes: 1
Views: 4769
Reputation: 23394
Your problem is a racing condition problem, so you can't just remove the $timeout
. Pretty much what happens is:
bx-slider
element;bx-slider
looks for <li>
elements (none at this time) and create the list;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:
bx-slider
element;ng-repeat
and build the <li>
list;update
function, that invokes your component building process, that duplicates the last element;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:
$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 problemUpvotes: 5
Reputation: 13007
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.
Upvotes: 0
Reputation: 449
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.
Upvotes: 3