Skip to content
Permalink
Browse files

fix($compile): respect return value from controller constructor

The return value of the controller constructor is now respected in all cases.

If controllerAs is used, the controller will be re-bound to scope. If bindToController is used,
the previous binding $watches (if any) will be unwatched, and bindings re-installed on the new
controller.
  • Loading branch information...
caitp committed Dec 17, 2014
1 parent 35498d7 commit 62d514b06937cc7dd86e973ea11165c88343b42d
Showing with 152 additions and 25 deletions.
  1. +30 −19 src/ng/compile.js
  2. +10 −2 src/ng/controller.js
  3. +112 −4 test/ng/compileSpec.js
@@ -1373,15 +1373,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (nodeLinkFn.scope) {
childScope = scope.$new();
compile.$$addScopeInfo(jqLite(node), childScope);
var onDestroyed = nodeLinkFn.$$onScopeDestroyed;
if (onDestroyed) {
nodeLinkFn.$$onScopeDestroyed = null;
childScope.$on('$destroyed', function() {
for (var i=0, ii = onDestroyed.length; i < ii; ++i) {
onDestroyed[i]();
}
onDestroyed = null;
});
var destroyBindings = nodeLinkFn.$$destroyBindings;
if (destroyBindings) {
nodeLinkFn.$$destroyBindings = null;
childScope.$on('$destroyed', destroyBindings);
}
} else {
childScope = scope;
@@ -1955,18 +1950,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (controllers) {
// Initialize bindToController bindings for new/isolate scopes
var scopeDirective = newIsolateScopeDirective || newScopeDirective;
var bindings;
var controllerForBindings;
if (scopeDirective && controllers[scopeDirective.name]) {
var bindings = scopeDirective.$$bindings.bindToController;
bindings = scopeDirective.$$bindings.bindToController;
controller = controllers[scopeDirective.name];

if (controller && controller.identifier && bindings) {
thisLinkFn.$$onScopeDestroyed =
controllerForBindings = controller;
thisLinkFn.$$destroyBindings =
initializeDirectiveBindings(scope, attrs, controller.instance,
bindings, scopeDirective);
}
}
forEach(controllers, function(controller) {
controller();
var result = controller();
if (result !== controller.instance &&
controller === controllerForBindings) {
// Remove and re-install bindToController bindings
thisLinkFn.$$destroyBindings();
thisLinkFn.$$destroyBindings =
initializeDirectiveBindings(scope, attrs, result,
bindings, scopeDirective);
}
});
controllers = null;
}
@@ -2558,12 +2564,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
}
if (newScope) {
newScope.$on('$destroy', unwatch);
} else {
onNewScopeDestroyed = (onNewScopeDestroyed || []);
onNewScopeDestroyed.push(unwatch);
}
onNewScopeDestroyed = (onNewScopeDestroyed || []);
onNewScopeDestroyed.push(unwatch);
break;

case '&':
@@ -2574,7 +2576,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
}
});
return onNewScopeDestroyed;
var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
onNewScopeDestroyed[i]();
}
} : noop;
if (newScope && destroyBindings !== noop) {
newScope.$on('$destroy', destroyBindings);
return noop;
}
return destroyBindings;
}
}];
}
@@ -133,8 +133,16 @@ function $ControllerProvider() {
addIdentifier(locals, identifier, instance, constructor || expression.name);
}

return extend(function() {
$injector.invoke(expression, instance, locals, constructor);
var instantiate;
return instantiate = extend(function() {
var result = $injector.invoke(expression, instance, locals, constructor);
if (result !== instance && (isObject(result) || isFunction(result))) {
instance = result;
if (identifier) {
// If result changed, re-assign controllerAs value to scope.
addIdentifier(locals, identifier, instance, constructor || expression.name);
}
}
return instance;
}, {
instance: instance,
@@ -3910,8 +3910,8 @@ describe('$compile', function() {
'baz': 'biz'
};
element = $compile('<div foo-dir dir-data="remoteData" ' +
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
$rootScope.$digest();
expect(controllerCalled).toBe(true);
});
@@ -3950,8 +3950,8 @@ describe('$compile', function() {
'baz': 'biz'
};
element = $compile('<div foo-dir dir-data="remoteData" ' +
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
$rootScope.$digest();
expect(controllerCalled).toBe(true);
});
@@ -3983,6 +3983,114 @@ describe('$compile', function() {
expect(childScope.theCtrl).toBe(myCtrl);
});
});


it('should re-install controllerAs and bindings for returned value from controller (new scope)', function() {
var controllerCalled = false;
var myCtrl;

function MyCtrl() {
}
MyCtrl.prototype.test = function() {
expect(this.data).toEqualData({
'foo': 'bar',
'baz': 'biz'
});
expect(this.str).toBe('Hello, world!');
expect(this.fn()).toBe('called!');
}

module(function($compileProvider, $controllerProvider) {
$controllerProvider.register('myCtrl', function() {
controllerCalled = true;
myCtrl = this;
return new MyCtrl();
});
$compileProvider.directive('fooDir', valueFn({
templateUrl: 'test.html',
bindToController: {
'data': '=dirData',
'str': '@dirStr',
'fn': '&dirFn'
},
scope: true,
controller: 'myCtrl as theCtrl'
}));
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('test.html', '<p>isolate</p>');
$rootScope.fn = valueFn('called!');
$rootScope.whom = 'world';
$rootScope.remoteData = {
'foo': 'bar',
'baz': 'biz'
};
element = $compile('<div foo-dir dir-data="remoteData" ' +
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
$rootScope.$digest();
expect(controllerCalled).toBe(true);
var childScope = element.children().scope();
expect(childScope).not.toBe($rootScope);
expect(childScope.theCtrl).not.toBe(myCtrl);
expect(childScope.theCtrl.constructor).toBe(MyCtrl);
childScope.theCtrl.test();
});
});


it('should re-install controllerAs and bindings for returned value from controller (isolate scope)', function() {
var controllerCalled = false;
var myCtrl;

function MyCtrl() {
}
MyCtrl.prototype.test = function() {
expect(this.data).toEqualData({
'foo': 'bar',
'baz': 'biz'
});
expect(this.str).toBe('Hello, world!');
expect(this.fn()).toBe('called!');
}

module(function($compileProvider, $controllerProvider) {
$controllerProvider.register('myCtrl', function() {
controllerCalled = true;
myCtrl = this;
return new MyCtrl();
});
$compileProvider.directive('fooDir', valueFn({
templateUrl: 'test.html',
bindToController: true,
scope: {
'data': '=dirData',
'str': '@dirStr',
'fn': '&dirFn'
},
controller: 'myCtrl as theCtrl'
}));
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('test.html', '<p>isolate</p>');
$rootScope.fn = valueFn('called!');
$rootScope.whom = 'world';
$rootScope.remoteData = {
'foo': 'bar',
'baz': 'biz'
};
element = $compile('<div foo-dir dir-data="remoteData" ' +
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
$rootScope.$digest();
expect(controllerCalled).toBe(true);
var childScope = element.children().scope();
expect(childScope).not.toBe($rootScope);
expect(childScope.theCtrl).not.toBe(myCtrl);
expect(childScope.theCtrl.constructor).toBe(MyCtrl);
childScope.theCtrl.test();
});
});
});


0 comments on commit 62d514b

Please sign in to comment.
You can’t perform that action at this time.