Skip to content

Commit 8a1eb16

Browse files
caitppetebacondarwin
authored andcommitted
fix($compile): do not write @-bound properties if attribute is not present
Shadows only when attributes are non-optional and not own properties, only stores the observed '@' values when they are indeed strings. Partial revert of 6339d30d1379689da5eec9647a953f64821f8b0 Closes angular#12151 Closes angular#12144
1 parent ed27e0e commit 8a1eb16

File tree

2 files changed

+94
-27
lines changed

2 files changed

+94
-27
lines changed

src/ng/compile.js

+12-15
Original file line numberDiff line numberDiff line change
@@ -2566,36 +2566,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
25662566
lastValue,
25672567
parentGet, parentSet, compare;
25682568

2569-
if (!hasOwnProperty.call(attrs, attrName)) {
2570-
// In the case of user defined a binding with the same name as a method in Object.prototype but didn't set
2571-
// the corresponding attribute. We need to make sure subsequent code won't access to the prototype function
2572-
attrs[attrName] = undefined;
2573-
}
2574-
25752569
switch (mode) {
25762570

25772571
case '@':
2578-
if (!attrs[attrName] && !optional) {
2579-
destination[scopeName] = undefined;
2572+
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
2573+
destination[scopeName] = attrs[attrName] = void 0;
25802574
}
2581-
25822575
attrs.$observe(attrName, function(value) {
2583-
destination[scopeName] = value;
2576+
if (isString(value)) {
2577+
destination[scopeName] = value;
2578+
}
25842579
});
25852580
attrs.$$observers[attrName].$$scope = scope;
2586-
if (attrs[attrName]) {
2581+
if (isString(attrs[attrName])) {
25872582
// If the attribute has been provided then we trigger an interpolation to ensure
25882583
// the value is there for use in the link fn
25892584
destination[scopeName] = $interpolate(attrs[attrName])(scope);
25902585
}
25912586
break;
25922587

25932588
case '=':
2594-
if (optional && !attrs[attrName]) {
2595-
return;
2589+
if (!hasOwnProperty.call(attrs, attrName)) {
2590+
if (optional) break;
2591+
attrs[attrName] = void 0;
25962592
}
2597-
parentGet = $parse(attrs[attrName]);
25982593

2594+
parentGet = $parse(attrs[attrName]);
25992595
if (parentGet.literal) {
26002596
compare = equals;
26012597
} else {
@@ -2634,7 +2630,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
26342630
break;
26352631

26362632
case '&':
2637-
parentGet = $parse(attrs[attrName]);
2633+
// Don't assign Object.prototype method to scope
2634+
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
26382635

26392636
// Don't assign noop to destination if expression is not valid
26402637
if (parentGet === noop && optional) break;

test/ng/compileSpec.js

+82-12
Original file line numberDiff line numberDiff line change
@@ -2521,10 +2521,17 @@ describe('$compile', function() {
25212521
};
25222522

25232523
expect(func).not.toThrow();
2524-
expect(element.find('span').scope()).toBe(element.isolateScope());
2525-
expect(element.isolateScope()).not.toBe($rootScope);
2526-
expect(element.isolateScope()['constructor']).toBe($rootScope.constructor);
2527-
expect(element.isolateScope()['valueOf']).toBeUndefined();
2524+
var scope = element.isolateScope();
2525+
expect(element.find('span').scope()).toBe(scope);
2526+
expect(scope).not.toBe($rootScope);
2527+
2528+
// Not shadowed because optional
2529+
expect(scope.constructor).toBe($rootScope.constructor);
2530+
expect(scope.hasOwnProperty('constructor')).toBe(false);
2531+
2532+
// Shadowed with undefined because not optional
2533+
expect(scope.valueOf).toBeUndefined();
2534+
expect(scope.hasOwnProperty('valueOf')).toBe(true);
25282535
})
25292536
);
25302537

@@ -2539,10 +2546,13 @@ describe('$compile', function() {
25392546
};
25402547

25412548
expect(func).not.toThrow();
2542-
expect(element.find('span').scope()).toBe(element.isolateScope());
2543-
expect(element.isolateScope()).not.toBe($rootScope);
2544-
expect(element.isolateScope()['constructor']).toBe('constructor');
2545-
expect(element.isolateScope()['valueOf']).toBe('valueOf');
2549+
var scope = element.isolateScope();
2550+
expect(element.find('span').scope()).toBe(scope);
2551+
expect(scope).not.toBe($rootScope);
2552+
expect(scope.constructor).toBe('constructor');
2553+
expect(scope.hasOwnProperty('constructor')).toBe(true);
2554+
expect(scope.valueOf).toBe('valueOf');
2555+
expect(scope.hasOwnProperty('valueOf')).toBe(true);
25462556
})
25472557
);
25482558

