Angular: manipulating event firing order - namely ng-blur & ng-click? - angularjs

I have two html elements: <aleph> and <beth>. The element <aleph> has a ng-blur attribute and the element <beth> has the ng-click attribute.
If I am in focus on elment <aleph> and click on <beth> it appears (according to experimentation) that the ng-blur will fire first and ng-click will fire next.
http://jsfiddle.net/UTn5y/70/
Will that always be the behavior?
Could I change that behavior without any effort?

Will that always be the behavior?
Yes, the blur event will always be fired first
Could I change that behavior without any effort?
No you can not change the behavior. However, you can manage it by hand. You can use $timeout in your ng-blur callback, and check for a variable which can be manipulated by ng-click.
function ngClick () {
$scope.clicked = true;
// rest of your code
}
function ngBlur () {
$scope.clicked = false;
$timeout(function () { // will be executed after ngClick function in case of click
if ($scope.clicked) {
return;
}
// rest of your code
})
}
Here is a working version:
http://jsfiddle.net/zhfew91j/2/

Related

Angular Directive Firing Multiple Times

I am newer to AngularJS and having an issue that I hope someone can point me in the right direction to figuring out. I have created a directive called sizeWatcher which is placed as an attribute into the HTML which essentially just gets the height of the element it's placed on and echos that height into a scope variable named style that I set onto another element through the use of ng-style="style".
I'm finding that whenever I open my accordion, the $watch fires on the directive but it's firing multiple times. I have a console.log in my $watch and am seeing 3 log entries, the first 2 are the same (guessing this happens on click before the accordion opens, and then the accordion opens and the 3rd log entry is the final height after the accordion is opened). The main issue is that the style variable is only getting set to the smaller heights before the accordion is expanded even though the log is registering the greater height as the last time the directive is hit -- How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive? Any insight into this would be greatly appreciated. Relevant code attached below:
TEMPLATE:
<div class="content-wrap" id="height-block" ng-style="style">
<!-- Other HTML etc... -->
<uib-accordion size-watcher close-others="oneAtATime">
<!-- Accordion Directive HTML.. -->
</uib-accordion>
</div>
JavaScript:
.directive("sizeWatcher", function () { //add size-watcher attribute to element on the page to have it echo its' height to the {{style}} scope
function link(scope, element, attrs) {
scope.$watch(function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
});
}
return {
restrict: "AE", //attribute & element declarations
link: link
};
})
How can I ignore the first $watch event firings and only act
accordingly on the last and final run-through of the directive?
You can ignore watcher when new or old values are undefined and not equal to each other:
$scope.$watch(function () {
return element.height(); // or something else
},
function (newVal, oldVal) {
if (newVal !== undefined && oldVal !== undefined && newVal !== oldVal) {
// your stuff
if (newVal > 150) {
scope.style = {
height: newVal + 'px'
};
}
}
});
Anyways you can play with if statement regards to your needs
FYI, to improve performance $watch returns cancel callback so you can stop watcher whenever you want:
var cancelWatch = $scope.$watch(function () {
return element.height();
},
function (newVal, oldVal) {
if (<some condition>) {
cancelWatch();
}
});
Obviously an answer to this needs a link to the Angular-Documentation for $watch ;)
it states the following:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
which probably explains your first call.
I'm guessing the second call happens because the accordion is rerendered after initialization (with a title/ or label or anything) which triggers the $digest and thus the $watch expression on the height.
Finally the third call happens when you open the accordion and the height actually changes.
To fix this you can compare the newValue and oldValue of the watched expression like Maxim Shoustin said in his answer. Here is an example (again taken from the Angular-docs)
scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() { return food; },
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
However if you actually want to change the style of the element you might want to take a look into ng-class instead of manually registering any watchers!
This is happening because you are not using $watch correct way,
The first parameter to $watch is a variable which you want to watch(this can be a callback).
The second parameter to $watch is a callback which performs the desired action on change
So in your case it would be something like this
scope.$watch(
function () {
return element[0].offsetHeight;
},
function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
}
)
Please notice the first function, so whenever the value it is returning changes, the second callback will execute
Hope this helps you

Detecting window focus the angular way?

Is there a good angular way to detect window focus? I am using html5 notifications and I would like to only fire if the window is out of focus.
Thanks!
There's a built-in angular directive ngFocus here maybe it helps if you attach it to the body
<window, input, select, textarea, a
ng-focus="">
...
</window, input, select, textarea, a>
Edit: For window focus, there's the $window wrapper and you can do something like:
$window.onfocus = function(){
console.log("focused");
}
Edit #CristiBerceanu is right - you should use the built-in ng-focus directive. However, take this answer as a guideline for any missing event you want to bind.
You must create a directive:
angular
.module('MyModule', [])
.directive('onFocus', function(){
return {
restrict: 'A',
scope: {
'focus': '&onFocus'
},
link: function($scope, $element, $attributes) {
var focus = function(event) {
$scope.focus({'$event': event});
};
$element.on("focus", focus);
$scope.$on('$destroy', function(){
$element.off('focus', onClick);
});
}
}
});
Notice how the event is bound in the directive by jquery and NOT directly in the controller. Additionally, notice that a bound expression is tied using the & prefix (evaluable expression binding) instead of regular prefixes like # (text-binding) or = (scope property reference, bi-directional, binding).
In Cristi Berceanu's answer, he suggests assigning a function to $window.onfocus, which does work. However, there is a problem with that... only one function can be assigned to $window.focus at a time. Thus, by assigning a function to $window.onfocus, you could accidentally overwrite a previous function, and your function will be vulnerable to being overwritten later, too.
Here's a different solution that allows multiple functions to run with the window's focus or blur events:
var onFocus = function () {
// do something
};
var onBlur = function () {
// do something else
};
var win = angular.element($window);
win.on("focus", onFocus);
win.on("blur", onBlur);
This will allow you to assign multiple functions to the focus and blur events for the $window object.
If you added the functions inside a controller and want to remove those functions when the controller is destroyed, you can do something like this:
$scope.$on("$destroy", function handler() {
win.off("focus", onFocus);
win.off("blur", onBlur);
$interval.cancel(interval);
});
Solution inspired by this post: https://www.bennadel.com/blog/2934-handling-window-blur-and-focus-events-in-angularjs.htm
you can write a directive to attach to the body element and inside it you can use $window.onfocus event to notify your angular app using events or a service, the same thing you can do from inside a service, it all depends on your architecture

