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"/>
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.