Skip to content

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 angular#7635 Closes angular#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

Lines changed: 64 additions & 41 deletions
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

Lines changed: 52 additions & 9 deletions
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

Lines changed: 78 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy