Namespacing, code consistency and proper design patterns really matter in software engineering, and Angular addresses a lot of issues we face as front-end engineers really nicely.
I’d like to show you some techniques using the bindToController
property on Directives that will help clean up your DOM-Controller namespacing, help keep code consistent, and help follow an even better design pattern when constructing Controller Objects and inheriting data from elsewhere.
Table of contents
Prerequisites
Use bindToController
alongside controllerAs
syntax, which treats Controllers as Class-like Objects, instantiating them as constructors and allowing us to namespace them once instantiated, such as the following:
<div ng-controller="MainCtrl as vm">
{{ vm.name }}
</div>
Previously, without controllerAs
we’d have no native namespacing of a Controller, and JavaScript Object properties simply floated around the DOM making it harder to keep code consistent inside Controllers, as well as running into inheritance issues with $parent
. That’s all we’ll cover on this during this article, there’s a mighty post I’ve already published about it.
Problem
Issues arise when writing Controllers that use the controllerAs
syntax, we begin writing our components using a Class-like Object, only to end up injecting $scope
to get access to inherited data (from “isolate scope”). A simple example of what we’d start with:
// controller
function FooDirCtrl() {
this.bar = {};
this.doSomething = function doSomething(arg) {
this.bar.foobar = arg;
}.bind(this);
}
// directive
function fooDirective() {
return {
restrict: 'E',
scope: {},
controller: 'FooDirCtrl',
controllerAs: 'vm',
template: [
// vm.name doesn't exist just yet!
'<div><input ng-model="vm.name"></div>'
].join('')
};
}
angular
.module('app')
.directive('fooDirective', fooDirective)
.controller('FooDirCtrl', FooDirCtrl);
Now we need to “inherit” scope, so let’s create the isolation hash in scope: {}
to reference the binding we want:
function fooDirective() {
return {
...
scope: {
name: '='
},
...
};
}
And stop. Now we need to inject $scope
, my Class-like Object has been vandalised by this $scope
Object I’ve tried so hard to get rid of to adopt better design principles, and now I’ve got to inject it.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Onwards with the mess:
// controller
function FooDirCtrl($scope) {
this.bar = {};
this.doSomething = function doSomething(arg) {
this.bar.foobar = arg;
$scope.name = arg.prop; // reference the isolate property
}.bind(this);
}
At this point, we’ve likely ruined all excitement we had about the new Directive now our Class-like Object pattern has been ruined by $scope
.
Not only this, but our pseudo-template would be affected with an un-namespaced variable floating amidst vm.
prefixed ones:
<div>
<div>
{{ name }}
<input type="text" ng-model="vm.username">
</div>
</div>
Solution
Before we go into what we’ll deem as a solution, there are a lot of negative comments about Angular’s attempts to replicate Class-like Object patterns, and I’m aware of the design, but we’re making the most of what we’ve got - nothing’s perfect and likely never will be, even with the rewrite v2.0. This post covers a great solution to cleaning up Angular’s bad $scope
habits as best as we can to write “proper” JavaScript designed in a better way.
Enter the bindToController
property. In the docs, bindToController
suggests that setting the value to true
enables the inherited properties to be bound to the Controller, not the $scope
Object.
function fooDirective() {
return {
...
scope: {
name: '='
},
bindToController: true,
...
};
}
This means we can refactor the previous code example, removing $scope
:
// controller
function FooDirCtrl() {
this.bar = {};
this.doSomething = function doSomething(arg) {
this.bar.foobar = arg;
this.name = arg.prop; // reference the isolate property using `this`
}.bind(this);
}
The Angular documentation doesn’t suggest that you can use an Object instead of bindToController: true
, but in the Angular source code this line is present:
if (isObject(directive.bindToController)) {
bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true);
}
If it’s an Object, parse the isolate bindings there instead. This means we can move our scope: { name: '=' }
example binding across to it to make it more explicit that isolate bindings are in fact inherited and bound to the controller (my preferred syntax):
function fooDirective() {
return {
...
scope: {},
bindToController: {
name: '='
},
...
};
}
Now we’ve solved the JavaScript solution, let’s look at the template change impact this has.
Previously, we might have had name
inherited and bound to $scope
, whereas now we can use the same namespace as our Controller - rejoice. This keeps everything very consistent and readable. Finally we can vm.
prefix our inherited name
property to keep things in our template consistent!
<div>
{{ vm.name }}
<input type="text" ng-model="vm.username">
</div>
Live Refactor examples
I’ve setup a few live examples on jsFiddle to demonstrate the refactor process (this was a great change for me and my team migrating from Angular 1.2 to 1.4 recently).
Note: Each example uses two way isolate binding from a parent Controller passed down into the Directive, type to see changes reflected back up to the parent.
First example, using $scope
Object’s passed in. Would leave templating inconsistencies and Controller logic $scope
and this
mashups.
Second example, refactor $scope
with bindToController: true
Boolean value. Fixes templating namespace issues as well as keeping the Controller logic consistent under the this
Object.
Third example (preferred), refactor bindToController: true
into an Object, moving scope: {}
properties across to it for clarity. Fixes same as example two, but adds clarity for other developers working/revisiting the piece of code.