AngularJS: what is the execution order of $watch? - angularjs

I have a watch function:
scope.$watch('target', function(){
scope.hasUnsavedChanges = true;
}, true);
Then I change some value:
functiion change(target){
scope.hasUnsavedChanges = false;
scope.target.Xxx = '';
console.log('scope.hasUnsavedChanges = ' + scope.hasUnsavedChanges);
}
It outputs false. So when the watch function executes?
And how to run some code after scope.hasUnsavedChanges becomes true in the above code?

$watch functions executes after every time $scope.$digests() function is called.
And if you want to execute code after a variable like hasUnsavedChanges become true. Wouldn't it make more sense to make it a function instead and execute all code there like:
scope.setUnsavedChanges = function(){
scope.hasUnsavedChanges = true;
//the rest of your code goes here
}
scope.$watch('target', function(){
scope.setUnsavedChanges();
}, true);

Related

angular `$broadcast` issue - how to fix this?

In my app, I am boradcasting a event for certain point, with checking some value. it works fine But the issue is, later on whenever i am trigger the broadcast, still my conditions works, that means my condition is working all times after the trigger happend.
here is my code :
scope.$watch('ctrl.data.deviceCity', function(newcity, oldcity) {
if (!newcity) {
scope.preloadMsg = false;
return;
}
scope.$on('cfpLoadingBar:started', function() {
$timeout(function() {
if (newcity && newcity.originalObject.stateId) { //the condition not works after the first time means alwasy appends the text
console.log('each time');
$('#loading-bar-spinner').find('.spinner-icon span')
.text('Finding install sites...');
}
}, 100);
});
});
you can deregister the watcher by storing its reference in a variable and then calling it:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
if( someCondition === true ){
myWatch(); //deregister the watcher by calling its reference
}
});
if you want to switch logic, just set some variable somewhere that dictates the control flow of the method:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
scope.calledOnce = false;
if(!scope.calledOnce){
//... run this the first time
scope.calledOnce = true;
}
else {
// run this the second time (and every other time if you do not deregister this watch or change the variable)
// if you don't need this $watch anymore afterwards, just deregister it like so:
myWatch();
}
})

Assigning values in functions called $interval

I'm having difficulty understanding how value assignments are handled within functions called by $interval.
Given the following code:
this.value = null;
this.setSelection = function() {
this.value = Math.floor(Math.random()*11);
};
this.isSelected = function(item) {
if(this.value === item) {
return "selected";
}
};
If I call the setSelection in this manner $interval(this.setSelection, 2000); the function is called as on the defined interval, but the scope of the assigned value seems to be such that the actual this.value is not altered and when the digest calls isSelected, the value is still null.
However, if I call the function directly (such as this incorrect form) $interval(this.setSelection(), 2000); the value is correctly updated and when digest runs isSelected it recognizes the new value. Obviously, the interval does not run again because $interval had expected a function reference.
I had also tried wrapping the function call in an anonymous function, but this resulted in an error stating the function was undefined.
So my question is, why when my function is called correctly by $interval, I get this unexpected result?
this depends on context. When calling $interval(this.setSelection, 2000), you are losing the context of this. The method is called just as a function. When this.value = ... is being executed, this is something completely different than what you expect (probably the containing function).
One way to fix this is a simple workaround:
this.value = null;
var self = this;
this.setSelection = function() {
self.value = Math.floor(Math.random()*11);
};
Another solution is to bind the function to this:
$interval(this.setSelection.bind(this), 2000)

$compile an HTML template and perform a printing operation only after $compile is completed

In a directive link function, I want to add to document's DIV a compiled ad-hoc template and then print the window. I try the following code and printer preview appears, but the data in preview is still not compiled.
// create a div
printSection = document.createElement('div');
printSection.id = 'printSection';
document.body.appendChild(printSection);
// Trying to add some template to div
scope.someVar = "This is print header";
var htmlTemplate = "<h1>{{someVar}}</h1>";
var ps = angular.element(printSection);
ps.append(htmlTemplate);
$compile(ps.contents())(scope);
// What I must do to turn information inside printSection into compiled result
// (I need later to have a table rendered using ng-repeat?)
window.print();
// ... currently shows page with "{{someVar}}", not "This is print header"
Is it also so that $compile is not synchronous? How I can trigger window.print() only after it finished compilation?
you just need to finish the current digestion process to be able to print
so changing
window.print();
to
_.defer(function() {
window.print();
});
or $timeout, or any deferred handler.
will do the trick.
The other way (probably the 'right' approach) is to force the newly compilated content's watchers to execute before exiting the current $apply phase :
module.factory("scopeUtils", function($parse) {
var scopeUtils = {
/**
* Apply watchers of given scope even if a digest progress is already in process on another level.
* This will only do a one-time cycle of watchers, without cascade digest.
*
* Please note that this is (almost) a hack, behaviour may be hazardous so please use with caution.
*
* #param {Scope} scope : scope to apply watchers from.
*/
applyWatchers : function(scope) {
scopeUtils.traverseScopeTree(scope, function(scope) {
var watchers = scope.$$watchers;
if(!watchers) {
return;
}
var watcher;
for(var i=0; i<watchers.length; i++) {
watcher = watchers[i];
var value = watcher.get(scope);
watcher.fn(value, value, scope);
}
});
},
traverseScopeTree : function(parentScope, traverseFn) {
var next,
current = parentScope,
target = parentScope;
do {
traverseFn(current);
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while((current = next));
}
};
return scopeUtils;
});
use like this :
scopeUtils.applyWatchers(myFreshlyAddedContentScope);

Caret position in textarea with AngularJS

I am asking myself if I am doing it right. The problem I have is that I want to preserve caret position after AngularJS update textarea value.
HTML looks like this:
<div ng-controlle="editorController">
<button ng-click="addSomeTextAtTheEnd()">Add some text at the end</button>
<textarea id="editor" ng-model="editor"></textarea>
</div>
My controller looks like this:
app.controller("editorController", function($scope, $timeout, $window) {
$scope.editor = "";
$scope.addSomeTextAtTheEnd = function() {
$timeout(function() {
$scope.editor = $scope.editor + " Period!";
}, 5000);
}
$scope.$watch("editor", function editorListener() {
var editor = $window.document.getElementById("editor");
var start = editor.selectionStart;
var end = editor.selectionEnd;
$scope.$evalAsync(function() {
editor.selectionStart = start;
editor.selectionEnd = end;
});
});
});
Let say I start typing some text in textarea. Then I hit the button which will soon add " Period!" at the end of $scope.editor value. During the 5 seconds timeout I make focus on textarea again and write some more text. After 5 seconds my textarea value is updated.
I am watching for $scope.editor value. The editorListener will be executed on every $digest cycle. In this cycle also happens two-way data binding. I need to correct caret position right after data binding. Is $scope.$evalAsync(...) the right place where should I do this or not?
Here is a directive I use to manipulate the caret position; however, like I stated in a comment, there is an issue with IE.
Below is something that may help you plan this out. One thing I noticed in your question is that you mention a condition where the user may re-focus the input box to type additional text, which I would believe resets the timeout; would this condition be true?
Instead of using a button to add text, would you rather just add it without? Like running the addSomeTextAtTheEnd function whenever a user un-focuses from the input box?
If you have to use the button, and a user re-focuses on the input box and types more into, you should cancel your button timeout.
Like:
myVar = setTimeout(function(){ alert("Hello"); }, 3000);
// Then clear the timeout in your $watch if there is any change to the input.
clearTimeout(myVar);
If you do it this way, perhaps you will not even need to know the cursor position as the addSomeTextAtTheEnd function timeout will just reset on any input change before the 5 second timeout. If the 5 second timeout occurs then the addSomeTextAtTheEnd will run and "append text to the end" like it's supposed to do. Please give more information and I will update this as needed.
app.directive('filterElement', ['$filter', function($filter){
return {
restrict:'A', // Declares an Attributes Directive.
require: '?ngModel', // ? checks for parent scope if one exists.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return }
var conditional = attrs.rsFilterElement.conditional ? attrs.rsFilterElement.conditional : null;
scope.$watch(attrs.ngModel, function(value){
if( value == undefined || !attrs.rsFilterElement ){ return }
// Initialize the following values
// These values are used to set the cursor of the input.
var initialCursorPosition = elem[0].selectionStart
var initialValueLength = elem[0].value.length
var difference = false
// Sets ngModelView and ngViewValue
ngModel.$setViewValue($filter( attrs.rsFilterElement )( value, conditional ));
attrs.$$element[0].value = $filter( attrs.rsFilterElement )( value, conditional );
if(elem[0].value.length > initialValueLength){ difference = true }
if(elem[0].value.length < initialValueLength){ initialCursorPosition = initialCursorPosition - 1 }
elem[0].selectionStart = difference ? elem[0].selectionStart : initialCursorPosition
elem[0].selectionEnd = difference ? elem[0].selectionEnd : initialCursorPosition
});
} // end link
} // end return
}]);

Angularjs wait until

I have:
$scope.bounds = {}
And later in my code:
$scope.$on('leafletDirectiveMap.load', function(){
console.log('runs');
Dajaxice.async.hello($scope.populate, {
'west' : $scope.bounds.southWest.lng,
'east': $scope.bounds.northEast.lng,
'north' : $scope.bounds.northEast.lat,
'south': $scope.bounds.southWest.lat,
});
});
The bounds as you can see at the begging they are empty but they are loaded later (some milliseconds) with a javascript library (leaflet angular). However the $scope.$on(...) runs before the bounds have been set so the 'west' : $scope.bounds.southWest.lng, returns an error with an undefined variable.
What I want to do is to wait the bounds (southWest and northEast) to have been set and then run the Dajaxice.async.hello(...).
So I need something like "wait until bounds are set".
You can use $watch for this purpose, something like this:
$scope.$on('leafletDirectiveMap.load', function(){
$scope.$watch( "bounds" , function(n,o){
if(n==o) return;
Dajaxice.async.hello($scope.populate, {
'west' : $scope.bounds.southWest.lng,
'east': $scope.bounds.northEast.lng,
'north' : $scope.bounds.northEast.lat,
'south': $scope.bounds.southWest.lat,
});
},true);
});
If you want to do this every time the bounds change, you should just use a $watch expression:
$scope.$watch('bounds',function(newBounds) {
...
});
If you only want to do it the first time the bounds are set, you should stop watching after you've done your thing:
var stopWatching = $scope.$watch('bounds',function(newBounds) {
if(newBounds.southWest) {
...
stopWatching();
}
});
You can see it in action here: http://plnkr.co/edit/nTKx1uwsAEalc7Zgss2r?p=preview

Resources