Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat(controller): support as instance syntax
Browse files Browse the repository at this point in the history
Support ng-controller="MyController as my" syntax
which publishes the controller instance to the
current scope.

Also supports exporting a controller defined with route:
````javascript
angular.module('routes', [], function($routeProvider) {
  $routeProvider.when('/home', {controller: 'Ctrl as home', templateUrl: '...'});
});
````
  • Loading branch information
mhevery committed Apr 23, 2013
1 parent 021bdf3 commit cd38cbf
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 34 deletions.
34 changes: 25 additions & 9 deletions src/ng/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
* {@link ng.$controllerProvider#register register} method.
*/
function $ControllerProvider() {
var controllers = {};
var controllers = {},
CNTRL_REG = /^(\w+)(\s+as\s+(\w+))?$/;


/**
Expand Down Expand Up @@ -56,17 +57,32 @@ function $ControllerProvider() {
* a service, so that one can override this service with {@link https://gist.github.com/1649788
* BC version}.
*/
return function(constructor, locals) {
if(isString(constructor)) {
var name = constructor;
constructor = controllers.hasOwnProperty(name)
? controllers[name]
: getter(locals.$scope, name, true) || getter($window, name, true);
return function(expression, locals) {
var instance, match, constructor, identifier;

assertArgFn(constructor, name, true);
if(isString(expression)) {
match = expression.match(CNTRL_REG),
constructor = match[1],
identifier = match[3];
expression = controllers.hasOwnProperty(constructor)
? controllers[constructor]
: getter(locals.$scope, constructor, true) || getter($window, constructor, true);

assertArgFn(expression, constructor, true);
}

instance = $injector.instantiate(expression, locals);

if (identifier) {
if (typeof locals.$scope !== 'object') {
throw new Error('Can not export controller as "' + identifier + '". ' +
'No scope object provided!');
}

locals.$scope[identifier] = instance;
}

return $injector.instantiate(constructor, locals);
return instance;
};
}];
}
73 changes: 71 additions & 2 deletions src/ng/directive/ngController.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,84 @@
* @scope
* @param {expression} ngController Name of a globally accessible constructor function or an
* {@link guide/expression expression} that on the current scope evaluates to a
* constructor function.
* constructor function. The controller instance can further be published into the scope
* by adding `as localName` the controller name attribute.
*
* @example
* Here is a simple form for editing user contact information. Adding, removing, clearing, and
* greeting are methods declared on the controller (see source tab). These methods can
* easily be called from the angular markup. Notice that the scope becomes the `this` for the
* controller's instance. This allows for easy access to the view data from the controller. Also
* notice that any changes to the data are automatically reflected in the View without the need
* for a manual update.
* for a manual update. The example is included in two different declaration styles based on
* your style preferences.
<doc:example>
<doc:source>
<script>
function SettingsController() {
this.name = "John Smith";
this.contacts = [
{type: 'phone', value: '408 555 1212'},
{type: 'email', value: 'john.smith@example.org'} ];
};
SettingsController.prototype.greet = function() {
alert(this.name);
};
SettingsController.prototype.addContact = function() {
this.contacts.push({type: 'email', value: 'yourname@example.org'});
};
SettingsController.prototype.removeContact = function(contactToRemove) {
var index = this.contacts.indexOf(contactToRemove);
this.contacts.splice(index, 1);
};
SettingsController.prototype.clearContact = function(contact) {
contact.type = 'phone';
contact.value = '';
};
</script>
<div ng-controller="SettingsController as settings">
Name: <input type="text" ng-model="settings.name"/>
[ <a href="" ng-click="settings.greet()">greet</a> ]<br/>
Contact:
<ul>
<li ng-repeat="contact in settings.contacts">
<select ng-model="contact.type">
<option>phone</option>
<option>email</option>
</select>
<input type="text" ng-model="contact.value"/>
[ <a href="" ng-click="settings.clearContact(contact)">clear</a>
| <a href="" ng-click="settings.removeContact(contact)">X</a> ]
</li>
<li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li>
</ul>
</div>
</doc:source>
<doc:scenario>
it('should check controller', function() {
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
expect(element('.doc-example-live li:nth-child(1) input').val())
.toBe('408 555 1212');
expect(element('.doc-example-live li:nth-child(2) input').val())
.toBe('john.smith@example.org');
element('.doc-example-live li:first a:contains("clear")').click();
expect(element('.doc-example-live li:first input').val()).toBe('');
element('.doc-example-live li:last a:contains("add")').click();
expect(element('.doc-example-live li:nth-child(3) input').val())
.toBe('yourname@example.org');
});
</doc:scenario>
</doc:example>
<doc:example>
<doc:source>
<script>
function SettingsController($scope) {
Expand Down Expand Up @@ -93,6 +161,7 @@
});
</doc:scenario>
</doc:example>
*/
var ngControllerDirective = [function() {
return {
Expand Down
51 changes: 28 additions & 23 deletions src/ng/directive/ngView.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* @example
<example module="ngView" animations="true">
<file name="index.html">
<div ng-controller="MainCntl">
<div ng-controller="MainCntl as main">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
Expand All @@ -37,26 +37,26 @@
ng-animate="{enter: 'example-enter', leave: 'example-leave'}"></div>
<hr />
<pre>$location.path() = {{$location.path()}}</pre>
<pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{$route.current.params}}</pre>
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
<pre>$routeParams = {{$routeParams}}</pre>
<pre>$location.path() = {{main.$location.path()}}</pre>
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{main.$route.current.params}}</pre>
<pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
<pre>$routeParams = {{main.$routeParams}}</pre>
</div>
</file>
<file name="book.html">
<div>
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
controller: {{book.name}}<br />
Book Id: {{book.params.bookId}}<br />
</div>
</file>
<file name="chapter.html">
<div>
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
Chapter Id: {{params.chapterId}}
controller: {{chapter.name}}<br />
Book Id: {{chapter.params.bookId}}<br />
Chapter Id: {{chapter.params.chapterId}}
</div>
</file>
Expand Down Expand Up @@ -104,31 +104,33 @@
angular.module('ngView', [], function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl
controller: BookCntl,
controllerAlias: 'book'
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl
controller: ChapterCntl,
controllerAlias: 'chapter'
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
function MainCntl($route, $routeParams, $location) {
this.$route = $route;
this.$location = $location;
this.$routeParams = $routeParams;
}
function BookCntl($scope, $routeParams) {
$scope.name = "BookCntl";
$scope.params = $routeParams;
function BookCntl($routeParams) {
this.name = "BookCntl";
this.params = $routeParams;
}
function ChapterCntl($scope, $routeParams) {
$scope.name = "ChapterCntl";
$scope.params = $routeParams;
function ChapterCntl($routeParams) {
this.name = "ChapterCntl";
this.params = $routeParams;
}
</file>
Expand Down Expand Up @@ -202,6 +204,9 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
if (current.controller) {
locals.$scope = lastScope;
controller = $controller(current.controller, locals);
if (current.controllerAlias) {
lastScope[current.controllerAlias] = controller;
}
element.children().data('$ngControllerController', controller);
}

Expand Down
2 changes: 2 additions & 0 deletions src/ng/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function $RouteProvider(){
* - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly
* created scope or the name of a {@link angular.Module#controller registered controller}
* if passed as a string.
* - `controllerAlias` – `{sttring=}` – A controller alias name. If present the controller will be
* published to scope under the `controllerAlias` name.
* - `template` – `{string=|function()=}` – html template as a string or function that returns
* an html template as a string which should be used by {@link ng.directive:ngView ngView} or
* {@link ng.directive:ngInclude ngInclude} directives.
Expand Down
11 changes: 11 additions & 0 deletions test/ng/controllerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,15 @@ describe('$controller', function() {

expect(ctrl.$scope).toBe(scope);
});


it('should publish controller instance into scope', function() {
var scope = {};

$controllerProvider.register('FooCtrl', function() { this.mark = 'foo'; });

var foo = $controller('FooCtrl as foo', {$scope: scope});
expect(scope.foo).toBe(foo);
expect(scope.foo.mark).toBe('foo');
});
});
23 changes: 23 additions & 0 deletions test/ng/directive/ngControllerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
describe('ngController', function() {
var element;

beforeEach(module(function($controllerProvider) {
$controllerProvider.register('PublicModule', function() {
this.mark = 'works';
});
}));
beforeEach(inject(function($window) {
$window.Greeter = function($scope) {
// private stuff (not exported to scope)
Expand All @@ -27,6 +32,10 @@ describe('ngController', function() {
$window.Child = function($scope) {
$scope.name = 'Adam';
};

$window.Public = function() {
this.mark = 'works';
}
}));

afterEach(function() {
Expand All @@ -41,6 +50,20 @@ describe('ngController', function() {
}));


it('should publish controller into scope', inject(function($compile, $rootScope) {
element = $compile('<div ng-controller="Public as p">{{p.mark}}</div>')($rootScope);
$rootScope.$digest();
expect(element.text()).toBe('works');
}));


it('should publish controller into scope from module', inject(function($compile, $rootScope) {
element = $compile('<div ng-controller="PublicModule as p">{{p.mark}}</div>')($rootScope);
$rootScope.$digest();
expect(element.text()).toBe('works');
}));


it('should allow nested controllers', inject(function($compile, $rootScope) {
element = $compile('<div ng-controller="Greeter"><div ng-controller="Child">{{greet(name)}}</div></div>')($rootScope);
$rootScope.$digest();
Expand Down
21 changes: 21 additions & 0 deletions test/ng/directive/ngViewSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ describe('ngView', function() {
});


it('should instantiate controller with an alias', function() {
var log = [], controllerScope,
Ctrl = function($scope) {
this.name = 'alias';
controllerScope = $scope;
};

module(function($compileProvider, $routeProvider) {
$routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl, controllerAlias: 'ctrl'});
});

inject(function($route, $rootScope, $templateCache, $location) {
$templateCache.put('/tpl.html', [200, '<div></div>', {}]);
$location.path('/some');
$rootScope.$digest();

expect(controllerScope.ctrl.name).toBe('alias');
});
});


it('should support string controller declaration', function() {
var MyCtrl = jasmine.createSpy('MyCtrl');

Expand Down

7 comments on commit cd38cbf

@olostan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to find somewhere desription/discussion about this commit? Is using this.smth = ... more prefarable then $scope.smth = ...? If yes, why? I assume nothing is added just to add, so there should be a good reason :) thnx.

@coli
Copy link

@coli coli commented on cd38cbf Apr 25, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about directive controllers?

@wbyoko
Copy link
Contributor

@wbyoko wbyoko commented on cd38cbf May 16, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, now to test most controllers you won't have to create a mock scope and as well not inject scope into the controller.
When your using inherited properties from up the scope chain it's more explicit.

Can't wait to use this. but would like to know about directive controllers.
Though, I assume you can use the ng-controller directive in the directive template if it can't be done in the object definition

@iammerrick
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directive controllers would be terrific for publishing an API for more behavior or dynamic directives like dialogs and popovers.

@meenie
Copy link

@meenie meenie commented on cd38cbf May 23, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olostan, maybe if you wanted to make whatever you are doing in your controller private? Because $scope inherits from it's parent and makes properties and methods available for it's children as well. If you put the properties and methods directly in the controller instead, they will remain private to that controller.

@meenie
Copy link

@meenie meenie commented on cd38cbf May 24, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olostan, just found this video by John Lindquist that better explains why you would want to use it - http://www.egghead.io/video/tTihyXaz4Bo

I do hope they keep the feature as it seems very very useful!

@guillaume86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to inject the alias in the controller?
Or even better inject a child scope "scoped" to the controller ($controllerScope),
so you can $watch something in the controller without knowing the alias ($watch("member" where member belong to controller).

Another idea: provide a way to make the controller inherit Scope (or mix it in) - not sure this one is a good idea

Please sign in to comment.