In this post I cover the principle and practice of two-way binding in an AngularJS environment, with an emphasis on “the dot” and live views. An understanding of Javascript is definitely required, as is a basic – but not advanced – understanding of AngularJS.
Among AngularJS’s many interesting and useful features, two-way binding is perhaps one of the better known. Google “AngularJS” and practically any site pitching AngularJS lists two-way binding as a main framework benefit. Certainly two-way binding is awesome to see in action, and the easy implementation is impressive for those with a background in an imperative library like jQuery where such behavior requires a couple lines of code to implement. Less well understood are all the moving parts hidden by the black box that drives the magic of two-way binding. In this post we open the black box and study the moving parts for ourselves.
The MVC Model vs The Angular Model
In traditional MVC frameworks models are the application’s connection to the backend data source. When the application is used model’s are accessed (by the controller) and the retrieved data is blended with the view template to form the version with which the user ultimately interacts with. As the user clicks around, the controller continues to query the model for data. Once the data is returned it becomes available to the view at “render” time – usually on page reload. AJAX helps with some of this by eliminating the burden of page reloading just to update potentially small data sets. Nevertheless, an explicit view change is still required and, more importantly, additional effort was required from the developer to provide this functionality.
AngularJS takes a different approach to the model concept. The view and the model are intertwined in an AngularJS application. Views are considered a projection of the current model state, as data sourcing from the view is handled by the model and then turned around and fed right back into the view. Immediately. In fact, no developer effort is required beyond the simplest of configurations:
demo.tpl.html
<body data-ng-app=”demoApp”> <div data-ng-controller="DemoController as demo" <input type="text" data-ng-model="firstName" /> <h3>{{ firstName }}</h3> </div> </body>
demo.ctrl.js
var demoApp = angular.module(‘demoApp’,[]); demoApp.controller( DemoController, function ( $scope ) { });
Note how the controller is empty? In the most trivial implementation the model and view are capable of instant, bidirectional data sharing. No user defined controller required. This tight relationship is why models are sometimes called “the single source of truth” -they exist only to move around and manipulate data in the view.
Power of Binding
Using regular HTML for view templating is another well advertised AngularJS feature. Elements without any AngularJS hooks are ignored once the framework takes over DOM processing, following the DOMContentLoaded browser event. This feature is how AngularJS is able to work even when not attached to a root element like <body>
. Elements with hooks become “bound” when the HTML template is rendered into a view. Binding is the process wherein a $scope property becomes attached to one of these hooks. So, when a $scope
variable changes, the bound element also changes (as demonstrated above).
Binding, despite the popular slogan, is technically a relationship without direction (sad, right?). The idea of a direction is a way to better understand the relationship between bound elements on the associated $scope
property. The $scope
– not to be confused with a controller which depends on an injected $scope
to work – is a plain old JavaScript object, and bound view elements become properties on this object. Rendering a bound $scope
property into the view is as easy as:
{{firstName}} or <span ng-bind=”firstName”></span>
These binding techniques are commonly called one-way binding.
So what about two-way binding? Consider these fragments from the code sample from the first example:
demo.ctrl.js
demoApp.controller( DemoController, function ( $scope ) { $scope.firstName = “Jason”; })
demo.tpl.html
<input type="text" data-ng-model="firstName" /> <h3>{{ firstName }}</h3>
When the HTML template is rendered into a view “firstName” (surrounded by the double curly brackets) is replaced by the $scope
property of the same name. This is the first half of the two-way binding. When the user types a new name into the text field a listener is fired and AngularJS instantly knows to update the $scope
property “firstName” with whatever the user types. This is the second half of the two-way binding. Finally, thanks to the digest process, the bound view element is updated to reflect the new $scope
property.
That’s all there is to it!
The Dot Is Your Friend
Models assigned to HTML template elements come in two basic flavors: with or without the dot. Both styles work out of the box, fire and forget. The difference is how AngularJS handles the input behind the scenes – and that difference makes a big impact on how accessible the data is other application scopes.
When models are bound to a <form>
element without the dot, the value collected by the element is stored as a property of the current $scope
. ngModel will faithfully update all instances of this property on any other element to which it is bound, including other <form>
elements. If a child $scope
extends from the current $scope
, the child $scope
will have it’s version of the property value updated as well. However, if a second <form>
element exists within the child’s $scope
, and is bound to the same model defined at the parent’s $scope
level, guess what happens? Nothing. The child $scope
accepts the new value and overwrites the entire property, while the parent’s $scope
stays the same. Whew. Here’s some sample code:
demo.tpl.html
<div data-ng-controller=”ParentController as theParent”> <div> <label>Parent Controller</label> <input type="text" data-ng-model="firstName" /> <h3>{{ firstName }}</h3> </div> <div data-ng-controller="ChildController as theChild"> <label>Child Controller</label> <input type="text" data-ng-model="firstName" /> <h3>{{ firstName }}</h3> </div> </div>
When the user enters “Jason” into the parent controller’s input field, the ngModel “firstName” ensures bound elements are immediately updated. Since, however, the child scope merely inherited the value of firstName as a string property, when the user types “Lunsford” into the child controller’s input field, the associated property on the child’s $scope
is overwritten and synchronicity is broken.
Sometimes this behavior is exactly what the application requires, but typically it’s a pain in the ass to lose synchronicity between parent and child scopes – and this is where the dot comes in.
<div data-ng-controller=”ParentController as theParent”> <div> <label>Parent Controller</label> <input type="text" data-ng-model="ourUser.firstName" /> <h3>{{ firstName }}</h3> </div> <div data-ng-controller="ChildController as theChild"> <label>Child Controller</label> <input type="text" data-ng-model="ourUser.firstName" /> <h3>{{ firstName }}</h3> </div> </div>
The behavior here is largely the same, with one giant exception. When the user enters “Lunsford” into the child controller’s input field the “ourUser” object’s “firstName” property is updated. The “ourUser” object is shared bi-laterally between parent and child $scope
, and not as a static, once-and-done inheritance. Thus, change the object’s property in one field and updates go everywhere.
Live Views – All Together Now!
Binding of any type – one-way or two – depends on AngularJS’s compiling process. Compiling evaluates the pure HTML generated by the developer and finds all child nodes under the ngApp directive to which an ngModel is attached (as discussed above). The pure HTML is commonly called a template and after AngularJS finishes compiling it – enabling all directives and attaching models to $scope
– the end result is the live view.
Live views are watched and updated by $digest()
, AngularJS’s private event loop. This private loop is what affords AngularJS the opportunity to handle events in real time. When a model value has changed, or even when AngularJS suspects a value changed, the $digest
loop runs in order to compare model values versus those currently in the view. Any value mismatches are considered “dirty” – which is why this process of model/view value comparison is sometimes called “dirty checking”. AngularJS knows which of these values to compare – in other words, those values which are two-way bound – because they were added to a “watch list” by the $scope’s $watch()
method during the compile process.
That’s it – magic revealed. Thanks for reading! Please feel free to comment.
Modus Create
Related Posts
-
AngularJS: Tricks with angular.extend()
AngularJS has this built-in method for doing object to object copies called angular.extend(). It is…
-
Rapid Prototyping with AngularJS
Building great web applications is challenging and all great web applications start with a proof…