AngularJS is a Model-View-Controller framework. Model is the data, View is the rendering of the data in the browser and Controller is the glue that based on events updates the Model. Specifically, in AngularJS the controller functions share data using the linked scopes objects. The two way binding between the scopes and the templates allows easy and automatic view updates without spending much time. It all happens magically.
1 | .controller('MyController', function ($scope) { |
While this is powerful, this exposes way too many properties to every child scope, and mixes the data model (the scope properties) with the view generation (the values that should be shown in the DOM). The mixing of the prototypically linked scopes with templates is a bad idea: everything is visible and accessible. You operate as if everything in your template was lexically scoped code, but you could not see the entire file!
Let us see how to fix the data model visibility by isolating the children scopes, and how
to separate the scopes from the templates using the new syntax controllerAs
available in Angular 1.3.
Isolating scopes
Let us look at an example with 3 controllers. I will call them "Parent", "Child" and "GrandChild".
Only the parent scope has any data attached to the $scope
object.
1 | angular.module('MyApp', []) |
Notice that the controllers are NOT nested in the JavaScript code. Instead they are written as if they did not know about each other at all. But if we nest them in the HTML markup, they become linked automatically.
1 | <div ng-controller="Parent">foo: {{ foo }}, bar: {{ bar }} |
The generated DOM will reflect foo
and bar
data in all 3 controllers.
Might be good, but usually this is a bad
thing because this is as if all variables were public / global.
GrandChild controller might not be interested in
the bar
property, but it has access to it anyway.
The scope objects are prototypically linked, as if we wrote the following JavaScript code to create them
1 | var parentScope = { |
How can we isolate each scope and control which variables it needs from the parent scope?
We cannot use anything but the default "public" inheritance when using stand alone ng-controller
directives.
To separate scopes, we must start writing custom directives. Luckily in this simple example it is easy to do
1 | <body ng-app="MyApp"> |
1 | angular.module('MyApp', []) |
The nesting of controllers / directives is explicit in the templates. The parent
template includes
the custom directive <child>
, and if we go the directive child
, we see that it's template includes
the <grand-child>
directive. The scopes no longer share information, because we used the scope: {}
property.
Thus the child
directive can use its own values for foo
, and the grandChild
can explicitly
grab the parent's foo
value while ignoring (and avoiding) the bar
property. The grandChild
avoids
bar
even though the child
controller tries to pass it via an attribute.
We could also automatically propagate the changes from the child
to the grandChild
using
the scope: { '=' }
syntax instead of the $scope.$parent
syntax.
1 | .directive('child', function () { |
I prefer the later choice - it is shorter and avoids writing the controller function.
Separate model from the view
Sharing the data (model) via scopes is convenient, but by default any template expression can access
everything on the $scope
object. For example let us pass the user
object on the scope to be able to
print the user's first and last name. By mistake a template might print the user's secret phrase!
1 | angular.module('MyApp', []) |
I am looking at the controllerAs
and bindToController
keywords in Angular and find them to be very
useful. They allow the separation between the hierarchy of data (the nested $scope
objects)
and the values used inside the template markup (the expressions inside `{{ }}` brackets).
In a sense, the new keywords allow us to separate the model from the view much cleaner than before.
Using controllerAs
we can better control what can be found in the template,
even without isolating every scope.
For example to pass both foo
and bar
through the directives,
but to avoid using the bar
value in the template,
we can use controllerAs
syntax.
1 | angular.module('MyApp', []) |
Notice that the templates now refer to the controller's name in the expressions.
We CAN still access the values on the $scope
, perhaps accidentally, but it will be much simpler
to see where we went wrong:
1 | // accidentally forget to refer to `grandChild.bar` on the controller instance |
Binding scope properties to the controller
Writing a controller constructor for each value of the property quickly becomes tiresome.
1 | directive('parent', function () { |
If we want all scope properties to appear on the controller instance, we can use bindToController
property.
In Angular 1.3 we can only bind all properties by setting it to true
.
1 | directive('child', function () { |
The properties foo
and bar
will be passed from the parent scope and then bound to the controller instance
during its creation. One needs some controller function (even angular.noop
works!) in order for this to pass,
plus the directive must use an isolate scope.
*Note 'child' directive does NOT have foo
and bar
attached to its scope.
Instead the scope has the property
1 | { |
Thus the grandChild
directive might not be able to access the foo
directly. Instead you have to use the controller name when creating the directive and specifying
attribute names in the template <grand-child foo="child.foo" bar="bar" />
1 | .directive('child', function () { |
Controlling binding in Angular 1.4
Binding every user scope property to the controller kind of defeats the purpose of making the templates
(the view part) a little more separated from the data (the model part).
Thus Angular 1.4 allows bindToController
to be
an object of properties to bind to the controller instance, instead of a primitive boolean.
The semantics is the same as an isolate scope specification. For example if we want to link foo
and bar
to the outside data, but only be able to show foo
in our templates we could do the following
1 | .directive('child', function () { |
The result will be foo: foo bar:
because there is nothing bound under the name child.bar
.
Of course the template can simply access the scope property bar
instead of the controller's bar
.
1 | 'foo: {{ child.foo }}\n' + |
But this is up to you to control. Maybe there should be a mode to disallow the direct scope expressions in the templates and only allow accessing properties on the controller?
Conclusions
I now think of sharing data via scope tree as my model. I can control what properties I pass to the child
directives using <child-name prop1="myProperty1" prop2="myProperty2" />
syntax. Inside the child directive
I declare what properties I need using syntax
1 | scope: { |
Using controllerAs
with bindToController
allows me to similarly control what data is available in
my templates. Instead of every template expression being able to access every scope property, I now
expose only certain values, keeping the rest on the scope, but "hidden" (well not really hidden) from
the template.
1 | outside directive with $scope.foo = "foo" |
Hopefully I will be able to write my own plugin to ban the direct scope access from the templates without
going through the bindToController
mapping first.
Or Angular team could add this as an option (similar to ng-strict-di
).