Skip to content

Instantly share code, notes, and snippets.

@vojtajina
Created January 20, 2012 21:51
Show Gist options
  • Save vojtajina/1649788 to your computer and use it in GitHub Desktop.
Save vojtajina/1649788 to your computer and use it in GitHub Desktop.
Angular: BC module for scope/controller separation
/**
* @license AngularJS
* (c) 2010-2012 AngularJS http://angularjs.org
* License: MIT
*/
/**
* Backward compatibility module for AngularJS
* @author Vojta Jina <[email protected]>
*
* Load this module to enable old-style controllers, where controller and scope are mixed together.
*
* This module decorates Angular's $controller service:
* - if given controller does not ask for $scope, it instantiates it in old-way
* - if given controller does ask for $scope, instantiation is delegated to default $controller
* service.
*
* This also allows migrating apps step by step.
*/
angular.module('ngScopeController', ['ng'], ['$provide', function($provide) {
$provide.decorator('$controller', ['$injector', '$delegate', '$parse', '$window',
function($injector, $delegate, $parse, $window) {
var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(.+?)\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
/**
* Return a list of arguments to inject
* If no $inject property specified it parse the argument names.
* @param fn
*/
function inferInjectionArgs(fn) {
// assert fn is a function
if (!angular.isFunction(fn)) {
var error = new Error("Controller must be a function, got " +
(typeof fn === 'object' ? fn.constructor.name || 'Object' : typeof fn));
throw error;
}
if (fn.$inject) return fn.$inject;
// guess from argument names
var args = [];
var fnText = fn.toString().replace(STRIP_COMMENTS, '');
var argDecl = fnText.match(FN_ARGS);
angular.forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, name) {
args.push(name);
});
});
return args;
}
/**
* $controller service
*
* @param {Function|string} Class Constructor function of the controller or string id.
* @param {Object} locals Locals for injecting, must contain a $scope.
* @return {Object} Instance of the controller.
*/
return function(Class, locals) {
var scope = locals.$scope;
// given string id, find ctrl on window or current scope
if (angular.isString(Class)) {
var getter = $parse(Class);
Class = getter(scope) || getter($window);
}
var injectArgs = inferInjectionArgs(Class);
// asking for scope - delegate to original service
if (injectArgs.indexOf('$scope') !== -1) {
return $delegate(Class, locals);
}
// not asking for scope - BC hack
var classPrototype = Class.prototype;
for(var key in classPrototype) {
scope[key] = angular.bind(scope, classPrototype[key]);
}
$injector.invoke(Class, scope, locals);
return scope;
};
}]);
}]);
describe('$controller', function() {
var $controller;
beforeEach(module('ngScopeController'));
beforeEach(inject(function($injector) {
$controller = $injector.get('$controller');
}));
it('should return instance of given controller class', function() {
var MyClass = function($scope) {},
ctrl = $controller(MyClass, {$scope: {}});
expect(ctrl).toBeDefined();
expect(ctrl instanceof MyClass).toBe(true);
});
it('should inject arguments allowing locals', inject(function($http) {
var MyClass = function($scope, $http, $location) {
this.$scope = $scope;
this.$http = $http;
this.$location = $location;
};
var scope = {},
localLocation = {},
ctrl = $controller(MyClass, {$scope: scope, $location: localLocation});
expect(ctrl.$http).toBe($http);
expect(ctrl.$scope).toBe(scope);
expect(ctrl.$location).toBe(localLocation);
}));
it('should get controller from $window', inject(function($window) {
var ctrl;
$window.a = {
Some: function() {this.id = 'Some';},
Other: function($scope) {this.id = 'Other';}
};
ctrl = $controller('a.Some', {$scope: {}});
expect(ctrl).toBeDefined();
expect(ctrl.id).toBe('Some');
ctrl = $controller('a.Other', {$scope: {}});
expect(ctrl instanceof $window.a.Other).toBe(true);
expect(ctrl.id).toBe('Other');
}));
it('should get controller from current scope', function() {
var ctrl, scope = {};
scope.a = {
Some: function() {this.id = 'Some';},
Other: function($scope) {this.id = 'Other';}
};
ctrl = $controller('a.Some', {$scope: scope});
expect(ctrl).toBeDefined();
expect(ctrl.id).toBe('Some');
ctrl = $controller('a.Other', {$scope: scope});
expect(ctrl instanceof scope.a.Other).toBe(true);
expect(ctrl.id).toBe('Other');
});
it('should export all methods to scope if scope not injected', function() {
var MyClass = function() {
this.prop1 = 1;
this.method1 = function(value) {
this.prop1 = 2;
return value;
};
};
MyClass.prototype = {
prop2: 1,
method2: function(value) {
this.prop2 = 2;
return value;
}
};
var scope = {},
ctrl = $controller(MyClass, {$scope: scope});
expect(ctrl).toBe(scope);
expect(scope.prop1).toBe(1);
expect(scope.prop2).toBe(1);
expect(scope.method1(true)).toBe(true);
expect(scope.method2(true)).toBe(true);
expect(scope.prop1).toBe(2);
expect(scope.prop2).toBe(2);
});
});
@idosela
Copy link

idosela commented Apr 13, 2012

If you support IE8 and below you need the following fix for the Array.indexOf method:
https://gist.github.com/2379920

@rafaeleyng
Copy link

What is BC?

@ifalok007
Copy link

@rafaeleyng BC is Backward Compatible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment