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).