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

Commit 909cabd

Browse files
vojtajinaIgorMinar
authored andcommitted
fix($compile): make isolate scope truly isolate
Fixes issue with isolate scope leaking all over the place into other directives on the same element. Isolate scope is now available only to the isolate directive that requested it and its template. A non-isolate directive should not get the isolate scope of an isolate directive on the same element, instead they will receive the original scope (which is the parent scope of the newly created isolate scope). Paired with Tobias. BREAKING CHANGE: Directives without isolate scope do not get the isolate scope from an isolate directive on the same element. If your code depends on this behavior (non-isolate directive needs to access state from within the isolate scope), change the isolate directive to use scope locals to pass these explicitly. // before <input ng-model="$parent.value" ng-isolate> .directive('ngIsolate', function() { return { scope: {}, template: '{{value}}' }; }); // after <input ng-model="value" ng-isolate> .directive('ngIsolate', function() { return { scope: {value: '=ngModel'}, template: '{{value}} }; }); Closes #1924 Closes #2500
1 parent 3662140 commit 909cabd

File tree

3 files changed

+114
-29
lines changed

3 files changed

+114
-29
lines changed

Diff for: src/ng/compile.js

+31-25
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ function $CompileProvider($provide) {
901901
return linkFnFound ? compositeLinkFn : null;
902902

903903
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
904-
var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
904+
var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n;
905905

906906
// copy nodeList so that linking doesn't break due to live list updates.
907907
var stableNodeList = [];
@@ -913,11 +913,13 @@ function $CompileProvider($provide) {
913913
node = stableNodeList[n];
914914
nodeLinkFn = linkFns[i++];
915915
childLinkFn = linkFns[i++];
916+
$node = jqLite(node);
916917

917918
if (nodeLinkFn) {
918919
if (nodeLinkFn.scope) {
919-
childScope = scope.$new(isObject(nodeLinkFn.scope));
920-
jqLite(node).data('$scope', childScope);
920+
childScope = scope.$new();
921+
$node.data('$scope', childScope);
922+
safeAddClass($node, 'ng-scope');
921923
} else {
922924
childScope = scope;
923925
}
@@ -1155,10 +1157,8 @@ function $CompileProvider($provide) {
11551157
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
11561158
$compileNode);
11571159
if (isObject(directiveValue)) {
1158-
safeAddClass($compileNode, 'ng-isolate-scope');
11591160
newIsolateScopeDirective = directive;
11601161
}
1161-
safeAddClass($compileNode, 'ng-scope');
11621162
}
11631163
}
11641164

@@ -1291,7 +1291,7 @@ function $CompileProvider($provide) {
12911291

12921292
}
12931293

1294-
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
1294+
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
12951295
nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
12961296

12971297
// might be normal or delayed nodeLinkFn depending on if templateUrl is present
@@ -1303,11 +1303,13 @@ function $CompileProvider($provide) {
13031303
if (pre) {
13041304
if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
13051305
pre.require = directive.require;
1306+
if (newIsolateScopeDirective === directive) pre.isolateScope = true;
13061307
preLinkFns.push(pre);
13071308
}
13081309
if (post) {
13091310
if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
13101311
post.require = directive.require;
1312+
if (newIsolateScopeDirective === directive) post.isolateScope = true;
13111313
postLinkFns.push(post);
13121314
}
13131315
}
@@ -1348,7 +1350,7 @@ function $CompileProvider($provide) {
13481350

13491351

13501352
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
1351-
var attrs, $element, i, ii, linkFn, controller;
1353+
var attrs, $element, i, ii, linkFn, controller, isolateScope;
13521354

13531355
if (compileNode === linkNode) {
13541356
attrs = templateAttrs;
@@ -1359,8 +1361,11 @@ function $CompileProvider($provide) {
13591361

13601362
if (newIsolateScopeDirective) {
13611363
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
1364+
var $linkNode = jqLite(linkNode);
13621365

1363-
var parentScope = scope.$parent || scope;
1366+
isolateScope = scope.$new(true);
1367+
$linkNode.data('$isolateScope', isolateScope);
1368+
safeAddClass($linkNode, 'ng-isolate-scope');
13641369

13651370
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
13661371
var match = definition.match(LOCAL_REGEXP) || [],
@@ -1370,19 +1375,19 @@ function $CompileProvider($provide) {
13701375
lastValue,
13711376
parentGet, parentSet;
13721377

1373-
scope.$$isolateBindings[scopeName] = mode + attrName;
1378+
isolateScope.$$isolateBindings[scopeName] = mode + attrName;
13741379

13751380
switch (mode) {
13761381

13771382
case '@':
13781383
attrs.$observe(attrName, function(value) {
1379-
scope[scopeName] = value;
1384+
isolateScope[scopeName] = value;
13801385
});
1381-
attrs.$$observers[attrName].$$scope = parentScope;
1386+
attrs.$$observers[attrName].$$scope = scope;
13821387
if( attrs[attrName] ) {
13831388
// If the attribute has been provided then we trigger an interpolation to ensure
13841389
// the value is there for use in the link fn
1385-
scope[scopeName] = $interpolate(attrs[attrName])(parentScope);
1390+
isolateScope[scopeName] = $interpolate(attrs[attrName])(scope);
13861391
}
13871392
break;
13881393

@@ -1393,23 +1398,23 @@ function $CompileProvider($provide) {
13931398
parentGet = $parse(attrs[attrName]);
13941399
parentSet = parentGet.assign || function() {
13951400
// reset the change, or we will throw this exception on every $digest
1396-
lastValue = scope[scopeName] = parentGet(parentScope);
1401+
lastValue = isolateScope[scopeName] = parentGet(scope);
13971402
throw $compileMinErr('nonassign',
13981403
"Expression '{0}' used with directive '{1}' is non-assignable!",
13991404
attrs[attrName], newIsolateScopeDirective.name);
14001405
};
1401-
lastValue = scope[scopeName] = parentGet(parentScope);
1402-
scope.$watch(function parentValueWatch() {
1403-
var parentValue = parentGet(parentScope);
1406+
lastValue = isolateScope[scopeName] = parentGet(scope);
1407+
isolateScope.$watch(function parentValueWatch() {
1408+
var parentValue = parentGet(scope);
14041409

1405-
if (parentValue !== scope[scopeName]) {
1410+
if (parentValue !== isolateScope[scopeName]) {
14061411
// we are out of sync and need to copy
14071412
if (parentValue !== lastValue) {
14081413
// parent changed and it has precedence
1409-
lastValue = scope[scopeName] = parentValue;
1414+
lastValue = isolateScope[scopeName] = parentValue;
14101415
} else {
14111416
// if the parent can be assigned then do so
1412-
parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
1417+
parentSet(scope, parentValue = lastValue = isolateScope[scopeName]);
14131418
}
14141419
}
14151420
return parentValue;
@@ -1418,8 +1423,8 @@ function $CompileProvider($provide) {
14181423

14191424
case '&':
14201425
parentGet = $parse(attrs[attrName]);
1421-
scope[scopeName] = function(locals) {
1422-
return parentGet(parentScope, locals);
1426+
isolateScope[scopeName] = function(locals) {
1427+
return parentGet(scope, locals);
14231428
};
14241429
break;
14251430

@@ -1435,7 +1440,7 @@ function $CompileProvider($provide) {
14351440
if (controllerDirectives) {
14361441
forEach(controllerDirectives, function(directive) {
14371442
var locals = {
1438-
$scope: scope,
1443+
$scope: directive === newIsolateScopeDirective ? isolateScope : scope,
14391444
$element: $element,
14401445
$attrs: attrs,
14411446
$transclude: boundTranscludeFn
@@ -1467,21 +1472,22 @@ function $CompileProvider($provide) {
14671472
for(i = 0, ii = preLinkFns.length; i < ii; i++) {
14681473
try {
14691474
linkFn = preLinkFns[i];
1470-
linkFn(scope, $element, attrs,
1475+
linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
14711476
linkFn.require && getControllers(linkFn.require, $element));
14721477
} catch (e) {
14731478
$exceptionHandler(e, startingTag($element));
14741479
}
14751480
}
14761481

14771482
// RECURSION
1478-
childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
1483+
// TODO(vojta): only pass isolate if the isolate directive has template
1484+
childLinkFn && childLinkFn(isolateScope || scope, linkNode.childNodes, undefined, boundTranscludeFn);
14791485

14801486
// POSTLINKING
14811487
for(i = postLinkFns.length - 1; i >= 0; i--) {
14821488
try {
14831489
linkFn = postLinkFns[i];
1484-
linkFn(scope, $element, attrs,
1490+
linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
14851491
linkFn.require && getControllers(linkFn.require, $element));
14861492
} catch (e) {
14871493
$exceptionHandler(e, startingTag($element));

Diff for: test/ng/compileSpec.js

+83-4
Original file line numberDiff line numberDiff line change
@@ -1375,7 +1375,7 @@ describe('$compile', function() {
13751375
return function (scope, element) {
13761376
iscope = scope;
13771377
log(scope.$id);
1378-
expect(element.data('$scope')).toBe(scope);
1378+
expect(element.data('$isolateScope')).toBe(scope);
13791379
};
13801380
}
13811381
};
@@ -1416,7 +1416,7 @@ describe('$compile', function() {
14161416
return function (scope, element) {
14171417
iscope = scope;
14181418
log(scope.$id);
1419-
expect(element.data('$scope')).toBe(scope);
1419+
expect(element.data('$isolateScope')).toBe(scope);
14201420
};
14211421
}
14221422
};
@@ -1535,7 +1535,7 @@ describe('$compile', function() {
15351535
expect(function(){
15361536
$compile('<div class="iscope-a; scope-b"></div>');
15371537
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for new/isolated scope on: ' +
1538-
'<div class="iscope-a; scope-b ng-isolate-scope ng-scope">');
1538+
'<div class="iscope-a; scope-b">');
15391539
})
15401540
);
15411541

@@ -2085,7 +2085,7 @@ describe('$compile', function() {
20852085

20862086

20872087
describe('isolated locals', function() {
2088-
var componentScope;
2088+
var componentScope, regularScope;
20892089

20902090
beforeEach(module(function() {
20912091
directive('myComponent', function() {
@@ -2112,6 +2112,23 @@ describe('$compile', function() {
21122112
scope: { attr: 'xxx' }
21132113
};
21142114
});
2115+
directive('storeScope', function() {
2116+
return {
2117+
link: function(scope) {
2118+
regularScope = scope;
2119+
}
2120+
}
2121+
});
2122+
}));
2123+
2124+
it('should give other directives the parent scope', inject(function($rootScope) {
2125+
compile('<div><input type="text" my-component store-scope ng-model="value"></div>');
2126+
$rootScope.$apply(function() {
2127+
$rootScope.value = 'from-parent';
2128+
});
2129+
expect(element.find('input').val()).toBe('from-parent');
2130+
expect(componentScope).not.toBe(regularScope);
2131+
expect(componentScope.$parent).toBe(regularScope)
21152132
}));
21162133

21172134
describe('attribute', function() {
@@ -2376,6 +2393,68 @@ describe('$compile', function() {
23762393
});
23772394

23782395

2396+
it('should require controller of an isolate directive from a non-isolate directive on the ' +
2397+
'same element', function() {
2398+
var IsolateController = function() {};
2399+
var isolateDirControllerInNonIsolateDirective;
2400+
2401+
module(function() {
2402+
directive('isolate', function() {
2403+
return {
2404+
scope: {},
2405+
controller: IsolateController
2406+
};
2407+
});
2408+
directive('nonIsolate', function() {
2409+
return {
2410+
require: 'isolate',
2411+
link: function(_, __, ___, isolateDirController) {
2412+
isolateDirControllerInNonIsolateDirective = isolateDirController;
2413+
}
2414+
};
2415+
});
2416+
});
2417+
2418+
inject(function($compile, $rootScope) {
2419+
element = $compile('<div isolate non-isolate></div>')($rootScope);
2420+
2421+
expect(isolateDirControllerInNonIsolateDirective).toBeDefined();
2422+
expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true);
2423+
});
2424+
});
2425+
2426+
2427+
it('should require controller of a non-isolate directive from an isolate directive on the ' +
2428+
'same element', function() {
2429+
var NonIsolateController = function() {};
2430+
var nonIsolateDirControllerInIsolateDirective;
2431+
2432+
module(function() {
2433+
directive('isolate', function() {
2434+
return {
2435+
scope: {},
2436+
require: 'nonIsolate',
2437+
link: function(_, __, ___, nonIsolateDirController) {
2438+
nonIsolateDirControllerInIsolateDirective = nonIsolateDirController;
2439+
}
2440+
};
2441+
});
2442+
directive('nonIsolate', function() {
2443+
return {
2444+
controller: NonIsolateController
2445+
};
2446+
});
2447+
});
2448+
2449+
inject(function($compile, $rootScope) {
2450+
element = $compile('<div isolate non-isolate></div>')($rootScope);
2451+
2452+
expect(nonIsolateDirControllerInIsolateDirective).toBeDefined();
2453+
expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true);
2454+
});
2455+
});
2456+
2457+
23792458
it('should support controllerAs', function() {
23802459
module(function() {
23812460
directive('main', function() {

Diff for: xx

Whitespace-only changes.

0 commit comments

Comments
 (0)