As the application scales in size, controlling communication between components requires enough thought to ensure there isn’t too much or too little of it. In this post we will look at the various ways of communicating in AngularJS 1.x. Some of these techniques may still apply in the Angular 2 world (eg: Events), however the actual mechanics will be different.
AngularJS 1.x offers several ways of communicating between components. These are based on the core abstractions that Angular provides, namely: Services, Directives, Controllers and of course the Scope.
Below, we explore these alternatives with some simple examples.
Data binding was the pivotal feature of Angular that got its initial popularity. By having a scope (the model) bound to a template you are able to replace placeholder mustache-like strings {{ model.prop }}
with their actual values from the scope (aka model). This way of expanding templates to build pages is very convenient and productive. Here, Scope acts as the binding glue to fill in values for the mustache-strings. At the same time, scope also has references to event-handlers that can be invoked by interacting with the DOM.
Note that these placeholders automatically create a two-way binding between the model and the DOM. This is possible, as you already know, via the watchers. Also worth mentioning is that with Angular 1.3, you can create one-time bindings with {{ ::model.prop }}
syntax. Make sure you put the ::
.
The example below shows the controller and its usage in the template. The key part here is the use of scope (the binding glue) to read model values as well provide interaction.
angular.module('exemplar').controller('MidLevelController', function($scope) {
$scope.midLevelLabel = 'Call Mid Level';
$scope.midLevelMethod = function() {
console.log('Mid Level called');
};
});
<div class="panel mid-level" ng-controller="MidLevelController">
Mid Level Panel
<div class="panel bottom-level">
<button ng-click="callMidLevelMethod()">{{ midLevelLabel }}</button>
</div>
</div>
Scopes in Angular directives (and Controllers) prototypically inherit from the parent scopes. This means a child-directive (or Controller) is able to reference and use properties of its parent scope just by knowing the properties. Although not a recommended approach, this can work well for simple directives that are not using Isolate Scopes. Here there is an implicit contract between the parent and child directives (or Controllers) to share a few properties.
In the example below, you can see that the BottomLevelController
is able to invoke a method on the TopLevelController
purely because of the prototypical scope.
<div class="panel top-level" ng-controller="TopLevelController">
Top Level Panel
<div class="panel mid-level" ng-controller="MidLevelController">
Mid Level Panel
<div class="panel bottom-level" ng-controller="BottomLevelController">
<button ng-click="callMidLevelMethod()">{{ midLevelLabel }}</button>
<button ng-click="topLevelMethod('Bottom Level')">Call Top Level</button>
</div>
</div>
</div>
And here are the controllers:
angular
.module('exemplar')
.controller('TopLevelController', function($scope) {
$scope.topLevelMethod = function(sender) {
console.log('Top Level called by : ' + sender);
};
})
.controller('MidLevelController', function($scope) {
$scope.midLevelLabel = 'Call Mid Level';
$scope.midLevelMethod = function(sender) {
console.log('Mid Level called by: ' + sender);
};
})
.controller('BottomLevelController', function($scope) {
$scope.callMidLevelMethod = function() {
$scope.midLevelMethod('bottom-level');
};
});
When there is a natural, nested relationship with directives, it is possible to communicate between them by having the child depend on the parent’s Controller.
This is generally done within the child-directive by providing a
link()
function and a dependency on the parent directive’s controller. The dependency is established using therequire
attribute of the child’s directive-definition-object.
You can even depend on more controllers from the parent chain by using the array syntax. They all show up as the fourth parameter in the
link()
function. {” Note, for this to work, the parent-directive must have a Controller defined. “}
Consider the example below where we have nested directive structure:
<parent-component>
<child-component></child-component>
</parent-component>
Here we can wire the <child-component>
and <parent-component>
with the following directives. Note line#23 where we require
the parent controller and line#25 where we take in the instance in the link
function.
angular
.module('exemplar')
.directive('parentComponent', function() {
return {
restrict: 'E',
templateUrl: 'parent-child-directive/parent-component.template.html',
transclude: true,
controller: ParentComponentController,
};
function ParentComponentController($scope) {
var vm = this;
vm.takeAction = function() {
console.log('The <child-component> called me');
};
}
})
.directive('childComponent', function() {
return {
restrict: 'E',
require: '^parentComponent',
templateUrl: 'parent-child-directive/child-component.template.html',
link: function(scope, element, attrs, parentController) {
scope.notifyParent = function() {
parentController.takeAction();
};
},
};
});
Services are the singletons of Angular that are used to capture behavior. However by the virtue of being singletons, they also act as shared storage and can be used to aid communication between disparate components (Directives). The communicating parties depend on the shared service and use the methods on the service to do the communication.
In the example below, you can see a simple clipboardService
that provides a shared storage for the copyButton
and pasteButton
directives.
(function() {
angular.module('exemplar').factory('clipboardService', serviceFunction);
function serviceFunction() {
var clipboard = {};
return {
copy: function(data, key) {
/* ... */
},
get: function(key) {
/* ... */
},
};
}
})();
angular
.module('exemplar')
.directive('copyButton', function(clipboardService) {
return {
restrict: 'A',
link: function(scope) {
scope.performCopy = function() {
// Invoke Copy
clipboardService.copy({}, 'abc');
};
},
};
})
.directive('pasteButton', function(clipboardService) {
return {
restrict: 'A',
link: function(scope) {
scope.performPaste = function() {
// Fetch from clipboard
var data = clipboardService.get('abc');
/* ... Handle the clipboard data ... */
};
},
};
});
Events are the cornerstones of all UI Frameworks (or any event-driven framework). Angular gives you two ways to communicate up and down the UI tree. Communicate with parents or ancestors via $emit()
. Talk to children or descendants via $broadcast()
.
As an extension, you can talk to every component in the app via $rootScope.$broadcast()
. This is a great way to relay global events.
On the other hand, a more focused $rootScope.$emit()
is useful for directed communication. Here $rootScope
acts like a shared service. Communicating with events is more like message-passing where you establish the event-strings and the corresponding data that you want to send with that event. With the right protocol (using some event-string convention) you can open up a bi-directional channel to communicate up and down the UI tree.
In the example below, you can see two controllers (DeepChildController
and RootController
) which communicate using the $rootScope
. With $rootScope
, you get a built-in shared service to allow any child component to communicate with the root.
angular
.module('exemplar')
.controller('DeepChildController', function($scope, $rootScope) {
$scope.notifyOnRoot = function() {
$rootScope.$emit('app.action', { name: 'deep-child' });
};
})
.controller('RootController', function($scope, $rootScope) {
$rootScope.$on('app.action', function(event, args) {
console.log('Received app.action from: ', args.name);
});
});
<div class="root" ng-controller="RootController">
<!-- Some where deep inside the bowels of the app -->
<ul>
<li>One</li>
<li>Two</li>
<li ng-controller="DeepChildController">Three has
<button ng-click="notifyOnRoot()">Talk to Root</button>
</li>
</ul>
</div>
Although you may be using Angular, you are not limited to doing everything the angular way. You can even have side communication outside of Angular using a shared bus from the PubSub model. You could also use WebWorkers for running intensive operations and showing the results via Angular.
The catch here is that once you want to display results on the DOM, you will have to enter the Angular context. This is easily done with a call to $rootScope.apply()
at some point where you obtain the results.
Now the question is how do you get the $rootScope outside of angular. Well, below is the snippet. Here we are assuming your ng-app is rooted on document.body
.
// Get $rootScope
var rootScope = angular
.element(document.body)
.injector()
.get('$rootScope');
// Trigger a $digest
rootScope.$apply(function() {
// Set scope variables for DOM update
});
Communicating at scale (inside your app) comes with a few gotchas and can seriously affect performance. For example, if you are listening to a streaming server that is pumping market data every second, you may be running $scope.$digest()
or $rootScope.$digest()
every second!. You can imagine the turmoil it will cause in terms of performance. End result: An ultra-sluggish app.
One of the most popular techniques to handle high volume communication is to debounce
the handler. This ensures that the actual event is only handled once in a defined time interval. So for the duration of the interval, events are ignored. Debouncing can be introduced at various places in your data-pipeline to control the burst of events.
Note: If you don’t want to ignore the data in an event, you can buffer it for use at the end of the interval. In general, batching is universal for controlling volume. It is much more efficient to combine several small activities into one batched-activity.
Communication within an application, just like building software, is a mixture of Art + Science. The science part is the mechanics of communication, many of which we have seen above. The art is knowing when to employ the right one! Using the right methods can make a big difference to the maintainability, stability and overall performance of your application.
Although we have used Angular as a pretext to discuss these communication styles, many of them are universal and apply to any JavaScript application. I’ll leave it as an exercise for you to map to your own favorite framework.
Question: Have I missed out any particular technique here? Something that you have used effectively? Please do share in comments.