Chapter 4 - AngularJS Component Primer Part 2: Models and Scope
In the last chapter we discussed directives, which are created by AngularJS developers to encapsulate custom HTML, custom extensions to HTML, and create almost fully self-contained UI components including presentation and behavior. We focused primarily on the presentation aspect of AngularJS. However, web applications and widgets are useless without the data they are built for. In this chapter we will discuss how data is represented, packaged and manipulated in the AngularJS universe with a focus towards loose coupling and componentization.
Data Models in AngularJS
Developers new to AngularJS might be somewhat confused when the landing page of angularjs.org mentions that AngularJS models are plain POJOs or plain old JavaScript objects, but then there is this thing called $scope which is the data that is bound to the HTML in AngularJS’ data-binding. The distinction is that the last sentence was not entirely correct. It should be “which is what holds” the data that is bound to the HTML. To be clearer, the data in AngularJS is modeled in a basic JavaScript object, which may be converted to JSON and visa versa. That model is then attached to an AngularJS $scope object which provides a context for the data in the DOM hierarchy.
4.0 A model and a context in AngularJS
// Model is a plain old JavaScript object- no getters, setters, or inheritance
// It may start life as a JSON string from a REST call or from form field inputs
var aDataModel = {
field_0: 'a string',
field_1: 5
field_2: false,
field_3: {
subField_0: ['an array']
}
};
// $scope is a contextual container for a model
function ourController($scope){
// Our data is now bound to the view, and so is another property and method
// not a part of our data.
$scope.data = aDataModel;
$scope.method_0 = function(){};
$scope.property_0 = '';
};
Models in AngularJS do not need to be part of any inheritance hierarchy or have OO style setters and getters, or other methods. On the other hand, scopes in AngularJS usually (but not always) are part of an inheritance hierarchy, and have a number of built-in methods and properties. Any developers with Backbone.js experience know that data models in that toolkit are inherited and have OO style encapsulation methods.
The reasoning behind using POJOs for models in AngularJS is for increased ease of testing, maintenance, and reusability. Tomorrow if we decide to switch to another framework or even Web Components, untangling the data will require minimal effort.
Data Representation in the View
The $scope object in AngularJS is the container that allows both the view and the controller access to the data. When we attach our data object to a $scope object in AngularJS we have effectively created a ViewModel as most developers would define MVVM. The AngularJS team refer to AngularJS as an MVC framework perhaps for marketing purposes or for attractiveness to back-end developers, but the distinction between VM and C is fuzzy at best and probably depends more on whether it is used for web apps or components.
Expressions and Bindings
In compiled template HTML, or the view, $scope data fields, methods, and properties are normally accessed and manipulated via AngularJS expressions. AngularJS expressions are snippets of JavaScript with a few distinctions. Expressions evaluate against the scope of the current element or directive, whereas, JavaScript expressions embedded in HTML always evaluate against the global (usually window) context. Attempting to evaluate a property that is null or undefined fails silently rather than throwing a reference error, and control flow logic (if, else, while, etc.) is not allowed since computational logic belongs in controllers, not templates.
4.1 Difference between AngularJS and JavaScript expressions
// setting and accessing a scope context
<div id="expExample"
ng-controller="ourController"
ng-click="angularExpression()"
onclick="globalExpression()">
<span> {{ aScopeProperty }} </span>
<input type=text ng-bind="aScopeProperty"/>
</div>
The code above is short, but represents much of a developer’s everyday use of AngularJS. We are applying a scope to div#expExample, and binding properties and methods to elements on or within the scope.
The default AngularJS binding delimiters are {{}}. The can be changed if there is conflict with other client or server template delimiters. {{}} is the read-only version of ng-bind. {{}} is typically used when displaying a scope property as output text in HTML. ng-bind is typically used to give two-way binding to form input elements, although it can also be used in cases where a delay in the JavaScript execution thread might cause an undesired momentary flash of the raw “{{ var }}”. Behind the scenes, the expressions are passed to the $eval() method on the $scope object analogous to JavaScript’s eval() method with the exception that $eval() evaluates against the $scope context. It is a best-practice to keep statements for $eval() simple as in a property name or a function call that returns a value.
$scope creation and destruction
Automatic Scopes
The concept of scope, scope hierarchies and other inter-scope relationships are a source of massive hair loss among AngularJS noobs. “Why does a bound property change when I do this, but not when I do that?” The answer is almost always because of an attempt to access the same property, but within different, unexpected scope contexts due to a scope popping up out of nowhere for some reason. So it important to understand the various AngularJS activities that result in automatic scope creating- one of the downsides to a massive reduction in boilerplate.
So starting from the top, when an AngularJS app module is defined and attached to the DOM via ng-app, a root scope is automatically created on the ng-app element and is accessible from any sub scope via $rootScope. That said, my advice is to avoid at all costs directly referencing the $rootScope property from an inner scope as it leads to highly coupled dependencies. Accessing a $rootScope from a UI component directive scope should never be done.
In AngularJS single page apps, scopes are most often created when ng-controller=”myController” is placed in an element declaration. Any data-bindings within that DOM fragment will reference that scope until an inner element or directive that instantiates a new scope is reached.
Scopes are automatically created when a new template is inserted into the DOM via ng-route or ng-include. Same goes for a form element when given a name attribute, <form name="name">, and also for any directive that clones HTML templates, a new scope is created for each cloned template when attached to the DOM as with ng-repeat.
Manual Scope Creation
Behind the scenes, any scopes that are automatically created via one of the avenues above are done so with the $new() method, available on any existing $scope object. We may also purposely call $scope.$new()to create one manually, but the use cases are rare and is usually a clue that we are not doing something the AngularJS way.
Finally new scopes may be granted existence on purpose when a custom directive is instantiated, and we almost always want to create a new scope if our directive encapsulates a UI component. Recall the scope option in the directive definition object from the previous chapter. For the purpose of using directives to create re-usable UI components, we will focus closely on this type of scope creation.
Scope Inheritance Options for Directives
When defining a directive for the purpose of encapsulating a UI component, we typically want to do a few special tasks with any new scope that is created. If we omit the scope option in our directive definition object, the default is no new scope, the same as if we declare scope:false. On the other hand, if we declare scope:true, a new scope is instantiated when the directive executes, but it inherits everything, prototypal style, from the parent scope. This is the default for automatic scope creation, and the reason for the hair pulling when the property names are the same, but the values are not. Manipulating a value on a sub-scope creates a new variable overwriting the inherited value.
The third value for the scope option is scope:{}. This creates an isolated scope for the directive instance with no inheritance from any parent scope. Because UI components should not know about state outside their boundaries, we typically want to use this option for our component directives. One side note with isolate scopes is that only one isolate scope can exist on an element, but this is usually not an issue with elements designated as component boundaries.
The value for the scope option does not need to be an empty object literal. We can include default properties, values and methods for the isolated scope using object literal notation. Also, the previous statement of no inheritance was not entirely correct. We can also selectively and purposely inherit specific properties from a parent or an ancestor scope, and we can also selectively accept values of the directive element attributes. Both of these abilities create some interesting options for defining APIs on our custom UI components.
Isolate Scopes as Component APIs
While directives are a great tool for creating re-usable UI components, the primary focus of the framework is toward being comprehensive for web application functionality. Therefore, there is no official AngularJS best-practice for defining UI component APIs beyond choosing a method that allows for loose coupling and dependency injection. The framework gives us multiple options for dependency injection, configuration, and API communication from the containing DOM. The option used should be the best fit for the use case and the user.
Directive API as HTML Attributes
Suppose we have some epic user-story that requires we create a pallet of UI widgets and components that might be used by designers or junior developers to create websites or webapps. The consumers of our pallet might not have a high level of programming skill, so we would want to give them a method to include any configuration or data declaratively, or as close to plain English as possible.
For example, we could create a custom search bar directive with our organization’s look-and-feel that a page author could include via adding a custom element:
<my-search-bar></my-search-bar>
But this is of limited use since search bars are often needed in different contexts, locations, and with certain configurations.
If we were using jQuery UI widgets, any customization or configuration would need to happen by passing in a configuration object to the imperative statement the creates the widget on an element:
$('#elementId').searchBar({autoComplete:true});
This limits the accessibility of widget use to developers with an adequate knowledge of JavaScript.
However, via AngularJS directives we could give the page author the ability to pass in configuration information declaratively via element attributes:
<my-search-bar auto-complete="true"></my-search-bar>
removing the need to touch any JavaScript. This is accomplished by configuring the isolate scope options in the directive definition object to use the values of matched attributes on our directive element.
4.2 Isolate scope attribute options
<my-search-bar auto-complete="true"></my-search-bar>
myModule.directive('mySearchBar', function factory(injectables) {
var directiveDefinitionObj = {
template: ...,
restrict: 'E',
scope: {
autoComplete: '@', // matches an attribute of same name
anotherModelName: '@autoComplete' // if using a different name
},
controller: ...,
link: function postLink( ... ) { ... }
};
return directiveDefinitionObj;
});
Directive API as Selective Scope Inheritance
The downside of UI component APIs using static attribute values like the example above is the restriction that only string values may be passed in since that is what all HTML attribute values are, and the value is not a live binding.
If our use-case requires that the component consumer have the ability to pass in configuration information of all types and dynamically at run-time, then we more options available if the consumer is familiar with JavaScript and AngularJS at least somewhat.
By including a variable within AngularJS tags,
<my-search-bar auto-complete="{{ scopeVar }}"></my-search-bar>
as the value of an attribute we can create a live, read-only binding to a value on the parent scope. If scopeVar on the parent scope changes during the life of the directive instance, then so will the corresponding property on the directive scope.
Additional variable and method inheritance options for isolate scopes exist. Using ‘=’ instead of ‘@’ creates a live two-way binding between a value on a parent element scope and the directive scope, and using ‘&’ allows the scope to call a function within the parent element’s context. Both of these options can be useful for inner directives, but we should avoid using these options as UI component APIs since they allow a component directive to directly affect state outside its boundaries. If the world outside a component’s boundaries needs to react to some change in component state, its is much better to use the publish-subscribe pattern made available to AngularJS scopes via:
$scope.$emit(evtName, args) //propagates up the scope hierarchy
$scope.$broadcast(evtName, args) //propagates down the scope hierarchy
$scope.$on(evtName, listenerFn)
Any scope with the same $rootScope can communicate via events whether isolate or not. Note that while the publish-subscribe pattern is discouraged in favor of data-binding in the official AngularJS documentation, for maintaining loosely coupled UI components, it is necessary.
Built In $scope methods and properties
There are several methods included by AngularJS with every instantiated $scope object. They fall into a few different categories including:
- events for inter-scope communication
- data-binding and execution context facilitation
- programmatic creation and destruction
Decoupled Communication
As mentioned above, AngularJS provide three scope methods for inter-scope communication: $broadcast(), $emit(), $on().
$on() is an event listener function analogous to those provided by browser DOMs and jQuery. $on() takes two default arguments, a string representing an event name to listen for and an event handler function or reference to fire upon detection of the named event. The event handler function receives an AngularJS event object as its first parameter, and any parameters subsequently passed as additional arguments from $emit or $broadcast. The event object contains properties typical of JavaScript event objects such as currentScope, targetScope, stopPropagation, preventDefault, etc. See the docs at:
http://docs.angularjs.org/api/ng.$rootScope.Scope
…for the full list.
$on() allows any scope object in the page hierarchy to function as an event bus. The ng-app $rootScope and the root scopes of UI component directives are typical places to maintain an event bus. For our UI component directives we might wish to set up an event bus for any internal directives or controllers that need to communicate in some way when direct data-binding is not desirable or possible such as when inner directives are themselves UI components.
$broadcast() and $emit() are both used to fire events. Their arguments are identical, but their actions are in opposite directions. $broadcast() will propagate an event downwards through a scope hierarchy, and $emit() will propagate the event upwards.
The first argument to both is a custom event name (string). While any string may be used, it is advisable to follow what has become a JavaScript convention by including name-spacing with the event name separated by colons to help prevent any potential naming collisions.
$scope.$emit( 'myComponent:myController:eventName', args);
The any additional arguments to either $broadcast() or $emti() will be passed directly as parameters to any events handlers referenced in the matching $on() statements.
$emit() has special value when constructing directives that will function as UI components. Since it is considered very poor form to create UI components that can directly manipulate or influence any other component, page, or application state outside its boundaries. Publishing benign events that communicate a UI component’s state outside of its boundaries that another component may listen for and optionally react to is an acceptable alternative. It does not require a UI component to have any specific knowledge of the outside world.
In a sense, creating a UI component with a published set of events that it can emit can serve as a reverse API for a page application to react to any change in a component’s internal state without needing to know what really goes on inside the component.
4.3 Component Scope Pub-Sub Communication
// contrived example of an application reacting to a state change of an
// inner UI compomnent
<div ng-class="searchBarSize" ng-controller="myAppController">
<my-search-bar compact="true"></my-search-bar>
</div>
myModule.directive('mySearchBar', function factory(injectables) {...
}).controller('searchBarCtlr', function($scope){
// a maximize button in our scope is toggled
$scope.on('maximizeBttn:on', function(){
// ask the outer application to make me larger
$scope.$emit('mySearchBar:pleaseExpandMe');
});
// the same button is toggles again
$scope.on('maximizeBttn:off', function(){
// ask the outer application to make me smaller
$scope.$emit('mySearchBar:pleaseContractMe');
});
});
});
myApp.controller('myAppController', function($scope){
$scope.$on('mySearchBar:pleaseExpandMe', function(evtObj){
$scope.searchBarSize = 'large-search-bar';
});
$scope.$on('mySearchBar: pleaseContractMe', function(evtObj){
$scope.searchBarSize = 'small-search-bar';
});
});
Data Binding and Execution Context
$apply(), $digest(), $watch(), $watchCollection(), $eval(), and $evalAsync() are the built-in AngularJS $scope methods that perform the heavy lifting for the automatic data-binding which lies at the core of AngularJS’ popularity. During the normal course AngularJS application development, these functions are usually called automatically by the built in directives and run in the background. However, not everything fits neatly into the AngularJS universe, and there are instances when we will need to include these functions explicitly in our source or unit test code.
$apply(non_angular_expression) is used to wrap a function with code from outside the AngularJS context to execute within the AngularJS lifecycle and context. $apply ends up being utilized quite often when importing functionality from other toolkits and frameworks, browser DOM event callbacks, and window functions such as setInterval, setTimeout, XHR calls, etc. Behind the scenes, $apply() calls $eval() and $digest() to provide AngularJS context and lifecycle. It’s not always clear up front when some code should be wrapped in $apply(), but if there is a scope value that is not being updated when it should be or at all then an $apply() wrapper is likely needed. A typical scenario where $apply() is needed is if we use an external AJAX library and need to set a scope value from within the the onSuccess or onError callback function.
4.4 Explicit $apply() wrapper needed
// scenario where $apply must be called explicitly
myModule.directive('mySearchBar', function factory() {...
}).controller('searchBarCtlr', function($scope, $extAjaxLib){
$scope.ajaxCall = function(ajaxObj){
$extAjaxLib.send({ajaxObj}, function successCallback(data){
//this does not update $scope as expected
$scope.data = data;
//this does update as expected
$scope.$apply(function(){
$scope.data = data;
});
});
};
});
});
Because AngularJS hasn’t been around long enough to benefit from a large library of third party plugins at the same level as something like jQuery, we will often want to wrap third party plugins from other toolkits rather than recoding the needed functionality from scratch, and $apply() will need to be applied (pun intended) quite often in this case.
Besides $apply(), $watch(watchExpression, listener, objectEquality) is another built-in scope function that sometimes needs to be called explicitly, though not nearly as often. $watch() is used to register a listener callback function that fires when a $scope expression changes. A typical use-case for explicit use of $watch is when we wish to run some code in response to a change in a $scope value.
$watchCollection(object, listener) performs the same function as $watch() with the exception that it watches a the first level properties (shallow) of an object for any changes.
Extreme care must be exercised when explicitly calling $watch() that the listener function is able to complete quickly. It needs to be efficient, and avoid any long-running operations such as DOM access since it will likely be called many times during a single $digest() cycle.
$digest() is the scope function that kicks off AngularJS’ system of recursive dirty-checking of all scope properties that have registered $watch’s. $watch listeners execute and “watched” properties are rechecked until all “watched” properties on the scope have stabilized. There are actually about 30 pages of important AngularJS information in the previous two sentences. A complete discussion of everything that happens during the $digest cycle while very important to understand, is unfortunately beyond the scope of this title. The AngularJS developer’s guide (http://docs.angularjs.org/guide/scope) gives a good overview of all you should know about the internals of the $digest loop. Suffice to say that you should not need to ever call $digest() directly except perhaps as part of some unit test code. You might, however, want to detect that a $digest loop has occurred which can be done via a $watch listener function.
On a side note, it will be quite welcome when the ECMA Script 6 Object.observe() is finally implemented by all major browsers. Object.observe() will obsolete the need for $watches and dirty-checking in AngularJS removing the major performance issue inherent in AngularJS’ data-binding system. Experimental versions of Chrome with Object.observe() enabled show digest loop performance increase by a factor of 40-60.
$eval(angularExpression) and $evalAsync(angularExpression) are used to explicitly evaluate AngularJS expressions. The latter uses the setTimeout(0) trick to defer the run till the end of the current JavaScript execution queue. They are called automatically when $apply() runs and usually are only needed explicitly in test case code.
Creation and Destruction
$new(isolate), and $destroy() are scope methods that are used to explicitly create and remove scopes. We rarely need to use these explicitly in our source code as they are called automatically whenever we insert or remove compiled templates in the DOM via built-in directives or client-side routing. There may be times when we do need these functions while writing test code. Also, do note that $destroy() removes all child scopes, data and bindings to prevent memory leaks similar to jQuery’s remove() function, and it also broadcasts a $destroy event that may be useful for performing any additional cleanup tasks such as removing non-AngularJS bindings on the DOM hierarchy to be removed.
Debugging Scopes and Testing Controllers
As new $scope instances are created in every AngularJS app, they are given an $id property that is numbered in ascending sequence as the scope begins life. We can then inspect each scope in the app or component hierarchy referenced by $id with AngularJS Batarang for Chrome which can be got at the Chrome Web Store.
Batarang adds some quite useful extensions to the Chrome Developer Tools. The most useful is the ability to inspect the AngularJS scope properties for any element in the DOM the same as we can for styling, event listeners, and DOM properties. We can also inspect the hierarchy of scopes in the AngularJS app or component. Both of these are very helpful in uncovering where those mystery scopes came from or where those scope values went since AngularJS does a lot of automatic scope creation under the covers especially when it comes to forms, includes, etc.
Approaches to testing controllers vary widely, and it is beyond our scope to present a comprehensive discussion of testing. The AngularJS developer guide, tutorial, and recommended technical books for AngularJS application development cover controller testing thoroughly. However, there are some key points to remember.
First, it cannot be stated enough that any and all external dependencies must be injected into any controller function as parameters. Never reference anything in the DOM directly such as class or id names. The same goes for all global functions and properties. All of these either have a preexisting AngularJS wrapper such as $location, $window, $document, $log, or it is a simple matter to create your own wrapper as an AngularJS service. Ideally we want all of our functions in our AngularJS source code to exhibit the highest degree of functional transparency as possible.
Second, it is highly recommended that controller tests inject both $rootScope, and $controller in order to manually instantiate a new scope using $new() and manually instantiate a new controller instance immediately after.
Summery
In the previous two chapters we discussed using the AngularJS framework to create and interact with the view via built-in and custom directives (chapter 3), and to interact with and enhance the model (chapter 4). Along the way we discussed strategies and best-practices for constructing UI component APIs and interfaces through creative use of AngularJS built-in binding and injection schemes.
We learned that we can create UI component APIs using element attributes, transclusion, and event publish and subscribe that can allow our components to exist and function without any direct knowledge of any outside DOM.
In the next section we will explore some examples of real-world user stories where UI components created with AngularJS can solve enterprise level problems such as excessive source code bloat, lack of consistent look and feel, and inability to share, export and reuse UI code across a large organization.