Directive using timeout not waiting for animation to finish - angularjs

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.

Related

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

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Defer process after data retrieval and display

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.

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)
});
});
}
}
});

AngularJS (1.1.5): Can you cancel Directives with priority cancel

Is it possible to keep the built-in ng-click handler from firing when you have a custom ng-click directive with a priority > 0 that fires first? Or to delay the built-in one in some way?
In my case I have a custom ngClick directive that applies an animation to an element then waits for the animation to complete. Once it is complete, and only then, should the built-in ngClick fire. Reasoning is that the clicked element is on a slide-out drawer that is automatically hidden from within the ngClick handler. If the directive can't keep it from firing, then the drawer is closed before the animation even starts.
From the custom directive, I can use this to invoke the default ngClick, but the original needs to be cancelled in this case...
Requirement: The solution should not require the developer write any code within their ngClick handler. I am definitely able to code this in the controller, but want to avoid having the controller having to know that it is supposed to wait. (i.e. in case I change how the directive implements the indicator and it takes a different amount of time)
$timeout(function() {service.close();}, 400);
Here is an example of what I want to accomplish.
Markup
<li ng-repeat="product in service.products"
ng-click="onClick('{{product}}')"
pre-click="onClicking('{{product}}')"
animate="wasClicked(product)">
{{product}}
</li>
Directive
angular.module('sales.directives')
.directive('ngClick', [
'$timeout',
function ($timeout) {
return {
priority: 50, // higher priority
restrict: 'A',
scope: false,
link: function (scope, element, attributes) {
element.bind('click', function () {
if (attributes.preClick) {
eval('scope.' + attributes.preClick);
}
});
if (attributes.animate !== undefined) {
scope.$watch(attributes.animate, function (newValue) {
if (newValue == true) {
element.addClass('animated');
// pause 400ms so animation can complete
$timeout(angular.noop, 400)
.then(function () {
element.removeClass('animated');
// I would like to invoke the original
// ngClick here, and then remove it from the
// queue so that it doesn't fire it again.
// Reason for invoking it here is that if I
// don't, then the base ngClick event will
// fire before this promise is resolved.
eval('scope.' + element.ngClick);
// ??
});
}
});
}
}
};
}]);
Controller
angular.module('sales')
.controller(
'ProductController',
[
'$scope', 'ProductService',
function ($scope, $timeout, service) {
$scope.clickedProduct = null;
$scope.onClicking = function (product) {
$scope.clickedProduct = product;
};
$scope.wasClicked = function (product) {
return $scope.clickedProduct === product;
};
$scope.onClick = function (product) {
service.selected = product;
};
}
]);
Any thoughts? Is there a better way to do this?

Resources