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

Commit 6e6fbe1

Browse files
committed
feat($compile): isolate scope properties in controller context via controllerAs
It is now possible to ask the $compiler's isolate scope property machinery to reference properties by their isolate scope. 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`.
1 parent 2d678f1 commit 6e6fbe1

File tree

3 files changed

+198
-52
lines changed

3 files changed

+198
-52
lines changed

src/ng/compile.js

+67-42
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
@@ -877,8 +881,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
877881
? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
878882
: $compileNodes;
879883

880-
forEach(transcludeControllers, function(instance, name) {
881-
$linkNode.data('$' + name + 'Controller', instance);
884+
forEach(transcludeControllers, function(controllerInstance, name) {
885+
$linkNode.data('$' + name + 'Controller', controllerInstance.instance);
882886
});
883887

884888
$linkNode.data('$scope', scope);
@@ -1195,6 +1199,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11951199
var terminalPriority = -Number.MAX_VALUE,
11961200
newScopeDirective,
11971201
controllerDirectives = previousCompileContext.controllerDirectives,
1202+
controllers,
11981203
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
11991204
templateDirective = previousCompileContext.templateDirective,
12001205
nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
@@ -1431,7 +1436,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14311436
value = null;
14321437

14331438
if (elementControllers && retrievalMethod === 'data') {
1434-
value = elementControllers[require];
1439+
if (value = elementControllers[require]) {
1440+
value = value.instance;
1441+
}
14351442
}
14361443
value = value || $element[retrievalMethod]('$' + require + 'Controller');
14371444

@@ -1460,9 +1467,43 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14601467
$element = attrs.$$element;
14611468

14621469
if (newIsolateScopeDirective) {
1463-
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
1464-
14651470
isolateScope = scope.$new(true);
1471+
}
1472+
1473+
transcludeFn = boundTranscludeFn && controllersBoundTransclude;
1474+
if (controllerDirectives) {
1475+
controllers = {};
1476+
forEach(controllerDirectives, function(directive) {
1477+
var locals = {
1478+
$scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
1479+
$element: $element,
1480+
$attrs: attrs,
1481+
$transclude: transcludeFn
1482+
}, controllerInstance;
1483+
1484+
controller = directive.controller;
1485+
if (controller == '@') {
1486+
controller = attrs[directive.name];
1487+
}
1488+
1489+
controllerInstance = $controller(controller, locals, true, directive.controllerAs);
1490+
1491+
// For directives with element transclusion the element is a comment,
1492+
// but jQuery .data doesn't support attaching data to comment nodes as it's hard to
1493+
// clean up (http://bugs.jquery.com/ticket/8335).
1494+
// Instead, we save the controllers for the element in a local hash and attach to .data
1495+
// later, once we have the actual element.
1496+
elementControllers[directive.name] = controllerInstance;
1497+
if (!hasElementTranscludeDirective) {
1498+
$element.data('$' + directive.name + 'Controller', controllerInstance.instance);
1499+
}
1500+
1501+
controllers[directive.name] = controllerInstance;
1502+
});
1503+
}
1504+
1505+
if (newIsolateScopeDirective) {
1506+
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
14661507

14671508
if (templateDirective && (templateDirective === newIsolateScopeDirective ||
14681509
templateDirective === newIsolateScopeDirective.$$originalDirective)) {
@@ -1475,27 +1516,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14751516

14761517
safeAddClass($element, 'ng-isolate-scope');
14771518

1519+
var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name];
1520+
var isolateBindingsContext = isolateScope;
1521+
if (isolateScopeController && isolateScopeController.identifier &&
1522+
newIsolateScopeDirective.bindToController === true) {
1523+
isolateBindingsContext = isolateScopeController.instance;
1524+
}
14781525
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
14791526
var match = definition.match(LOCAL_REGEXP) || [],
1480-
attrName = match[3] || scopeName,
1527+
attrName,
14811528
optional = (match[2] == '?'),
14821529
mode = match[1], // @, =, or &
14831530
lastValue,
14841531
parentGet, parentSet, compare;
14851532

1533+
attrName = match[3] || scopeName;
1534+
14861535
isolateScope.$$isolateBindings[scopeName] = mode + attrName;
14871536

14881537
switch (mode) {
14891538

14901539
case '@':
14911540
attrs.$observe(attrName, function(value) {
1492-
isolateScope[scopeName] = value;
1541+
isolateBindingsContext[scopeName] = value;
14931542
});
14941543
attrs.$$observers[attrName].$$scope = scope;
14951544
if( attrs[attrName] ) {
14961545
// If the attribute has been provided then we trigger an interpolation to ensure
14971546
// the value is there for use in the link fn
1498-
isolateScope[scopeName] = $interpolate(attrs[attrName])(scope);
1547+
isolateBindingsContext[scopeName] = $interpolate(attrs[attrName])(scope);
14991548
}
15001549
break;
15011550

@@ -1511,21 +1560,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15111560
}
15121561
parentSet = parentGet.assign || function() {
15131562
// reset the change, or we will throw this exception on every $digest
1514-
lastValue = isolateScope[scopeName] = parentGet(scope);
1563+
lastValue = isolateBindingsContext[scopeName] = parentGet(scope);
15151564
throw $compileMinErr('nonassign',
15161565
"Expression '{0}' used with directive '{1}' is non-assignable!",
15171566
attrs[attrName], newIsolateScopeDirective.name);
15181567
};
1519-
lastValue = isolateScope[scopeName] = parentGet(scope);
1568+
lastValue = isolateBindingsContext[scopeName] = parentGet(scope);
15201569
var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) {
1521-
if (!compare(parentValue, isolateScope[scopeName])) {
1570+
if (!compare(parentValue, isolateBindingsContext[scopeName])) {
15221571
// we are out of sync and need to copy
15231572
if (!compare(parentValue, lastValue)) {
15241573
// parent changed and it has precedence
1525-
isolateScope[scopeName] = parentValue;
1574+
isolateBindingsContext[scopeName] = parentValue;
15261575
} else {
15271576
// if the parent can be assigned then do so
1528-
parentSet(scope, parentValue = isolateScope[scopeName]);
1577+
parentSet(scope, parentValue = isolateBindingsContext[scopeName]);
15291578
}
15301579
}
15311580
return lastValue = parentValue;
@@ -1535,7 +1584,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15351584

15361585
case '&':
15371586
parentGet = $parse(attrs[attrName]);
1538-
isolateScope[scopeName] = function(locals) {
1587+
isolateBindingsContext[scopeName] = function(locals) {
15391588
return parentGet(scope, locals);
15401589
};
15411590
break;
@@ -1548,36 +1597,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15481597
}
15491598
});
15501599
}
1551-
transcludeFn = boundTranscludeFn && controllersBoundTransclude;
1552-
if (controllerDirectives) {
1553-
forEach(controllerDirectives, function(directive) {
1554-
var locals = {
1555-
$scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
1556-
$element: $element,
1557-
$attrs: attrs,
1558-
$transclude: transcludeFn
1559-
}, controllerInstance;
15601600

1561-
controller = directive.controller;
1562-
if (controller == '@') {
1563-
controller = attrs[directive.name];
1564-
}
1565-
1566-
controllerInstance = $controller(controller, locals);
1567-
// For directives with element transclusion the element is a comment,
1568-
// but jQuery .data doesn't support attaching data to comment nodes as it's hard to
1569-
// clean up (http://bugs.jquery.com/ticket/8335).
1570-
// Instead, we save the controllers for the element in a local hash and attach to .data
1571-
// later, once we have the actual element.
1572-
elementControllers[directive.name] = controllerInstance;
1573-
if (!hasElementTranscludeDirective) {
1574-
$element.data('$' + directive.name + 'Controller', controllerInstance);
1575-
}
1576-
1577-
if (directive.controllerAs) {
1578-
locals.$scope[directive.controllerAs] = controllerInstance;
1579-
}
1601+
if (controllers) {
1602+
forEach(controllers, function(controller, key) {
1603+
controller();
15801604
});
1605+
controllers = null;
15811606
}
15821607

15831608
// PRELINKING

src/ng/controller.js

+53-10
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 parameters:
73+
// `later` is a boolean value indicating whether or not we want to instantiate the controller
74+
// at a later time. This is used by the compiler for instantiating directive controllers, so
75+
// that it has the chance to apply bindings to the instance before invoking the constructor
76+
// (so that controllerAs bindings are available immediately).
77+
//
78+
// `ident` is a string which optionally overrides the identifier expression. This is used to
79+
// allow the directive API's controllerAs parameter to be assigned to scope.
7280
var instance, match, constructor, identifier;
73-
81+
later = later === true;
82+
if (ident && isString(ident)) {
83+
identifier = ident;
84+
}
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 constructor 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+
var returnedValue = $injector.invoke(expression, instance, locals, constructor);
118+
return (isObject(returnedValue) || isFunction(returnedValue)) ? returnedValue : 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
@@ -3283,6 +3283,84 @@ describe('$compile', function() {
32833283
expect(componentScope.$$isolateBindings.exprAlias).toBe('&expr');
32843284

32853285
}));
3286+
3287+
3288+
it('should expose isolate scope variables on controller with controllerAs when bindToController is true', function() {
3289+
var controllerCalled = false;
3290+
module(function($compileProvider) {
3291+
$compileProvider.directive('fooDir', valueFn({
3292+
template: '<p>isolate</p>',
3293+
scope: {
3294+
'data': '=dirData',
3295+
'str': '@dirStr',
3296+
'fn': '&dirFn'
3297+
},
3298+
controller: function($scope) {
3299+
expect(this.data).toEqualData({
3300+
'foo': 'bar',
3301+
'baz': 'biz'
3302+
});
3303+
expect(this.str).toBe('Hello, world!');
3304+
expect(this.fn()).toBe('called!');
3305+
controllerCalled = true;
3306+
},
3307+
controllerAs: 'test',
3308+
bindToController: true
3309+
}));
3310+
});
3311+
inject(function($compile, $rootScope) {
3312+
$rootScope.fn = valueFn('called!');
3313+
$rootScope.whom = 'world';
3314+
$rootScope.remoteData = {
3315+
'foo': 'bar',
3316+
'baz': 'biz'
3317+
};
3318+
element = $compile('<div foo-dir dir-data="remoteData" ' +
3319+
'dir-str="Hello, {{whom}}!" ' +
3320+
'dir-fn="fn()"></div>')($rootScope);
3321+
expect(controllerCalled).toBe(true);
3322+
});
3323+
});
3324+
3325+
3326+
it('should expose isolate scope variables on controller with controllerAs when bindToController is true', function() {
3327+
var controllerCalled = false;
3328+
module(function($compileProvider) {
3329+
$compileProvider.directive('fooDir', valueFn({
3330+
templateUrl: 'test.html',
3331+
scope: {
3332+
'data': '=dirData',
3333+
'str': '@dirStr',
3334+
'fn': '&dirFn'
3335+
},
3336+
controller: function($scope) {
3337+
expect(this.data).toEqualData({
3338+
'foo': 'bar',
3339+
'baz': 'biz'
3340+
});
3341+
expect(this.str).toBe('Hello, world!');
3342+
expect(this.fn()).toBe('called!');
3343+
controllerCalled = true;
3344+
},
3345+
controllerAs: 'test',
3346+
bindToController: true
3347+
}));
3348+
});
3349+
inject(function($compile, $rootScope, $templateCache) {
3350+
$templateCache.put('test.html', '<p>isolate</p>');
3351+
$rootScope.fn = valueFn('called!');
3352+
$rootScope.whom = 'world';
3353+
$rootScope.remoteData = {
3354+
'foo': 'bar',
3355+
'baz': 'biz'
3356+
};
3357+
element = $compile('<div foo-dir dir-data="remoteData" ' +
3358+
'dir-str="Hello, {{whom}}!" ' +
3359+
'dir-fn="fn()"></div>')($rootScope);
3360+
$rootScope.$digest();
3361+
expect(controllerCalled).toBe(true);
3362+
});
3363+
});
32863364
});
32873365

32883366

0 commit comments

Comments
 (0)