Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jonathan Smith
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jonathan Smith

scope.$apply() May Return A Value In AngularJS

By Ben Nadel on

When you're inside an AngularJS directive, and you want to make a change that AngularJS needs to know about, you can call the scope.$apply() method. This evaluates the given expression (in the context of the current scope) and then triggers a digest on the $rootScope. As I was digging through the AngularJS source code the other day, I realized that the $apply() method will return the result of the evaluated expression. I hadn't seen this previously (although it is documented), so I wanted to give it a try.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

While the scope.$apply() method will trigger a digest, there is another benefit to calling it - error handling. When AngularJS goes to evaluate the expression passed into the $apply() method, it does so inside a try/catch block. If any exception is raised, AngularJS will pass it off to the $exceptionHandler() so that your calling context doesn't break.

This means that the scope.$apply() method will return the evaluated expression; or, it will return nothing. But it won't break. This "possible return" value made think of something that Kyle Simpson recently said (I can't find the reference) - wrap untrusted return values in a promise in order to normalize them. This sounds like something that might be totally appropriate for scope.$apply().

To experiment with this, I created an AngularJS directive that pipes a click event into a scope-based handler. The handler will return a promise that the directive can use to manage its internal state; but, if the handler raises an exception, the directive will normalize the response and continue to manage its own state.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • scope.$apply() May Return A Value In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • div.zone {
  • background-color: #FAFAFA ;
  • border: 2px solid #CCCCCC ;
  • cursor: pointer ;
  • padding: 20px 20px 20px 20px ;
  • text-align: center ;
  • }
  •  
  • div.zone.active {
  • border-color: #FF00CC ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • scope.$apply() May Return A Value In AngularJS
  • </h1>
  •  
  • <div bn-clicker="processClick( x, y )" class="zone">
  • Click me to do stuff.
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.26.min.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.
  • app.controller(
  • "AppController",
  • function( $scope, $q, $timeout ) {
  •  
  • $scope.processClick = function( x, y ) {
  •  
  • var deferred = $q.defer();
  •  
  • // Mimic some sort of processing latency.
  • $timeout(
  • function handleDeferredResolution() {
  •  
  • console.info( "Processed click at {%d,%d}", x, y );
  •  
  • deferred.resolve();
  • x = y = deferred = null;
  •  
  • },
  • 1000,
  • false // No need for apply - no model was changed.
  • );
  •  
  • return( deferred.promise );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I handle a clickable area and pass the {x,y} coordinates to the given handler.
  • app.directive(
  • "bnClicker",
  • function( $q ) {
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • element.on(
  • "click",
  • function handleClickEvent( event ) {
  •  
  • var position = element.position();
  •  
  • // Calculate "local" click coordinates.
  • var coordinates = {
  • x: ( event.pageX - position.left ),
  • y: ( event.pageY - position.top )
  • };
  •  
  • // Setup the active state.
  • element.addClass( "active" );
  •  
  • // The processing handler is supposed to return a promise.
  • // That promise is then being pulled back through the $apply().
  • var promise = scope.$apply(
  • function applyClickToScope() {
  •  
  • // Pass the return value of the handler back through
  • // the return value of the $apply() invocation.
  • // --
  • // CAUTION: If the handler - processClick() - raises
  • // an exception, the return value will be undefined.
  • return( scope.processClick( coordinates ) );
  •  
  • }
  • );
  •  
  • // When the event has been processed, teardown the active
  • // state. However, since the promise may not be valid (if the
  • // handler raised an exception), it can be safer to wrap the
  • // "unknown" promise in another promise to normalize it.
  • $q.when( promise ).finally(
  • function handleFinally() {
  •  
  • element.removeClass( "active" );
  •  
  • }
  • );
  •  
  • // Clear closed-over variables.
  • event = position = coordinates = promise = null;
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration. In this case, we are binding an
  • // isolate scope method to the directive attribute.
  • return({
  • link: link,
  • restrict: "A",
  • scope: {
  • processClick: "&bnClicker"
  • }
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we're evaluating the click handler inside the context of scope.$apply(). We're then pulling that return value, from the click handler, through the $apply() and into the calling context. We're then further normalizing that promise using $q.when().

Anyway, the return value of scope.$apply() is a rather minor feature. But I do like the idea of being able to get it and normalize it. I'm still letting this sink in, so if you have any constructive feedback on the approach, I'm all ears.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.