AngularJS watcher not binding/ watching - angularjs

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

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.

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

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

When do we know the actual width of an element in Angular?

I'm tring to create a directive that will center a div.
So far, I have this code:
app.directive("setcenter", function () {
return {
scope:{
setcenter: '='
},
link: function (scope, element, attrs) {
scope.$watch('setcenter', function (newValue) {
if (newValue == true) {
var width = element.width();
element.css('position', 'absolute');
element.css('top', '80px');
element.css('left', '50%');
element.css('z-index', '200');
element.css('margin-left', '-' + width / 2 + 'px');
}
});
}
}
});
The problem is the width of the element. The whole point for this directive is that the div that uses this directive, don't have a width set. I want this directive to figure out the width and center the div.
The problem I encounter is that when the directive is invoked, the actual width of the div is not yet known. When I use this in my situation, the div is 800px, but when the page is finished loading, the div is 221px.
So, what can I do to wait till the actual width is known of the div?
First, I only have used this logic when I defined a controller for a directive rather than a link function. So defining it in a link function instead may cause different behavior, but I suggest you try it there first and if you can't get it to work then switch to using a controller.
As far as I can tell, the only change you would need to make this work would be to change $scope to scope calls and $element to element since the dependency injected objects become standard link function parameters.
$scope.getElementDimensions = function () {
return { 'h': $element.height(), 'w': $element.width() };
};
$scope.$watch($scope.getElementDimensions, function (newValue, oldValue) {
//<<perform your logic here using newValue.w and set your variables on the scope>>
}, true);
$element.bind('resize', function () {
$scope.$apply();
});
The idea for this usage came to me after reading a very similar type of usage about watching the $window rather than the current element, the original work can be found here.
Angular.js: set element height on page load
James' answer led me to:
app.directive('measureInto', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function() {
return element[0].clientWidth;
}, function(value){
scope[attrs.measureInto] = element[0].clientWidth + 10;
});
}
};
});
So, at runtime, I add this and assign into whatever scope variable I want the width of the element I'm looking for
I had a similar issue and found that the dimensions were reliably correct when all the ng-ifs (or anything else using ngAnimate) on the page had been resolved - it's possible something similar is happening here. If so, this would do the trick without adding any new listeners:
$scope.tryGetElementDimensions = function () {
if (!angular.element("your-element") || ((angular.element("your-element")[0].classList).contains("ng-animate")
$timeout(function() {
$scope.tryGetElementDimensions()
})
}
else {
$scope.getElementDimensions()
}
$scope.getElementDimensions = function (){
//whatever you actually wanted to do
}
link: function (scope, element, attrs) {
$scope.tryGetElementDimensions()
}
Angular adds ng-animate and ng-enter, ng-leave classes while it's animating and you can be confident it's finished when these classes have all been removed. $timeout without a second argument just waits for the next digest.
Can't comment yet, therefore this answer.
Found a similar solution like the one strom2357 is suggesting. $timeout works really well to let you know when the dom is ready, and it is super simple. I am using this solution to get the ui-view element width. Found it in a fiddle.
var app = angular.module('app', []);
app.controller('MyController', function($timeout, $scope){
$timeout(function(){
//This is where you would get width or height of an element
alert('DOM ready');
});
alert('DOM not ready');
});

Resources