Decorating Scope Methods In The AngularJS Prototype Chain
Last week, I looked at how to pass values up the $scope chain in an AngularJS application. We used a setter method in order to get around the asymmetric nature of prototype property access and mutation. This allowed us to set values into an inherited $scope instance. Now, since we needed to use a setter method; and since that setter method is inherited by the $scope prototype chain; it means that we are afforded the opportunity to decorate that setter method at various points in the $scope prototype chain.
As I discussed last week, each instance of $scope in an AngularJS application inherits, as its prototype object, the $scope that comes before it in the DOM (Document Object Model) hierarchy. This means that properties set in an ancestor scope will become available in a descendant scope. This gives each $scope instance the opportunity to create its own local version of a particular property (without corrupting ancestral $scopes). And, in the case of a $scope method, it provides a hook to decorate a method call as it gets passed up the prototype chain.
To experiment with this, we'll refactor our setWindowTitle() demo from last week. In this version, we'll decorate the setWindowTitle() at two places, adding two new features:
- Uppercase the window title.
- Append the phrase, "- AngularJS Demo".
In order to do this, we need to add two new $scopes (passed into two new Controllers). This requires two additional HTML elements:
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<!--
When setting the window title, we can display a default value
in the tags, then take it "over" using the ngBind directive.
This binds the tag content to the given Model (scope value).
-->
<title ng-bind="windowTitle">AngularJS Demo Loading</title>
</head>
<body>
<h1>
Decorating Scope Methods In AngularJS Prototype Chain
</h1>
<!--
I will decorate some scope values as they get "passed" up the
scope prototype chain.
-->
<div ng-controller="OuterController">
<!-- I will further decorate the title. -->
<div ng-controller="InnerController">
<form ng-controller="FormController" ng-submit="save()">
<p>
Window Title:<br />
<input type="text" ng-model="name" size="25" />
<input type="submit" value="Update Title" />
</p>
</form>
</div>
</div>
<!-- Load AngularJS from the CDN. -->
<script
type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var Demo = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define our root-level controller for the application.
Demo.controller(
"AppController",
function( $scope ) {
// Set up the default programmtic window title. Once
// the app runs, this will overwrite the value that
// is currently set in the HTML.
$scope.windowTitle = "Default Set In AppController";
// This App Controller is the only controller that
// has access to the Title element. As such, we need
// to provide a way for deeply nested Controllers to
// update the window title according to the page
// state.
$scope.setWindowTitle = function( title ) {
// This function closure has lexical access to
// the $scope instance associated with this App
// Controller. That means that when this method
// is invoked on a "sub-classed" $scope instance,
// it will affect this scope higher up in the
// prototype chain.
$scope.windowTitle = title;
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define our outer controller that deocrates the title.
Demo.controller(
"OuterController",
function( $scope ) {
// Get a reference to the original setWindowTitle()
// method that we are about to decorate.
var coreSetWindowTitle = $scope.setWindowTitle;
// Expose a local method that will override the
// core setWindowTitle() method. Since each scope
// instance is a prototype of the scope instances
// below it, this version will be the one that is
// inherited by the scopes below it.
$scope.setWindowTitle = function( title ) {
// Decorate the title.
title = (title + " - AngularJS Demo");
// Pass the call up the prototype chain.
coreSetWindowTitle.call( this, title );
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define our outer controller that deocrates the title.
Demo.controller(
"InnerController",
function( $scope ) {
// Get a reference to the "original" setWindowTitle()
// method that we are about to decorate. I use quotes
// becuase this is actually the version being
// inherited from the OuterController.
var coreSetWindowTitle = $scope.setWindowTitle;
// Expose the decorator to the sub-classes scopes
// farther down in the DOM tree.
$scope.setWindowTitle = function( title ) {
// Decorate the title.
title = title.toUpperCase();
// Pass the call up the prototype chain.
coreSetWindowTitle.call( this, title );
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define our Form controller.
Demo.controller(
"FormController",
function( $scope ) {
// Default the empty form field.
$scope.name = "You feeling lucky, punk?";
// When the form is saved, let's update the window
// title with the new value.
$scope.save = function() {
// The setWindowTitle() method is inherited from
// the scope prototype chain. This would typically
// be inherited from the AppController... however,
// this time, it is actually being inherited from
// the InnerController which is overwriting and
// decorating calls to the root setWindowTitle()
// scope method.
this.setWindowTitle( this.name );
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
</script>
</body>
</html>
As you can see, the OuterController and InnerController instances both decorate the setWindowTitle() method exposed by the AppController. In order to do this, they each obtain a reference to the method that they inherited; then they set, in their own local scope, a new version of the setWindowTitle() method. This new version is then inherited by their descendant scopes.
Now, when the FormController makes a call to setWindowTitle() with the phrase:
You feeling lucky, punk?
... the window ends up getting the actual title:
YOU FEELING LUCKY, PUNK? - AngularJS Demo
As you can see, the original value was decorated twice, resulting in an uppercase value with a new suffix.
Since I am just getting into AngularJS, I don't have a great use-case for this kind of $scope prototype chain manipulation. In fact, setting the window title is really the only thing that jumps to mind. That said, the architecture of the $scope chain does allow for some pretty cool stuff.
Want to use code from this post? Check out the license.
Reader Comments
Cool article. I was pondering the usage of prototypal inheritance in angularJS - and if Angular in fact alleviates the need for it altogether
@White Box,
So, there are two opportunities for inheritance in an AngularJS application: the $scope inheritance, and the model inheritance.
You don't have any [much] control over $scope inheritance. AngularJS gives that to you out of the box so that your nested views can inherit data and behavior. This is a really awesome feature.
Model inheritance (such as having one Service object inherit from another) is completely up to you. I have gone back and forth on this one. Sometimes, I like the idea; sometimes I don't. Often times, I'll remove some inheritance and replace it with a dependency-injected "helper" object that contains the shared behavior.
Furthermore, some kinds of inheritance are easier than others. Service object inheritance is relatively easy as long as you can figure out how to wire up your Factory / Service to inject your base class; then, define your prototype off that base class.
Controller-inheritance, on the other hand, is far more complex since the Controller collection doesn't really allow for factory-based building. As such, you're in a bit of Catch-22 - you can't inject a base controller UNTIL you've instantiated the sub-class controller... at which point, it's a bit too late to extend the base class.
Hi Ben,
thank you for your in detailed explanation of the scope-inheritance!
What do you think about using decorators to share inject services with similar logic across modules? I believe it might be more flexible than inheritance, as i can modify the service implementation upon the configuration of my application.
Is there a downside to this approach?
.config(['$provide', function($provide) {
return $provide.decorator('PizzaGuy', ['$delegate', ComedianProvider,
function($delegate) {
var pizzaGuy = $delegate;
pizzaGuy.availablePizzas = ComedianProvider.favoritePizzas;
return pizzaGuy;
}]);
}]);
/*later in any controller */
pizzaGuy.serveTo(comedian) => Some real NewYork Pizza for Jon Stewart
Best Greetings,
Sergej
Sorry, forgot to include "ComedianProvider" in function. :)
Great article, I really enjoyed it. I have been wondering which is the best way to implement method overriding in angular.js and your article has helped me to figure it out.
Thanks and keep it the good work
Cheers,
Jack