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

Commit 0b98cfa

Browse files
erwinmombaySomeKittens
authored andcommitted
fix(scopes): don't break one time ngbind expressions
1 parent 26b5da7 commit 0b98cfa

File tree

3 files changed

+66
-11
lines changed

3 files changed

+66
-11
lines changed

dist/hint.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,14 +1410,36 @@ function decorateRootScope($delegate, $parse) {
14101410

14111411
var _watch = scopePrototype.$watch;
14121412
var _digestEvents = [];
1413+
var skipNextPerfWatchers = false;
14131414
scopePrototype.$watch = function (watchExpression, reactionFunction) {
1415+
// if `skipNextPerfWatchers` is true, this means the previous run of the
1416+
// `$watch` decorator was a one time binding expression and this invocation
1417+
// of the $watch function has the `oneTimeInterceptedExpression` (internal angular function)
1418+
// as the `watchExpression` parameter. If we decorate it with the performance
1419+
// timers function this will cause us to invoke `oneTimeInterceptedExpression`
1420+
// on subsequent digest loops and will update the one time bindings
1421+
// if anything mutated the property.
1422+
if (skipNextPerfWatchers) {
1423+
skipNextPerfWatchers = false;
1424+
return _watch.apply(this, arguments);
1425+
}
1426+
14141427
if (typeof watchExpression === 'string' &&
14151428
isOneTimeBindExp(watchExpression)) {
1429+
skipNextPerfWatchers = true;
14161430
return _watch.apply(this, arguments);
14171431
}
14181432
var watchStr = humanReadableWatchExpression(watchExpression);
14191433
var scopeId = this.$id;
1434+
var expressions = null;
14201435
if (typeof watchExpression === 'function') {
1436+
expressions = watchExpression.expressions;
1437+
if (Object.prototype.toString.call(expressions) === '[object Array]' &&
1438+
expressions.some(isOneTimeBindExp)) {
1439+
skipNextPerfWatchers = true;
1440+
return _watch.apply(this, arguments);
1441+
}
1442+
14211443
arguments[0] = function () {
14221444
var start = perf.now();
14231445
var ret = watchExpression.apply(this, arguments);

src/modules/scopes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,36 @@ function decorateRootScope($delegate, $parse) {
7777

7878
var _watch = scopePrototype.$watch;
7979
var _digestEvents = [];
80+
var skipNextPerfWatchers = false;
8081
scopePrototype.$watch = function (watchExpression, reactionFunction) {
82+
// if `skipNextPerfWatchers` is true, this means the previous run of the
83+
// `$watch` decorator was a one time binding expression and this invocation
84+
// of the $watch function has the `oneTimeInterceptedExpression` (internal angular function)
85+
// as the `watchExpression` parameter. If we decorate it with the performance
86+
// timers function this will cause us to invoke `oneTimeInterceptedExpression`
87+
// on subsequent digest loops and will update the one time bindings
88+
// if anything mutated the property.
89+
if (skipNextPerfWatchers) {
90+
skipNextPerfWatchers = false;
91+
return _watch.apply(this, arguments);
92+
}
93+
8194
if (typeof watchExpression === 'string' &&
8295
isOneTimeBindExp(watchExpression)) {
96+
skipNextPerfWatchers = true;
8397
return _watch.apply(this, arguments);
8498
}
8599
var watchStr = humanReadableWatchExpression(watchExpression);
86100
var scopeId = this.$id;
101+
var expressions = null;
87102
if (typeof watchExpression === 'function') {
103+
expressions = watchExpression.expressions;
104+
if (Object.prototype.toString.call(expressions) === '[object Array]' &&
105+
expressions.some(isOneTimeBindExp)) {
106+
skipNextPerfWatchers = true;
107+
return _watch.apply(this, arguments);
108+
}
109+
88110
arguments[0] = function () {
89111
var start = perf.now();
90112
var ret = watchExpression.apply(this, arguments);

test/scopes.spec.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
var hint = angular.hint;
44
describe('ngHintScopes', function() {
55

6-
var $rootScope;
6+
var $rootScope, $compile;
77

88
beforeEach(module('ngHintScopes'));
9-
beforeEach(inject(function(_$rootScope_) {
9+
beforeEach(inject(function(_$rootScope_, _$compile_) {
1010
$rootScope = _$rootScope_;
11+
$compile = _$compile_;
1112
}));
1213

1314
describe('scope.$watch', function() {
@@ -52,24 +53,34 @@ describe('ngHintScopes', function() {
5253
});
5354

5455
if (angular.version.minor >= 3) {
55-
it('should not run perf timers for one time bind expressions', function() {
56+
it('should not run perf timers for one time bind expressions passed to watch', function() {
5657
var calls = hint.emit.calls;
5758
scope.$watch('::a.b', function() {});
5859
expect(calls.count()).toBe(0);
5960

6061
scope.$apply();
6162
var evt = calls.mostRecent().args[1].events[0];
62-
// this is the watch angular registers and deregisters on $$postDigest
63-
// for one time watch expressions
6463
expect(calls.count()).toBe(1);
65-
expect(evt.eventType).toBe('scope:watch');
66-
expect(evt.watch).toBe('oneTimeWatch');
67-
68-
scope.$apply()
69-
var evt = calls.mostRecent().args[1].events[0];
70-
expect(calls.count()).toBe(2);
7164
expect(evt).toBeUndefined();
7265
});
66+
67+
it('should not run perf timers for one time template bindings', function() {
68+
var elt = angular.element(
69+
'<div>' +
70+
'<span>{{::a}}</span>' +
71+
'<button ng-click="a = \'bar\'">Set</button>' +
72+
'</div>'
73+
);
74+
scope.a = 'foo';
75+
var view = $compile(elt)(scope);
76+
scope.$apply();
77+
var $binding = view.find('span');
78+
var $button = view.find('button');
79+
80+
$button.triggerHandler('click');
81+
scope.$apply();
82+
expect($binding.text()).toBe('foo');
83+
});
7384
}
7485

7586
// the event's time property is set to null before comparison with expectedEvent, so callers

0 commit comments

Comments
 (0)