Skip to content

Commit cd38cbf

Browse files
committed
feat(controller): support as instance syntax
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: '...'}); }); ````
1 parent 021bdf3 commit cd38cbf

File tree

7 files changed

+181
-34
lines changed

7 files changed

+181
-34
lines changed

src/ng/controller.js

+25-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
* {@link ng.$controllerProvider#register register} method.
1212
*/
1313
function $ControllerProvider() {
14-
var controllers = {};
14+
var controllers = {},
15+
CNTRL_REG = /^(\w+)(\s+as\s+(\w+))?$/;
1516

1617

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

66-
assertArgFn(constructor, name, true);
63+
if(isString(expression)) {
64+
match = expression.match(CNTRL_REG),
65+
constructor = match[1],
66+
identifier = match[3];
67+
expression = controllers.hasOwnProperty(constructor)
68+
? controllers[constructor]
69+
: getter(locals.$scope, constructor, true) || getter($window, constructor, true);
70+
71+
assertArgFn(expression, constructor, true);
72+
}
73+
74+
instance = $injector.instantiate(expression, locals);
75+
76+
if (identifier) {
77+
if (typeof locals.$scope !== 'object') {
78+
throw new Error('Can not export controller as "' + identifier + '". ' +
79+
'No scope object provided!');
80+
}
81+
82+
locals.$scope[identifier] = instance;
6783
}
6884

69-
return $injector.instantiate(constructor, locals);
85+
return instance;
7086
};
7187
}];
7288
}

src/ng/directive/ngController.js

