Defer process after data retrieval and display - angularjs

Hi I have a factory that GETs data from a backend. This data is then processed with a controller (as seen below) and injected in the web page with ng-repeat. Due to asynchronous nature of the system, I have troubles when I try to manipulate window. For example I need to use window.scrollTo function but only AFTER data was completely processed and displayed on screen with ng-repeat.
As you can see here, I tried to use a promise early in the controller. But it doesn't work: window.scrollTo is always processed before data has finished being processed on screen. I guess what I need is a way to actually force the data process to be completed and only then process the window.scrollTo function but I don't see how.
.controller('myCtrl',
function ($scope, prosFactory, fieldValues, $q ) {
$scope.listpros = function() {
prosFactory.getPros()
.success(function(data, status) {
var defer = $q.defer();
defer.promise
.then(function () {
$scope.prosItems = data; // pass data to ng repeat first
})
.then(function () {
$window.scrollTo(0, 66); // then scroll
});
defer.resolve();
}).error(function(data, status) {
alert("error");
}
);
};
I tried with a 2000 timeout on scrollTo function, but due to variation in internet speed, it sometimes delay the scroll to much, or sometime isn't enough.

A promise won't help much in this case, because it is not connected to the ng-repeat process by any means. However you could use a custom directive to $emit an event when the last item got processed. Your controller could listen to that specific event and react to it according to your needs.
This answer shows you how to achieve this, it even comes with a Plunker.
EDIT: General approach to control DOM creation
Within the AngularJS world, DOM elements are controlled and, in this case supervised, by directives. The answer I linked to above extends the behavior of a ng-repeat directive, but you could do the same for possibly any DOM element. A directive creation-emitter could look something like this:
myModule.directive('creationEmitter', function () {
return {
restrict: 'A',
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function (scope, iElement, iAttrs, controller) {
scope.$emit('preCompile', iElement)
},
post: function (scope, iElement, iAttrs, controller) {
scope.$emit('postCompile', iElement)
}
}
},
link: function (scope, iElement, iAttrs) {
scope.$emit('link', iElement)
}
};
});
You could now listen to all those events in a controller that's above the given element in the DOM (for child elements use $broadcast).

Its possible to chain promises and do intermediate processing.
The immideate callback must return what the next will receive.
I don't see any immediate problems with this approach:
prosFactory.getPros()
.then(processCb)
.then(doSomethingWithProcessedDataCb)
.catch(errorCb)
.finally(scrollWindowCb)
Check out the documentation, it's actually quite good.

Related

Directive using timeout not waiting for animation to finish

This "focusMe" directive using the $timeout service does not wait for my modal to finish loading, so the animation is choppy. The input box should be automatically focused so the user does not have to click the box (or press tab) to enter their badge number.
I've attempted to do my own hacky fixes, and found two directives online that people said worked for them. I've included the best attempt I made in this plnkr
Here is the broken directive:
signout.directive('focusMe', ['$timeout', '$parse', function ($timeout, $parse) {
return {
// scope: true, // optionally create a child scope
link: function (scope, element, attrs) {
var model = $parse(attrs.focusMe);
scope.$watch(model, function (value) {
console.log('value=', value);
if (value === true) {
$timeout(function () {
element[0].focus();
});
}
});
}
};
}]);
Here's a visualization of this issue:
Choppy
Smooth
My desired result is the directive waiting for animation to finish, then immediately focus on the input field for easy entry.

AngularJS Passing function with arguments to event directive

To start off, I know there are loads of similar questions but none that I found which supports execution of arbitrary methods with event listeners.
I have this directive which executes functions passed when the window resizes.
app.directive('onResize', function() {
var directive = {
'link': function(scope, element, attrs) {
var onResizeHandler = scope.$eval(attrs.onResize);
angular.element(window).on('resize', onResizeHandler);
angular.element(window).on('$destory', function() {element.off();});
}
};
return directive;
});
I can trigger the above directive with
<div data-on-resize="stickyHeader">...</div>
Which runs my method, inside my controller.
app.controller('myController', [$scope, function($scope) {
$scope.stickyHeader = function() {
console.log('event triggered')
};
}]);
All the above code works fine, but I need to pass some arguments to stickyHeader as in data-on-resize="stickyHeader(arg1, arg2)" When I try that, I get Cannot read property 'call' of undefined at ng (angular.js:3795) in the console. Not sure what I can do to make my directive support arbitrary methods with arguments.
The directive needs to evaluate the AngularJS expression defined by the on-resize attribute every time the events occurs:
app.directive('onResize', function($window) {
var directive = {
link: function(scope, elem, attrs) {
angular.element($window).on('resize', onResizeHandler);
scope.$on('$destroy', function() {
angular.element($window).off('resize', onResizeHandler);
});
function onResizeHandler(event) {
scope.$eval(attrs.onResize, {$event: event});
scope.$apply();
}
}
};
return directive;
});
Also since the resize event comes from outside the AngularJS framework, the event needs to be brought into the AngularJS execution context with $apply().
Further, to avoid memory leaks, the event handler needs to be unbound when the scope is destroyed.
Usage:
<div data-on-resize="stickyHeader($event)">...</div>
For more information, see AngularJS Developer Guide - $event.

AngularJS watcher not binding/ watching

