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

Commit 5f3f25a

Browse files
caitpIgorMinar
authored andcommitted
feat($compile): bind isolate scope properties to controller
It is now possible to ask the $compiler's isolate scope property machinery to bind isolate scope properties to a controller rather than scope itself. This feature requires the use of controllerAs, so that the controller-bound properties may still be referenced from binding expressions in views. The current syntax is to prefix the scope name with a '@', like so: scope: { "myData": "=someData", "myString": "@someInterpolation", "myExpr": "&someExpr" }, controllerAs: "someCtrl", bindtoController: true The putting of properties within the context of the controller will only occur if controllerAs is used for an isolate scope with the `bindToController` property of the directive definition object set to `true`. Closes #7635 Closes #7645
1 parent cb73a37 commit 5f3f25a

File tree

3 files changed

+194
-50
lines changed

3 files changed

+194
-50
lines changed

src/ng/compile.js

+64-41
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@
175175
* by calling the `localFn` as `localFn({amount: 22})`.
176176
*
177177
*
178+
* #### `bindToController`
179+
* When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController` will
180+
* allow a component to have its properties bound to the controller, rather than to scope. When the controller
181+
* is instantiated, the initial values of the isolate scope bindings are already available.
178182
*
179183
* #### `controller`
180184
* Controller constructor function. The controller is instantiated before the
@@ -981,7 +985,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
981985

982986
if (transcludeControllers) {
983987
for (var controllerName in transcludeControllers) {
984-
$linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName]);
988+
$linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance);
985989
}
986990
}
987991

@@ -1316,6 +1320,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
13161320
var terminalPriority = -Number.MAX_VALUE,
13171321
newScopeDirective,
13181322
controllerDirectives = previousCompileContext.controllerDirectives,
1323+
controllers,
13191324
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
13201325
templateDirective = previousCompileContext.templateDirective,
13211326
nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
@@ -1553,7 +1558,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15531558
value = null;
15541559

15551560
if (elementControllers && retrievalMethod === 'data') {
1556-
value = elementControllers[require];
1561+
if (value = elementControllers[require]) {
1562+
value = value.instance;
1563+
}
15571564
}
15581565
value = value || $element[retrievalMethod]('$' + require + 'Controller');
15591566

@@ -1586,14 +1593,56 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15861593
}
15871594

15881595
if (newIsolateScopeDirective) {
1589-
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
1590-
15911596
isolateScope = scope.$new(true);
1597+
}
1598+
1599+
transcludeFn = boundTranscludeFn && controllersBoundTransclude;
1600+
if (controllerDirectives) {
1601+
// TODO: merge `controllers` and `elementControllers` into single object.
1602+
controllers = {};
1603+
elementControllers = {};
1604+
forEach(controllerDirectives, function(directive) {
1605+
var locals = {
1606+
$scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
1607+
$element: $element,
1608+
$attrs: attrs,
1609+
$transclude: transcludeFn
1610+
}, controllerInstance;
1611+
1612+
controller = directive.controller;
1613+
if (controller == '@') {
1614+
controller = attrs[directive.name];
1615+
}
1616+
1617+
controllerInstance = $controller(controller, locals, true, directive.controllerAs);
1618+
1619+
// For directives with element transclusion the element is a comment,
1620+
// but jQuery .data doesn't support attaching data to comment nodes as it's hard to
1621+
// clean up (http://bugs.jquery.com/ticket/8335).
1622+
// Instead, we save the controllers for the element in a local hash and attach to .data
1623+
// later, once we have the actual element.
1624+
elementControllers[directive.name] = controllerInstance;
1625+
if (!hasElementTranscludeDirective) {
1626+
$element.data('$' + directive.name + 'Controller', controllerInstance.instance);
1627+
}
1628+
1629+
controllers[directive.name] = controllerInstance;
1630+
});
1631+
}
1632+
1633+
if (newIsolateScopeDirective) {
1634+
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
15921635

15931636
compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
15941637
templateDirective === newIsolateScopeDirective.$$originalDirective)));
15951638
compile.$$addScopeClass($element, true);
15961639

1640+
var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name];
1641+
var isolateBindingContext = isolateScope;
1642+
if (isolateScopeController && isolateScopeController.identifier &&
1643+
newIsolateScopeDirective.bindToController === true) {
1644+
isolateBindingContext = isolateScopeController.instance;
1645+
}
15971646
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
15981647
var match = definition.match(LOCAL_REGEXP) || [],
15991648
attrName = match[3] || scopeName,
@@ -1614,7 +1663,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16141663
if( attrs[attrName] ) {
16151664
// If the attribute has been provided then we trigger an interpolation to ensure
16161665
// the value is there for use in the link fn
1617-
isolateScope[scopeName] = $interpolate(attrs[attrName])(scope);
1666+
isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope);
16181667
}
16191668
break;
16201669

@@ -1630,21 +1679,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16301679
}
16311680
parentSet = parentGet.assign || function() {
16321681
// reset the change, or we will throw this exception on every $digest
1633-
lastValue = isolateScope[scopeName] = parentGet(scope);
1682+
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
16341683
throw $compileMinErr('nonassign',
16351684
"Expression '{0}' used with directive '{1}' is non-assignable!",
16361685
attrs[attrName], newIsolateScopeDirective.name);
16371686
};
1638-
lastValue = isolateScope[scopeName] = parentGet(scope);
1687+
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
16391688
var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) {
1640-
if (!compare(parentValue, isolateScope[scopeName])) {
1689+
if (!compare(parentValue, isolateBindingContext[scopeName])) {
16411690
// we are out of sync and need to copy
16421691
if (!compare(parentValue, lastValue)) {
16431692
// parent changed and it has precedence
1644-
isolateScope[scopeName] = parentValue;
1693+
isolateBindingContext[scopeName] = parentValue;
16451694
} else {
16461695
// if the parent can be assigned then do so
1647-
parentSet(scope, parentValue = isolateScope[scopeName]);
1696+
parentSet(scope, parentValue = isolateBindingContext[scopeName]);
16481697
}
16491698
}
16501699
return lastValue = parentValue;
@@ -1654,7 +1703,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16541703

16551704
case '&':
16561705
parentGet = $parse(attrs[attrName]);
1657-
isolateScope[scopeName] = function(locals) {
1706+
isolateBindingContext[scopeName] = function(locals) {
16581707
return parentGet(scope, locals);
16591708
};
16601709
break;
@@ -1667,37 +1716,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16671716
}
16681717
});
16691718
}
1670-
transcludeFn = boundTranscludeFn && controllersBoundTransclude;
1671-
if (controllerDirectives) {
1672-
elementControllers = {};
1673-
forEach(controllerDirectives, function(directive) {
1674-
var locals = {
1675-
$scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
1676-
$element: $element,
1677-
$attrs: attrs,
1678-
$transclude: transcludeFn
1679-
}, controllerInstance;
1680-
1681-
controller = directive.controller;
1682-
if (controller == '@') {
1683-
controller = attrs[directive.name];
1684-
}
1685-
1686-
controllerInstance = $controller(controller, locals);
1687-
// For directives with element transclusion the element is a comment,
1688-
// but jQuery .data doesn't support attaching data to comment nodes as it's hard to
1689-
// clean up (http://bugs.jquery.com/ticket/8335).
1690-
// Instead, we save the controllers for the element in a local hash and attach to .data
1691-
// later, once we have the actual element.
1692-
elementControllers[directive.name] = controllerInstance;
1693-
if (!hasElementTranscludeDirective) {
1694-
$element.data('$' + directive.name + 'Controller', controllerInstance);
1695-
}
1696-
1697-
if (directive.controllerAs) {
1698-
locals.$scope[directive.controllerAs] = controllerInstance;
1699-
}
1719+
if (controllers) {
1720+
forEach(controllers, function(controller) {
1721+
controller();
17001722
});
1723+
controllers = null;
17011724
}
17021725

17031726
// PRELINKING

src/ng/controller.js

+52-9
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,24 @@ function $ControllerProvider() {
6868
* It's just a simple call to {@link auto.$injector $injector}, but extracted into
6969
* a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
7070
*/
71-
return function(expression, locals) {
71+
return function(expression, locals, later, ident) {
72+
// PRIVATE API:
73+
// param `later` --- indicates that the controller's constructor is invoked at a later time.
74+
// If true, $controller will allocate the object with the correct
75+
// prototype chain, but will not invoke the controller until a returned
76+
// callback is invoked.
77+
// param `ident` --- An optional label which overrides the label parsed from the controller
78+
// expression, if any.
7279
var instance, match, constructor, identifier;
80+
later = later === true;
81+
if (ident && isString(ident)) {
82+
identifier = ident;
83+
}
7384

7485
if(isString(expression)) {
7586
match = expression.match(CNTRL_REG),
7687
constructor = match[1],
77-
identifier = match[3];
88+
identifier = identifier || match[3];
7889
expression = controllers.hasOwnProperty(constructor)
7990
? controllers[constructor]
8091
: getter(locals.$scope, constructor, true) ||
@@ -83,19 +94,51 @@ function $ControllerProvider() {
8394
assertArgFn(expression, constructor, true);
8495
}
8596

86-
instance = $injector.instantiate(expression, locals, constructor);
97+
if (later) {
98+
// Instantiate controller later:
99+
// This machinery is used to create an instance of the object before calling the
100+
// controller's constructor itself.
101+
//
102+
// This allows properties to be added to the controller before the constructor is
103+
// invoked. Primarily, this is used for isolate scope bindings in $compile.
104+
//
105+
// This feature is not intended for use by applications, and is thus not documented
106+
// publicly.
107+
var Constructor = function() {};
108+
Constructor.prototype = (isArray(expression) ?
109+
expression[expression.length - 1] : expression).prototype;
110+
instance = new Constructor();
87111

88-
if (identifier) {
89-
if (!(locals && typeof locals.$scope === 'object')) {
90-
throw minErr('$controller')('noscp',
91-
"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
92-
constructor || expression.name, identifier);
112+
if (identifier) {
113+
addIdentifier(locals, identifier, instance, constructor || expression.name);
93114
}
94115

95-
locals.$scope[identifier] = instance;
116+
return extend(function() {
117+
$injector.invoke(expression, instance, locals, constructor);
118+
return instance;
119+
}, {
120+
instance: instance,
121+
identifier: identifier
122+
});
123+
}
124+
125+
instance = $injector.instantiate(expression, locals, constructor);
126+
127+
if (identifier) {
128+
addIdentifier(locals, identifier, instance, constructor || expression.name);
96129
}
97130

98131
return instance;
99132
};
133+
134+
function addIdentifier(locals, identifier, instance, name) {
135+
if (!(locals && isObject(locals.$scope))) {
136+
throw minErr('$controller')('noscp',
137+
"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
138+
name, identifier);
139+
}
140+
141+
locals.$scope[identifier] = instance;
142+
}
100143
}];
101144
}

test/ng/compileSpec.js

+78
Original file line numberDiff line numberDiff line change
@@ -3516,6 +3516,84 @@ describe('$compile', function() {
35163516
expect(componentScope.$$isolateBindings.exprAlias).toBe('&expr');
35173517

35183518
}));
3519+
3520+
3521+
it('should expose isolate scope variables on controller with controllerAs when bindToController is true', function() {
3522+
var controllerCalled = false;
3523+
module(function($compileProvider) {
3524+
$compileProvider.directive('fooDir', valueFn({
3525+
template: '<p>isolate</p>',
3526+
scope: {
3527+
'data': '=dirData',
3528+
'str': '@dirStr',
3529+
'fn': '&dirFn'
3530+
},
3531+
controller: function($scope) {
3532+
expect(this.data).toEqualData({
3533+
'foo': 'bar',
3534+
'baz': 'biz'
3535+
});
3536+
expect(this.str).toBe('Hello, world!');
3537+
expect(this.fn()).toBe('called!');
3538+
controllerCalled = true;
3539+
},
3540+
controllerAs: 'test',
3541+
bindToController: true
3542+
}));
3543+
});
3544+
inject(function($compile, $rootScope) {
3545+
$rootScope.fn = valueFn('called!');
3546+
$rootScope.whom = 'world';
3547+
$rootScope.remoteData = {
3548+
'foo': 'bar',
3549+
'baz': 'biz'
3550+
};
3551+
element = $compile('<div foo-dir dir-data="remoteData" ' +
3552+
'dir-str="Hello, {{whom}}!" ' +
3553+
'dir-fn="fn()"></div>')($rootScope);
3554+
expect(controllerCalled).toBe(true);
3555+
});
3556+
});
3557+
3558+
3559+
it('should expose isolate scope variables on controller with controllerAs when bindToController is true', function() {
3560+
var controllerCalled = false;
3561+
module(function($compileProvider) {
3562+
$compileProvider.directive('fooDir', valueFn({
3563+
templateUrl: 'test.html',
3564+
scope: {
3565+
'data': '=dirData',
3566+
'str': '@dirStr',
3567+
'fn': '&dirFn'
3568+
},
3569+
controller: function($scope) {
3570+
expect(this.data).toEqualData({
3571+
'foo': 'bar',
3572+
'baz': 'biz'
3573+
});
3574+
expect(this.str).toBe('Hello, world!');
3575+
expect(this.fn()).toBe('called!');
3576+
controllerCalled = true;
3577+
},
3578+
controllerAs: 'test',
3579+
bindToController: true
3580+
}));
3581+
});
3582+
inject(function($compile, $rootScope, $templateCache) {
3583+
$templateCache.put('test.html', '<p>isolate</p>');
3584+
$rootScope.fn = valueFn('called!');
3585+
$rootScope.whom = 'world';
3586+
$rootScope.remoteData = {
3587+
'foo': 'bar',
3588+
'baz': 'biz'
3589+
};
3590+
element = $compile('<div foo-dir dir-data="remoteData" ' +
3591+
'dir-str="Hello, {{whom}}!" ' +
3592+
'dir-fn="fn()"></div>')($rootScope);
3593+
$rootScope.$digest();
3594+
expect(controllerCalled).toBe(true);
3595+
});
3596+
});
35193597
});
35203598

35213599

0 commit comments

Comments
 (0)