Separate model from view in Angular

Share data model via scopes and limit the view access via controllerAs syntax.

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
2
3
4
5
.controller('MyController', function ($scope) {
$scope.foo = "something";
});
<div ng-controller="MyController">{{ foo }}</div>
// div always reflects the current $scope.foo value

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
2
3
4
5
6
7
angular.module('MyApp', [])
.controller('Parent', function ($scope) {
$scope.foo = 'foo';
$scope.bar = 'bar';
})
.controller('Child', function () {})
.controller('GrandChild', function () {});

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
2
3
4
5
<div ng-controller="Parent">foo: {{ foo }}, bar: {{ bar }}
<div ng-controller="Child">foo: {{ foo }}, bar: {{ bar }}
<div ng-controller="GrandChild">foo: {{ foo }}</div>
</div>
</div>

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
2
3
4
5
6
7
8
9
10
11
12
var parentScope = {
foo: 'foo',
bar: 'bar'
};
var childScope = Object.create(parentScope);
var grandChildScope = Object.create(childScope);
// verify
grandChildScope.foo // 'foo'
grandChildScope.bar // 'bar'
parentScope.isPrototypeOf(childScope) // true
childScope.isPrototypeOf(grandChildScope) // true
parentScope.isPrototypeOf(grandChildScope) // true

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

page
1
2
3
4
<body ng-app="MyApp">
<h1>Scopes vs controllers</h1>
<parent></parent>
</body>
code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
angular.module('MyApp', [])
.directive('parent', function () {
return {
template: '<h2>Parent</h2>\n' +
'foo: {{ foo }}\n' +
'bar: {{ bar }}\n' +
'<child foo="foo" bar="bar" />\n',
controller: function ($scope) {
$scope.foo = 'foo';
$scope.bar = 'bar';
}
}
})
.directive('child', function () {
return {
scope: {},
template: '<h2>Child</h2>\n' +
'foo: {{ foo }}\n' +
'bar: {{ bar }}\n' +
'<grand-child foo="foo" bar="bar" />\n',
controller: function ($scope) {
$scope.foo = 'not foo';
$scope.bar = 'not bar';
}
}
})
.directive('grandChild', function () {
return {
scope: {},
template: '<h2>Grand child</h2>\n' +
'foo: {{ foo }}, but the bar should be "undefined" {{ bar }}\n',
controller: function ($scope) {
$scope.foo = $scope.$parent.foo;
}
}
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.directive('child', function () {
return {
scope: {},
template: '<h2>Child</h2>\n' +
'foo: {{ foo }}\n' +
'bar: {{ bar }}\n' +
'<grand-child foo="foo" />\n',
controller: function ($scope) {
$scope.foo = 'not foo';
$scope.bar = 'not bar';
}
}
})
.directive('grandChild', function () {
return {
scope: {
foo: '='
},
template: '<h2>Grand child</h2>\n' +
'foo: {{ foo }}, but the bar should be "undefined" {{ bar }}\n'
}
});

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
2
3
4
5
6
7
8
9
10
11
12
angular.module('MyApp', [])
.directive('parent', function () {
return {
template: '<h2>User</h2>\n' +
'first: {{ user.first }}\n' +
'last: {{ user.last }}\n' +
'{{ user.secret }}',
controller: function ($scope, User) {
$scope.user = User;
}
}
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
angular.module('MyApp', [])
.directive('parent', function () {
return {
template: '<h2>Parent</h2>\n' +
'foo: {{ parent.foo }}\n' +
'bar: {{ parent.bar }}\n' +
'<child />\n',
controllerAs: 'parent',
controller: function ($scope) {
this.foo = $scope.foo = 'foo';
this.bar = $scope.bar = 'bar';
}
}
})
.directive('child', function () {
return {
template: '<h2>Child</h2>\n' +
'foo: {{ child.foo }}\n' +
'bar: {{ child.bar }}\n' +
'<grand-child />\n',
controllerAs: 'child',
controller: function ($scope) {
this.foo = $scope.foo;
this.bar = $scope.bar;
}
}
})
.directive('grandChild', function () {
return {
template: '<h2>Grand child</h2>\n' +
'foo: {{ grandChild.foo }}, but the bar should be "undefined" {{ grandChild.bar }}\n',
controllerAs: 'grandChild',
controller: function ($scope) {
this.foo = $scope.foo;
}
}
});

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
2
3
4
5
6
7
8
9
10
11
12
// accidentally forget to refer to `grandChild.bar` on the controller instance
// getting the value of `$scope.bar` instead
.directive('grandChild', function () {
return {
template: '<h2>Grand child</h2>\n' +
'foo: {{ grandChild.foo }}, but the bar should be "undefined" {{ bar }}\n',
controllerAs: 'grandChild',
controller: function ($scope) {
this.foo = $scope.foo;
}
}
});

Binding scope properties to the controller

Writing a controller constructor for each value of the property quickly becomes tiresome.

1
2
3
4
5
6
7
8
9
10
11
12
13
directive('parent', function () {
return {
template: '<h2>Parent</h2>\n' +
'foo: {{ parent.foo }}\n' +
'bar: {{ parent.bar }}\n' +
'<child foo="foo" bar="bar" />\n',
controllerAs: 'parent',
controller: function ($scope) {
this.foo = $scope.foo = 'foo';
this.bar = $scope.bar = 'bar';
}
}
})

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
directive('child', function () {
return {
scope: {
foo: '=',
bar: '='
},
template: '<h2>Child</h2>\n' +
'foo: {{ child.foo }}\n' +
'bar: {{ child.bar }}\n' +
'<grand-child foo="foo" bar="bar" />\n',
controllerAs: 'child',
bindToController: true,
controller: angular.noop
}
})

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
2
3
4
5
6
{
child: {
foo: 'foo',
bar: 'bar'
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.directive('child', function () {
return {
scope: {
foo: '=',
bar: '='
},
template: '<h2>Child</h2>\n' +
'foo: {{ child.foo }}\n' +
'bar: {{ child.bar }}\n' +
'<grand-child foo="child.foo" bar="bar" />\n',
controllerAs: 'child',
bindToController: true,
controller: angular.noop
}
})
.directive('grandChild', function () {
return {
scope: {
foo: '='
},
template: '<h2>Grand child</h2>\n' +
'foo: {{ foo }}'
}
});

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

angular 1.4-beta.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.directive('child', function () {
return {
scope: {
foo: '=',
bar: '='
},
template: '<h2>Child</h2>\n' +
'foo: {{ child.foo }}\n' +
'bar: {{ child.bar }}\n' +
'<grand-child foo="child.foo" bar="bar" />\n',
controllerAs: 'child',
bindToController: {
foo: '='
},
controller: angular.noop
}
});

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
2
'foo: {{ child.foo }}\n' +
'bar: {{ bar }}\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
2
3
4
scope: {
foo: '=',
bar: '?='
}

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
2
3
4
5
6
7
8
9
10
    outside directive with $scope.foo = "foo"
| |
v v
---- model control ----- | --------- view control ------------|---- DOM (html) -----
| controllerAs: 'ctrl', |
| bindToController: { foo: '=' } |
scope: { | $digest
foo: '=', -----------|-> <p>foo is {{ ctrl.foo }}</p> -------> <p>foo is foo</p>
bar: '@' | |
} | |

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