CAUTION: Overloading The ng-Controller Directive In AngularJS
I'm putting "CAUTION" right in the title here because I'm not actually recommending that anyone do this. This is just a fun experiment whose only purpose is to demonstrate some of the flexibility and the power that the AngularJS directive architecture provides. We've already seen how powerful it is to be able to bind a single directive to multiple priorities within the same compilation and linking process. But, can we abuse that power and overload the ngController directive such that it binds both the "Controller" and the "User Interface (UI) behaviors"? You bet your sweet ass we can! So, let's do it!
Run this demo in my JavaScript Demos project on GitHub.
The trick to this demo stems from the fact that if you provide a compile() function in your directive configuration object, your compile() function has to to return a linking function. And, if you don't return a linking function, the directive won't link. This works great in situations where we want to overload an element Directive like the Script tag; and, it will also work if we want to overloaded the ngController directive to link when certain attribute values are present.
To demonstrate this, I have put together a demo that defines two Controllers using the core ng-controller directive. I then further define user interface (UI) behaviors by overloading each one of the ng-controller instances under certain circumstances:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Overloading The ng-Controller Directive In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Overloading The ng-Controller Directive In AngularJS
</h1>
<p ng-controller="BoxOneController" class="box box-one">
X: {{ coordinates.x }}, Y: {{ coordinates.y }}
<span
class="marker"
ng-style="{ left: ( coordinates.x + 'px' ), top: ( coordinates.y + 'px' ) }">
</span>
</p>
<p ng-controller="BoxTwoController" class="box box-two">
X: {{ coordinates.x }}, Y: {{ coordinates.y }}
<span
class="marker"
ng-style="{ left: ( coordinates.x + 'px' ), top: ( coordinates.y + 'px' ) }">
</span>
</p>
<p class="warning">
<strong>Caution:</strong> This is just a fun experiment. Don't actually ever
do this in your AngularJS applications.
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control box one.
app.controller(
"BoxOneController",
function( $scope ) {
// I define the relative coordinates of the marker.
$scope.coordinates = {
x: 0,
y: 0
};
// ---
// PUBLIC METHODS.
// ---
// I update the coordinates.
$scope.setPosition = function( x, y ) {
$scope.coordinates.x = Math.floor( x );
$scope.coordinates.y = Math.floor( y );
};
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control box two.
app.controller(
"BoxTwoController",
function( $scope ) {
// I define the relative coordinates of the marker.
$scope.coordinates = {
x: 0,
y: 0
};
// ---
// PUBLIC METHODS.
// ---
// I update the coordinates.
$scope.setPosition = function( x, y ) {
// NOTE: Locking coordinates down to 20px increments.
$scope.coordinates.x = ( Math.floor( x / 20 ) * 20 );
$scope.coordinates.y = ( Math.floor( y / 20 ) * 20 );
};
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide mouse-interactions for the Box-One controller.
app.directive(
"ngController",
function BoxOneDirectiveFactory() {
// Return the directive configuration object.
// --
// NOTE: Since we are over-loading the ngController directive binding,
// we only want to return the LINKING function if the controller type
// matches our specific use-case (ie, BoxOneController).
return({
compile: function( tElement, tAttributes ) {
return( ( tAttributes.ngController === "BoxOneController" ) && link );
},
priority: 0,
restrict: "A"
});
// I bind the JavaScript events to the local scope.
function link( scope, element, attributes ) {
// CAUTION: Assuming no change in parent dimensions after linking.
var parentOffset = element.offset();
// Translate mouse events into coordinates.
element.mousemove(
function handleMousemoveEvent( event ) {
scope.setPosition(
Math.max( 0, ( event.pageX - parentOffset.left - 15 ) ),
Math.max( 0, ( event.pageY - parentOffset.top - 15 ) )
);
// NOTE: Using $digest() instead of $apply() due to the vast
// number of events that will be triggered.
scope.$digest();
}
);
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide mouse-interactions for the Box-Two controller.
app.directive(
"ngController",
function BoxTwoDirectiveFactory() {
// Return the directive configuration object.
// --
// NOTE: Since we are over-loading the ngController directive binding,
// we only want to return the LINKING function if the controller type
// matches our specific use-case (ie, BoxTwoController).
return({
compile: function( tElement, tAttributes ) {
return( ( tAttributes.ngController === "BoxTwoController" ) && link );
},
priority: 0,
restrict: "A"
});
// I bind the JavaScript events to the local scope.
function link( scope, element, attributes ) {
// CAUTION: Assuming no change in parent dimensions after linking.
var parentOffset = element.offset();
var parentHeight = element.height();
var parentWidth = element.width();
// Translate mouse events into coordinates.
element.mousemove(
function handleMousemoveEvent( event ) {
scope.setPosition(
( parentWidth - Math.max( 0, ( event.pageX - parentOffset.left - 15 ) ) ),
( parentHeight - Math.max( 0, ( event.pageY - parentOffset.top - 15 ) ) )
);
// NOTE: Using $digest() instead of $apply() due to the vast
// number of events that will be triggered.
scope.$digest();
}
);
}
}
);
</script>
</body>
</html>
As you can see, the ng-controller directive is defined three times in this demo - once by the core framework (implied) and twice more in my custom code. In my version(s) of the ng-controller directive, however, I'm only providing a link function if the ngController attribute contains a specific value. In this way, I'm not applying the linking function to every ng-controller instance - only to the one that I'm targeting.
Again, I'm not saying that you should do this with ng-controller directive. Not only is the HTML markup completely unintuitive (ie, contains no indication of the shenanigans being executed), it also means that the every ngController compile() function needs to be called for each ng-controller attribute. Clearly not great for performance. This was just a fun demo to showcase the flexibility and power of the AngularJS directive architecture.
Want to use code from this post? Check out the license.
Reader Comments
Nice experiment
@Atticus,
Thank you, kindly!
Great article once again. I don't think there is anyone out there experimenting with angular like you do.
Keep on going!
@Pavlos,
Ha ha, thank you very much - I'm glad you're enjoying the experiments. Hope to keep the good stuff coming!