@@ -2553,10 +2563,17 @@ describe('$compile', function() {
25532563
};
25542564

25552565
expect(func).not.toThrow();
2556-
expect(element.find('span').scope()).toBe(element.isolateScope());
2557-
expect(element.isolateScope()).not.toBe($rootScope);
2558-
expect(element.isolateScope()['constructor']).toBe($rootScope.constructor);
2559-
expect(element.isolateScope()['valueOf']).toBeUndefined();
2566+
var scope = element.isolateScope();
2567+
expect(element.find('span').scope()).toBe(scope);
2568+
expect(scope).not.toBe($rootScope);
2569+
2570+
// Does not shadow value because optional
2571+
expect(scope.constructor).toBe($rootScope.constructor);
2572+
expect(scope.hasOwnProperty('constructor')).toBe(false);
2573+
2574+
// Shadows value because not optional
2575+
expect(scope.valueOf).toBeUndefined();
2576+
expect(scope.hasOwnProperty('valueOf')).toBe(true);
25602577
})
25612578
);
25622579

@@ -3554,6 +3571,31 @@ describe('$compile', function() {
35543571
}));
35553572

35563573

3574+
it('should not overwrite @-bound property each digest when not present', function() {
3575+
module(function($compileProvider) {
3576+
$compileProvider.directive('testDir', valueFn({
3577+
scope: {prop: '@'},
3578+
controller: function($scope) {
3579+
$scope.prop = $scope.prop || 'default';
3580+
this.getProp = function() {
3581+
return $scope.prop;
3582+
};
3583+
},
3584+
controllerAs: 'ctrl',
3585+
template: '<p></p>'
3586+
}));
3587+
});
3588+
inject(function($compile, $rootScope) {
3589+
element = $compile('<div test-dir></div>')($rootScope);
3590+
var scope = element.isolateScope();
3591+
expect(scope.ctrl.getProp()).toBe('default');
3592+
3593+
$rootScope.$digest();
3594+
expect(scope.ctrl.getProp()).toBe('default');
3595+
});
3596+
});
3597+
3598+
35573599
describe('bind-once', function() {
35583600

35593601
function countWatches(scope) {
@@ -4415,6 +4457,34 @@ describe('$compile', function() {
44154457
childScope.theCtrl.test();
44164458
});
44174459
});
4460+
4461+
it('should not overwrite @-bound property each digest when not present', function() {
4462+
module(function($compileProvider) {
4463+
$compileProvider.directive('testDir', valueFn({
4464+
scope: {},
4465+
bindToController: {
4466+
prop: '@'
4467+
},
4468+
controller: function() {
4469+
var self = this;
4470+
this.prop = this.prop || 'default';
4471+
this.getProp = function() {
4472+
return self.prop;
4473+
};
4474+
},
4475+
controllerAs: 'ctrl',
4476+
template: '<p></p>'
4477+
}));
4478+
});
4479+
inject(function($compile, $rootScope) {
4480+
element = $compile('<div test-dir></div>')($rootScope);
4481+
var scope = element.isolateScope();
4482+
expect(scope.ctrl.getProp()).toBe('default');
4483+
4484+
$rootScope.$digest();
4485+
expect(scope.ctrl.getProp()).toBe('default');
4486+
});
4487+
});
44184488
});
44194489

44204490

0 commit comments

Comments
 (0)