ng-blur is not called from a function triggered by ng-keyup

so I have a ng-blur function that correctly fires if I do this
$scope.disableReturnButton = function($event){
document.onkeyup=function(e) {
if(e.which == 13){
$event.target.blur();
return false;
}
}
}
however if I try to set the keyup event on the element only, it doesn't seem to trigger ng-blur when the blur event happens
$scope.disableReturnButton = function($event){
if($event.which == 13){
$event.target.blur();// this blurs the element but doesn't trigger the ng-blur
return false;
}
}
here's the markup
<span contenteditable="true" class="blanks"
ng-blur="blurredWhy($event)"
ng-focus="focusedWhy($event)"
ng-bind="data.why"
ng-keyup="disableReturnButton($event)"></span>
If you use return false; in the event handler, with jQuery it will stop propagation and stop other events from firing in the chain. Since Angular uses jQuery lite (or you have jQuery full as well on the page) returning false will kill upstream events.
The first example would work because it uses the document.onkeyup which registers the event listener outside of jQuery.
See event.preventDefault() vs. return false

$watch on clientWidth isn't working

Inside of my directive I have this and it works only in the beginning, and whenever I resize later it never fires.
scope.$watch ->
cw: element[0].clientWidth
,(newValue, oldValue)->
if newValue.cw isnt oldValue.cw
console.log "changed"
,true
Your function looks right to me, but it's important to note that it won't fire on resize unless you manually trigger a digest on the resize event.
Without this, angular doesn't realise there's been an event that should trigger a digest, and so none happen.
Try adding this (remember to inject $window):
angular.element($window).bind('resize', ()->
scope.$apply();
)
Just like Jason Goemaat said, its because Watches are only checked on a $digest cycle
Depending on how the way your element is resized maybe this example may help you.
Here is a menu that expands it self when pressed on the arrow.
<div class="menu" ng-class="{'expanded' : main.expanded}">
<div class="menu-handler" ng-click="main.expanded = !main.expanded">
</div>
</div>
The menu is expanded by a ng-click that makes a change on the scope, in this case its a boolean var that acts as a flag. As the opening of the menu is made throught the change in the scope it calls the $digest cycle iterating through all $watch.
scope.$watch(function() {
return element[0].clientWidth;
}, function(oldValue, newValue) {
console.log(oldValue, newValue);
});
You can view the full example in this JSfiddle
https://jsfiddle.net/owenbcal/zf17s0mL/
Hope i was helpfull.
the way I know about watching this kind of values is making a function that is fired once the value changes so then I watch that function :
in the service:
this.getDataOut = function () {
return self.dataOut;
};
in the controller:
$scope.$watch(dataService.getDataOut, function() {
$scope.dataOut = dataService.dataOut;
});

Adding ng-change to child elements from linking function of directive

I created a directive that should add a ng-change directive dynamically to all child input tags:
myApp.directive('autosave', function ($compile) {
return {
compile: function compile(tElement, tAttrs) {
return function postLink(scope, iElement, iAttrs) {
var shouldRun = scope.$eval(iAttrs.autosave);
if (shouldRun) {
iElement.find(':input[ng-model]').each(function () {
$(this).attr("ng-change", iAttrs.ngSubmit);
});
$compile(iElement.contents())(scope);
console.log("Done");
}
}; //end linking fn
}
};
});
The problem that I have is that the ng-change directive isn't running. I can see it that its added to the DOM element BUT not executing when value changes.
The strange thing is that if I try with ng-click, it does work.
Dont know if this is a bug on ng-change or if I did somehting wrong.
Fiddle is with ng-click (click on the input) http://jsfiddle.net/dimirc/fq52V/
Fiddle is with ng-change (should fire on change) http://jsfiddle.net/dimirc/6E3Sk/
BTW, I can make this work if I move all to compile function, but I need to be able to evaluate the attribute of the directive and I dont have access to directive from compile fn.
Thanks
You make your life harder than it is. you do'nt need to do all the angular compile/eval/etc stuff - at the end angular is javascript : see your modified (and now working) example here :
if (shouldRun) {
iElement.find(':input[ng-model]').on( 'change', function () {
this.form.submit();
});
console.log("Done");
}
http://jsfiddle.net/lgersman/WuW8B/1/
a few notes to your approach :
ng-change maps directly to the javascript change event. so your submit handler will never be called if somebody uses cut/copy/paste on the INPUT elements. the better solution would be to use the "input" event (which catches all modification cases).
native events like change/input etc will be bubbled up to the parent dom elements in the browser. so it would have exactly the same to attach the change listener to the form instead of each input.
if you want to autosave EVERY edit that you will have an unbelievable mass of calls to your submit handler. a better approach would be to slow-down/throttle the submit event delegation (see http://orangevolt.blogspot.de/2013/08/debounced-throttled-model-updates-for.html ).
if you want to autosave EVERY edit you skip your change handler stuff completely and suimply watch the scope for changes (which will happen during angular model updates caused by edits) and everything will be fine :
scope.watch( function() {
eElement[0].submit();
});

Resources