+71-2
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,84 @@
2121
* @scope
2222
* @param {expression} ngController Name of a globally accessible constructor function or an
2323
* {@link guide/expression expression} that on the current scope evaluates to a
24-
* constructor function.
24+
* constructor function. The controller instance can further be published into the scope
25+
* by adding `as localName` the controller name attribute.
2526
*
2627
* @example
2728
* Here is a simple form for editing user contact information. Adding, removing, clearing, and
2829
* greeting are methods declared on the controller (see source tab). These methods can
2930
* easily be called from the angular markup. Notice that the scope becomes the `this` for the
3031
* controller's instance. This allows for easy access to the view data from the controller. Also
3132
* notice that any changes to the data are automatically reflected in the View without the need
32-
* for a manual update.
33+
* for a manual update. The example is included in two different declaration styles based on
34+
* your style preferences.
3335
<doc:example>
36+
<doc:source>
37+
<script>
38+
function SettingsController() {
39+
this.name = "John Smith";
40+
this.contacts = [
41+
{type: 'phone', value: '408 555 1212'},
42+
{type: 'email', value: 'john.smith@example.org'} ];
43+
};
44+
45+
SettingsController.prototype.greet = function() {
46+
alert(this.name);
47+
};
48+
49+
SettingsController.prototype.addContact = function() {
50+
this.contacts.push({type: 'email', value: 'yourname@example.org'});
51+
};
52+
53+
SettingsController.prototype.removeContact = function(contactToRemove) {
54+
var index = this.contacts.indexOf(contactToRemove);
55+
this.contacts.splice(index, 1);
56+
};
57+
58+
SettingsController.prototype.clearContact = function(contact) {
59+
contact.type = 'phone';
60+
contact.value = '';
61+
};
62+
</script>
63+
<div ng-controller="SettingsController as settings">
64+
Name: <input type="text" ng-model="settings.name"/>
65+
[ <a href="" ng-click="settings.greet()">greet</a> ]<br/>
66+
Contact:
67+
<ul>
68+
<li ng-repeat="contact in settings.contacts">
69+
<select ng-model="contact.type">
70+
<option>phone</option>
71+
<option>email</option>
72+
</select>
73+
<input type="text" ng-model="contact.value"/>
74+
[ <a href="" ng-click="settings.clearContact(contact)">clear</a>
75+
| <a href="" ng-click="settings.removeContact(contact)">X</a> ]
76+
</li>
77+
<li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li>
78+
</ul>
79+
</div>
80+
</doc:source>
81+
<doc:scenario>
82+
it('should check controller', function() {
83+
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
84+
expect(element('.doc-example-live li:nth-child(1) input').val())
85+
.toBe('408 555 1212');
86+
expect(element('.doc-example-live li:nth-child(2) input').val())
87+
.toBe('john.smith@example.org');
88+
89+
element('.doc-example-live li:first a:contains("clear")').click();
90+
expect(element('.doc-example-live li:first input').val()).toBe('');
91+
92+
element('.doc-example-live li:last a:contains("add")').click();
93+
expect(element('.doc-example-live li:nth-child(3) input').val())
94+
95+
});
96+
</doc:scenario>
97+
</doc:example>
98+
99+
100+
101+
<doc:example>
34102
<doc:source>
35103
<script>
36104
function SettingsController($scope) {
@@ -93,6 +161,7 @@
93161
});
94162
</doc:scenario>
95163
</doc:example>
164+
96165
*/
97166
var ngControllerDirective = [function() {
98167
return {

src/ng/directive/ngView.js

+28-23
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* @example
2424
<example module="ngView" animations="true">
2525
<file name="index.html">
26-
<div ng-controller="MainCntl">
26+
<div ng-controller="MainCntl as main">
2727
Choose:
2828
<a href="Book/Moby">Moby</a> |
2929
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
@@ -37,26 +37,26 @@
3737
ng-animate="{enter: 'example-enter', leave: 'example-leave'}"></div>
3838
<hr />
3939
40-
<pre>$location.path() = {{$location.path()}}</pre>
41-
<pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
42-
<pre>$route.current.params = {{$route.current.params}}</pre>
43-
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
44-
<pre>$routeParams = {{$routeParams}}</pre>
40+
<pre>$location.path() = {{main.$location.path()}}</pre>
41+
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
42+
<pre>$route.current.params = {{main.$route.current.params}}</pre>
43+
<pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
44+
<pre>$routeParams = {{main.$routeParams}}</pre>
4545
</div>
4646
</file>
4747
4848
<file name="book.html">
4949
<div>
50-
controller: {{name}}<br />
51-
Book Id: {{params.bookId}}<br />
50+
controller: {{book.name}}<br />
51+
Book Id: {{book.params.bookId}}<br />
5252
</div>
5353
</file>
5454
5555
<file name="chapter.html">
5656
<div>
57-
controller: {{name}}<br />
58-
Book Id: {{params.bookId}}<br />
59-
Chapter Id: {{params.chapterId}}
57+
controller: {{chapter.name}}<br />
58+
Book Id: {{chapter.params.bookId}}<br />
59+
Chapter Id: {{chapter.params.chapterId}}
6060
</div>
6161
</file>
6262
@@ -104,31 +104,33 @@
104104
angular.module('ngView', [], function($routeProvider, $locationProvider) {
105105
$routeProvider.when('/Book/:bookId', {
106106
templateUrl: 'book.html',
107-
controller: BookCntl
107+
controller: BookCntl,
108+
controllerAlias: 'book'
108109
});
109110
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
110111
templateUrl: 'chapter.html',
111-
controller: ChapterCntl
112+
controller: ChapterCntl,
113+
controllerAlias: 'chapter'
112114
});
113115
114116
// configure html5 to get links working on jsfiddle
115117
$locationProvider.html5Mode(true);
116118
});
117119
118-
function MainCntl($scope, $route, $routeParams, $location) {
119-
$scope.$route = $route;
120-
$scope.$location = $location;
121-
$scope.$routeParams = $routeParams;
120+
function MainCntl($route, $routeParams, $location) {
121+
this.$route = $route;
122+
this.$location = $location;
123+
this.$routeParams = $routeParams;
122124
}
123125
124-
function BookCntl($scope, $routeParams) {
125-
$scope.name = "BookCntl";
126-
$scope.params = $routeParams;
126+
function BookCntl($routeParams) {
127+
this.name = "BookCntl";
128+
this.params = $routeParams;
127129
}
128130
129-
function ChapterCntl($scope, $routeParams) {
130-
$scope.name = "ChapterCntl";
131-
$scope.params = $routeParams;
131+
function ChapterCntl($routeParams) {
132+
this.name = "ChapterCntl";
133+
this.params = $routeParams;
132134
}
133135
</file>
134136
@@ -202,6 +204,9 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
202204
if (current.controller) {
203205
locals.$scope = lastScope;
204206
controller = $controller(current.controller, locals);
207+
if (current.controllerAlias) {
208+
lastScope[current.controllerAlias] = controller;
209+
}
205210
element.children().data('$ngControllerController', controller);
206211
}
207212

src/ng/route.js

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ function $RouteProvider(){
4444
* - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly
4545
* created scope or the name of a {@link angular.Module#controller registered controller}
4646
* if passed as a string.
47+
* - `controllerAlias` – `{sttring=}` – A controller alias name. If present the controller will be
48+
* published to scope under the `controllerAlias` name.
4749
* - `template` – `{string=|function()=}` – html template as a string or function that returns
4850
* an html template as a string which should be used by {@link ng.directive:ngView ngView} or
4951
* {@link ng.directive:ngInclude ngInclude} directives.

test/ng/controllerSpec.js

+11
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,15 @@ describe('$controller', function() {
8888

8989
expect(ctrl.$scope).toBe(scope);
9090
});
91+
92+
93+
it('should publish controller instance into scope', function() {
94+
var scope = {};
95+
96+
$controllerProvider.register('FooCtrl', function() { this.mark = 'foo'; });
97+
98+
var foo = $controller('FooCtrl as foo', {$scope: scope});
99+
expect(scope.foo).toBe(foo);
100+
expect(scope.foo.mark).toBe('foo');
101+
});
91102
});

test/ng/directive/ngControllerSpec.js

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
describe('ngController', function() {
44
var element;
55

6+
beforeEach(module(function($controllerProvider) {
7+
$controllerProvider.register('PublicModule', function() {
8+
this.mark = 'works';
9+
});
10+
}));
611
beforeEach(inject(function($window) {
712
$window.Greeter = function($scope) {
813
// private stuff (not exported to scope)
@@ -27,6 +32,10 @@ describe('ngController', function() {
2732
$window.Child = function($scope) {
2833
$scope.name = 'Adam';
2934
};
35+
36+
$window.Public = function() {
37+
this.mark = 'works';
38+
}
3039
}));
3140

3241
afterEach(function() {
@@ -41,6 +50,20 @@ describe('ngController', function() {
4150
}));
4251

4352

53+
it('should publish controller into scope', inject(function($compile, $rootScope) {
54+
element = $compile('<div ng-controller="Public as p">{{p.mark}}</div>')($rootScope);
55+
$rootScope.$digest();
56+
expect(element.text()).toBe('works');
57+
}));
58+
59+
60+
it('should publish controller into scope from module', inject(function($compile, $rootScope) {
61+
element = $compile('<div ng-controller="PublicModule as p">{{p.mark}}</div>')($rootScope);
62+
$rootScope.$digest();
63+
expect(element.text()).toBe('works');
64+
}));
65+
66+
4467
it('should allow nested controllers', inject(function($compile, $rootScope) {
4568
element = $compile('<div ng-controller="Greeter"><div ng-controller="Child">{{greet(name)}}</div></div>')($rootScope);
4669
$rootScope.$digest();

test/ng/directive/ngViewSpec.js

+21
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ describe('ngView', function() {
5555
});
5656

5757

58+
it('should instantiate controller with an alias', function() {
59+
var log = [], controllerScope,
60+
Ctrl = function($scope) {
61+
this.name = 'alias';
62+
controllerScope = $scope;
63+
};
64+
65+
module(function($compileProvider, $routeProvider) {
66+
$routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl, controllerAlias: 'ctrl'});
67+
});
68+
69+
inject(function($route, $rootScope, $templateCache, $location) {
70+
$templateCache.put('/tpl.html', [200, '<div></div>', {}]);
71+
$location.path('/some');
72+
$rootScope.$digest();
73+
74+
expect(controllerScope.ctrl.name).toBe('alias');
75+
});
76+
});
77+
78+
5879
it('should support string controller declaration', function() {
5980
var MyCtrl = jasmine.createSpy('MyCtrl');
6081

0 commit comments

Comments
 (0)