I'm trying to implement the "infinite-scroll" feature in a list using a directive, which should load progressively a new set of "orders" when the scroll of the html element reaches or exceeds 75% of the scrollable height and append it to the existing list.
Unfortunately, the watcher doesn't trigger when i scroll the list.
The directive is located in the right tag and the watcher triggers the listener function only the first time, when the element is rendered by the browser.
The strange thing is that if i change path and then i return to the path where the list is, the watcher start behaving correctly and trigger the listener function everytime i perform a scroll.
<ol orders-loader class="orders-list">...</ol>
angular:
(function () {
angular.
module('myApp')
.directive('ordersLoader', ['$window', '$timeout', 'ordersResource', ordersloaderDirective])
function ordersloaderDirective($window, $timeout, loading, ordersResource) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.orders = ordersResource; /*ordersResource use $resource to api calls
and then stocks the data in a array exposed in the scope*/
$timeout(function () {
scope.$watch(function () { return element[0].scrollTop }, function () {
if (*the scroll exceedes more or less 75% of the total scrollHeight*/) {
/*asking for more orders*/
}
});
}, 0);
}
}
}
I can't figure out where is the problem.
Solved
As yeouuu suggested, there was no digest cycle after the list scroll event, so i added:
element.bind('scroll', function () {
scope.$apply();
});
just before the $timeout function.
Whenever using plugins outside of angularJs that should trigger watcher you need to explicitly apply them. Otherwise Angular won't be aware of these changes/events.
In your case that means adding scope.$apply(); after the event.
Your edited solution:
element.bind('scroll', function () {
scope.$apply();
});
More information can be found here about the scope life cycle: https://docs.angularjs.org/guide/scope#scope-life-cycle

Defer compilation or linking of a child directive from the parent

I'm trying to build an AngularJS directive that, during the linking phase, moves it's children (which are also directives) into another location before the children get into their linking phase. The moving of the children is easily achieved using transclution, however, even though I don't immediately put the child elements back into the DOM they still get compiled and linked.
The use case that I'm building this for is an image gallery directive that manages the loading of its images so that a gallery with a large number of images only loads the ones that are actually needed for rendering. The 'images', that are the child directives of the gallery, set the image src during the linking phase. Unfortunately, the image directive can't be easily modified since it's used very prolifically throughout the rest of the site.
A generic prototype of the use case that I'm describing to can be seen here:
http://jsfiddle.net/vmJKK/1/
var app = angular.module('myApp', []);
app.directive('parent', function ($timeout) {
return {
restrict: 'E',
transclude: true,
template: '<div><ol><li ng-repeat="log in logs track by $index">{{log}}</li></ol><hr></div>',
link: function ($scope, $element, $attrs, ctrl, $transclude) {
var _children = [];
$scope.logs = [];
$transclude(function (clones) {
$scope.logs.push('Transcluding');
angular.forEach(clones, function (clone) {
if (clone.nodeName !== '#text') {
_children.push(clone);
}
});
});
$timeout(function () {
angular.forEach(_children, function (child) {
$scope.logs.push('Appending child');
$element.append(child);
});
}, 2000);
}
};
});
app.directive('child', function () {
return {
restrict: 'E',
template: 'I am a child!',
link: function ($scope, $element, $attrs) {
$scope.$parent.logs.push('Linking child');
}
};
});
The current output of the prototype execution is:
1. Transcluding
2. Linking child
3. Linking child
4. Appending child
5. Appending child
What I want it to be is:
1. Transcluding
2. Appending child
3. Linking child
4. Appending child
5. Linking child
...or something like that, where the linking of the child doesn't occur until it's being appended.
You should be able to change child directive without breaking functionality with the code similar to this:
link: function ($scope, $element, $attrs) {
function link() {
$scope.$parent.logs.push('Linking child');
}
if (angular.isDefined($attrs.defer)) {
$element[0].link = link;
} else {
link();
}
}
The idea is to wrap current link function inside function that is either called immediately or attached to DOM node as parameterless link() function and called from append function of parent directive (if defer attribute is provided). I hope this is acceptable in your case.
The code is here: http://jsfiddle.net/vmJKK/5/
By the way, I have never seen the question of this quality so far. Perfect.

How to detect when AngularJS has finished loading ALL DOM from many ng-repeates

Hey guys so I have the following predigament:
I have an ng-repeat with another ng-repeat with another ng-repeat and so forth ( creating something similar of a binary tree structure but with multiple roots). The problem is that my data is inserted into the proper structures and is waiting for the digest to actually display everything on the screen since some of the structures are quite large. How can I know when digest has finished rendering the last of the elements of my structure? I have the following added to my ng-repeates but that gets executed so many times because ng-repeats can also be ng-repeated... How can I only signal when the last of the templates has loaded and not every time a template loads? Here is what I have thanks to another thread Calling a function when ng-repeat has finished:
app.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit('ngRepeatFinished');
});
}
}
}
});
app.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit('ngRepeatFinished');
});
}
}
}
});
Problem is that this fires for the last ng-repeat but I cannot determined when the last of the nested ng-repeates finishes.. Is there any way to detect that the digest has finished rendering all of my templates and nested templates?
Normally this would just be handled by watching for $scope.$last in your specific repeat.
However, if we are loading up a lot of data at runtime execution, perhaps Angular $postDigest to the rescue!
P.S. Don't use $emit in this case. Remember that directives can link with any scope you need in any controller and modify their value, and that overall - using $emit or $broadcast should always be avoided unless absolutely necessary.
app.directive('resolveApplication', function($timeout) {
return {
restrict:'A',
link: function (scope) {
// start by waiting for digests to finish
scope.$$postDigest(function() {
// next we wait for the dom to be ready
angular.element(document).ready(function () {
// finally we apply a timeout with a value
// of 0 ms to allow any lingering js threads
// to catch up
$timeout(function() {
// your dom is ready and rendered
// if you have an ng-show wrapper
// hiding your view from the ugly
// render cycle, we can go ahead
// and unveil that now:
scope.ngRepeatFinished = true;
},0)
});
});
}
}
});

Resources