Handling Window Blur And Focus Events In AngularJS
Out of the box, AngularJS has ngBlur and ngFocus directives for handling blur and focus events, respectively. And, these work great for form inputs. However, they don't really help you if you want to know when the user has blurred or focused the actual browser window. If you're building "component directives", you can always bind to the window-based events in your link() function. But, I thought it would be a fun exercise to create window-oriented blur and focus directives for a more generalized use-case.
Run this demo in my JavaScript Demos project on GitHub.
To explore this idea, I'm going to create a small application in which we change the browser's window title when the user blurs the window. Then, when the user returns to the window, we'll revert back to the original title.
To accomplish this, I could simply have bound to the window blur and focus events within the "title" directive link() function and called it a day. But, again, as an exercise (and to create a more flexible solution), I wanted to create a more generalized use-case. As such, I've created two new directives - bn-window-blur and bn-window-focus - that can evaluate arbitrary expressions when the window events are fired.
In the following code, I'm using these two new directives to invoke methods on the main AppController. The AppController will then change the value stored in the "windowTitle" service. And, the "title" directive will render those changes in the DOM (Document Object Model), effectively changing the title of the browser window.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<!-- CAUTION: Title is a directive that will set the window title. -->
<title>
Handling Window Blur And Focus Events In AngularJS
</title>
</head>
<body
ng-controller="AppController as vm"
bn-window-blur="vm.setAway()"
bn-window-focus="vm.setBack()">
<h1>
Handling Window Blur And Focus Events In AngularJS
</h1>
<p>
Hello, thanks for visiting my site!
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
angular.module( "Demo" ).controller(
"AppController",
function AppController( $scope, windowTitle ) {
var vm = this;
// Expose public methods.
vm.setAway = setAway;
vm.setBack = setBack;
// ---
// PUBLIC METHODS.
// ---
// I handle the window blur event.
function setAway() {
windowTitle.setAwayTitle( "Bro, come back!" );
}
// I handle window focus event.
function setBack() {
windowTitle.removeAwayTitle();
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I expose Window-based focus and blur events.
angular.module( "Demo" )
.directive(
"bnWindowBlur",
function bnWindowBlurDirective( $window, $log ) {
// Return the directive configuration object.
return({
link: link,
restrict: "A"
});
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes ) {
// Hook up blur-handler.
var win = angular.element( $window ).on( "blur", handleBlur );
// When the scope is destroyed, we have to make sure to teardown
// the event binding so we don't get a leak.
scope.$on( "$destroy", handleDestroy );
// ---
// PRIVATE METHODS.
// ---
// I handle the blur event on the Window.
function handleBlur( event ) {
scope.$apply( attributes.bnWindowBlur );
$log.warn( "Window blurred." );
}
// I teardown the directive.
function handleDestroy() {
win.off( "blur", handleBlur );
}
}
}
)
.directive(
"bnWindowFocus",
function bnWindowFocusDirective( $window, $log ) {
// Return the directive configuration object.
return({
link: link,
restrict: "A"
});
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes ) {
// Hook up focus-handler.
var win = angular.element( $window ).on( "focus", handleFocus );
// When the scope is destroyed, we have to make sure to teardown
// the event binding so we don't get a leak.
scope.$on( "$destroy", handleDestroy );
// ---
// PRIVATE METHODS.
// ---
// I teardown the directive.
function handleDestroy() {
win.off( "focus", handleFocus );
}
// I handle the focus event on the Window.
function handleFocus( event ) {
scope.$apply( attributes.bnWindowFocus );
$log.warn( "Window focused." );
}
}
}
)
;
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I manage the window title.
// --
// NOTE: I only manage the title value - the actual application of the title to
// the DOM (Document Object Model) is handled elsewhere.
angular.module( "Demo" ).factory(
"windowTitle",
function windowTitleFactory() {
var title = "";
var awayTitle = "";
// Return the public API.
return({
getTitle: getTitle,
removeAwayTitle: removeAwayTitle,
setAwayTitle: setAwayTitle,
setTitle: setTitle
});
// ---
// PUBLIC METHODS.
// ---
// I return the currently-active title.
function getTitle() {
return( awayTitle || title );
}
// I remove the inactive window title.
function removeAwayTitle() {
awayTitle = "";
}
// I set the inactive window title.
function setAwayTitle( newAwayTitle ) {
awayTitle = newAwayTitle;
}
// I set the active window title.
function setTitle( newTitle ) {
title = newTitle;
awayTitle = "";
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I hook the windowTitle service up to the DOM.
angular.module( "Demo" ).directive(
"title",
function titleDirective( windowTitle ) {
// Return the directive configuration object.
return({
link: link,
restrict: "E"
});
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes ) {
// Extract the current window title and use that as a means to set
// the initial value of the title service.
windowTitle.setTitle( element.text() );
// Now, let's watch the window title for changes.
scope.$watch( windowTitle.getTitle, handleTitleChange );
// ---
// PRIVATE METHODS.
// ---
// I handle title changes and update the DOM accordingly.
function handleTitleChange( newTitle ) {
element.text( newTitle );
}
}
}
);
</script>
</body>
</html>
As you can see, when the user blurs the window, the AppController sets the title to, "Bro, come back!" And, when the user returns, the AppController reverts to the previous title. When we run this code and blur and focus the window a few times we get the following:
NOTE: In the following screenshot, I am in a "blur" state since I am focusing the Chrome Dev Tools.
Again, if you're building "component directives," these blur and focus events might be something that you just bind to in the link() function of your component. But, I think there's always value in a general-use case that can be applied to any Controller.
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben,
Nice directives ;-)
I think the Visibiliy API is more appropriate to trigger the focus/blur of the window element: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
Furthemore it is quite well supported http://caniuse.com/#feat=pagevisibility
@Simon,
Ah, very cool. I have not seen those DOM events before. I'll have to check it out. Thanks for the links.
very cool Ben! as always :)
Maybe you can use just
scope.$evalAsync( attributes.bnWindowBlur );
or just scope.$eval()
instead of $apply.
$apply is $EVIL!
:)
@Martin,
Ha ha, $apply() is not evil :) $apply() is how you tell AngularJS that the view-model has changed. Without that, you might end up changing something somewhere but that change won't be "rendered" until the next $digest runs.
And, $evalAsync() won't help you in this case. Since the window blur/focus events happen outside of the $digest lifecycle, you can't just tack on a new evaluation to the end of the current digest. As such, calling $evalAsync() will just create a timeout() which will end up triggering a $digest in the next tick.
Bottom line, when you change the view-model from outside of the AngularJS "world", you have to tell AngularJS about it or otherwise changes may not be propagated in a timely manner.
@Ben,
haha,
yes I am aware of everything you said master! :)
$scope.$apply is not always the best choice because the dirty checking has to run through whole scope tree, which may be slow in some cases :)
In some cases scope.$digest is sufficient.
That was my point.
and of course,
$evalAsync works :)
// your code but $evalAsync instead of $apply
http://codepen.io/Hotell/pen/MaEXGM?editors=101
// demo showcase
http://s.codepen.io/Hotell/debug/MaEXGM?
@Martin,
Correcty, $evalAsync() will work; but, it's not really getting you much except putting the $digest triggering in a future tick of the event loop. I agree that $evalAsync() is good and can definitely have a positive effect on performance when you are currently in a $digest loop since it will get flushed later on the loop life-cycle. But, if you are not currently in the $digest life-cycle, then $evalAsync() is essentially the same as calling $apply(), only in another tick.
Splitting hairs :)
@Martin,
The more I reflect on it, I don't think there is a *downside* to using $evalAsync(). If you're currently in a $digest loop, it just works. And, if you're not in a $digest loop, it just works (albeit with a timeout() in between). So, I guess, to some degree, it's sort of a "best of both worlds."
Perhaps, some of my push-back comes from the fact that earlier versions of AngularJS didn't have the "timeout" portion of $evalAsync(). Meaning, if you weren't in the middle of a $digest, your view-model change would just hang out there, unrendered, until something else triggered a new $digest.
Of course, there is something nice about explicit code.
@Ben,
yup exactly, I've also used $apply for situations like in your example and because $evalAsync just wasn't there.
thanks for chat! I love your posts and hope that one day I'll be able to buy you a beer for your effort to the community.
cheers!
@Martin,
Always a pleasure :)
This is perfect... I am working on a webapp project where users login with linkedIn. But apparently the standard security settings in iOS Safari does not allow communication between browser windows. Therefore, the user is logged in, but the webapp never gets the message, and the user has to reload for it to take effekt.
I realized that the app receives focus back, when the linkedIn login browser window closes. So I got around it by checking window focus/blur. If user is at the login state with a safari browser, the app simply reloads whenever focus is received. It's an ugly hack, but it works... Thanks! :-)
@Jette,
Ah, very cool. Seems like an elegant enough solution to me :)