Technical • April 14, 2014 • Saurabh Nanda
We’ve been building Single Page Apps” (SPAs) with AngularJS over the last few months and have been blown away with what it enables us to achieve. Our front-end code was a stinking pile of jQuery DOM manipulation, ad-hoc data attributes, and Handlebars templates earlier. With the super-heroics of AngularJS is has now transformed to a well laid out, comprehensible, & maintainable stack of client-side MVC code.
But this transformation happened after we wrapped our head around the AngularJS way of doing things. And on that steep learning curve, one of the areas which made us scratch our heads was how to get controllers & directives to “talk to each other”. So here’s a quick guide of what we’ve learnt so far. Hope this helps others climbing the AngularJS hillock.
The simplest way to get a “two-way communication” between a controller and a directive is to set-up a shared object between the two via an ‘=’ scope binding. The controller & the directive, both, can set-up watchers on the shared object to be notified of changes to it, and can thus use it as a way to communicate with each other.
Another way to get controllers & directives to talk to each other is via services. However, the biggest difference is that services are singleton objects within a module — ALL instances of controllers and directives injecting the said service will share the SAME single object.
Services make a good use-case for sharing app-wide data with all the instances of a directive. For example, if you need to display monetary/currency amounts at multiple places throughout your app, with the currency & precision being a configurable setting (via another form, which is part of the same SPA), a SettingsService would be an ideal choice.
In the sample code below, ALL instances of the currencyDisplay directive “talk” to the SettingsController “via the” SettingsService. In other words, ALL instances of the currencyDisplay directive share ONE SINGLE object with the SettingsController, i.e. the SettingsService.
You’ve probably used these little beasts extensively with ng-change & ng-click without much thought. Basically these enable the directive to “notify” the controller by executing a piece of JS (generally a function) in the context of the controller’s scope.
A possible use-case is where the look & feel of a directive needs to be consistent across multiple instances of the directive, whereas the response to certain external stimuli (eg, clicking the “next” button, clicking the “save” button, etc) needs to be different.
For example, we’ve used this technique extensively for an in-place editor that we built for internal use. Notice that the directive copies the value of the model passed to it into new_model and uses that as an ng-model for the input box. The controller updates the value of the model only if the remote AJAX call succeeds. This prevents the on-screen UI from changing if the value has not actually changed in the backend.
Keep in mind that you can’t pass arguments to an ampersand-binding like you would with a normal function call. This is because an ampersand-binding is NOT a regular function, it’s a function which wraps the expression that is passed to it. This enables you to pass bare expressions to the binding without necessarily declaring a function.
For example you can do something like:
<div current-input="amount" on-change="total=total+new_amount" ></div>
And invoke the onChange binding like:
scope.onChange({'new_amount' : amount_from_directive});
The advantage of using events over shared objects is that events let you communicate across scopes. Using scope.$broadcast() the directive can communicate “upward” in the scope hierarchy. Any controller “above” the directive’s scope can listen to the event and, thus receive notifications from the directive. On the other hand, any controller can use scope.$emit() and push notifications to a directive “lower down” in the scope hierarchy.
We ended up using events to get controllers & directives to talk to each other while implementing keyboard shortcuts for an SPA. jQuery’s keypress event is bound to a particular DOM element, which means that if a keypress happens when the focus is within the DOM element, the event handler will be fired. If you require keyboard shortcuts to work throughout your app (irrespective of where the focus is), you need to register the jQuery event handler either with body
or with a container div high-up in the DOM hierarchy. This can easily be done via a custom directive. However, the controller which needs to react to the keyboard shortcut could be anywhere in the scope hierarchy. How does the directive “notify” an interested controller about the keypress? A possible solution is by using events, as demonstrated below.
Notice, how both the controller are listening to the same event and performing different operations whenever the event occurs.
An alternative solution could be via creating a KeyboardShortcutQueueService which is injected into the directive and all “interested” controllers. The directive would place a keypress event into the KeyBoardShortcutQueue, which all controllers could setup watchers on.
That’s it folks! A quick overview of how to get directives and controller to communicate in AngularJS. Hope this is helpful for someone.