Skip to content
This repository was archived by the owner on Jul 5, 2023. It is now read-only.

Commit 3f1cbbb

Browse files
committed
Merge pull request #14 from scalyr/czerwin-include-fix
Change slyEvaluateOnlyWhen to evaluate newly created children
2 parents 6a02f5c + efe55eb commit 3f1cbbb

File tree

4 files changed

+163
-25
lines changed

4 files changed

+163
-25
lines changed

src/js/directives/slyEvaluate.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
*
99
* slyEvaluateOnlyWhen: A directive that prevents updating / evaluating
1010
* all bindings for the current element and its children unless
11-
* the expression has changed values. It currently assumes the
11+
* the expression has changed values. If new children are added, they
12+
* are always evaluated at least once. It currently assumes the
1213
* expression evaluates to an object and detects changes only by
13-
* a change in object reference.
14+
* a change in object reference.
1415
*
1516
* slyAlwaysEvaluate: Can only be used in conjunction with the
1617
* slyEvaluateOnlyWhen directive. This directive will ensure that
@@ -75,7 +76,7 @@ defineScalyrAngularModule('slyEvaluate', ['gatedScope'])
7576
// be gated.
7677
return isNull(alwaysEvaluateString) ||
7778
!(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0));
78-
});
79+
}, true /* Evaluate any newly added watchers when they are added */);
7980
},
8081
};
8182
},

src/js/lib/gatedScope.js

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ defineScalyrAngularModule('gatedScope', [])
5252
result.$$parentGatingFunction = this.$$gatingFunction;
5353
result.$$shouldGateFunction = this.$$shouldGateFunction;
5454
result.$$gatedWatchers = [];
55+
result.$$cleanUpQueue = this.$$cleanUpQueue;
5556

5657
return result;
5758
};
@@ -83,6 +84,14 @@ defineScalyrAngularModule('gatedScope', [])
8384
if (watch.gatingFunction !== targetGatingFunction)
8485
continue;
8586

87+
// Since we are about to execute the watcher as part of a digestGated
88+
// call, we can remove it from the normal digest queue if it was placed
89+
// there because the watcher was added after the gate function's first
90+
// evaluation.
91+
if (watch && !isNull(watch.cleanUp)) {
92+
watch.cleanUp();
93+
watch.cleanUp = null;
94+
}
8695
// Most common watches are on primitives, in which case we can short
8796
// circuit it with === operator, only when === fails do we use .equals
8897
if (watch && (value = watch.get(current)) !== (last = watch.last) &&
@@ -117,6 +126,8 @@ defineScalyrAngularModule('gatedScope', [])
117126
}
118127
} while ((current = next));
119128

129+
// Mark that this gating function has digested all children.
130+
targetGatingFunction.hasDigested = true;
120131
return dirty;
121132
};
122133

@@ -141,11 +152,38 @@ defineScalyrAngularModule('gatedScope', [])
141152
var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
142153
this.$$watchers = tmp;
143154
this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction;
155+
this.$$gatedWatchers[0].cleanUp = null;
144156

145157
// We know that the last field of the watcher object will be set to initWatchVal, so we
146158
// grab it here.
147159
initWatchVal = this.$$gatedWatchers[0].last;
160+
var watch = this.$$gatedWatchers[0];
161+
162+
// We should make sure the watch expression gets evaluated fully on at least one
163+
// digest cycle even if the gate function is now closed if requested by the gating function's
164+
// value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute
165+
// the watcher we just added and remove itself after the digest cycle completes.
166+
if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) {
167+
var self = this;
168+
watch.cleanUp = scopePrototype.$watch.call(self, function() {
169+
if (!isNull(watch.cleanUp)) {
170+
self.$$cleanUpQueue.unshift(watch.cleanUp);
171+
watch.cleanUp = null;
172+
}
173+
var value;
174+
var last = initWatchVal;
148175

176+
if (watch && (value = watch.get(self)) !== (last = watch.last) &&
177+
!(watch.eq
178+
? areEqual(value, last)
179+
: (typeof value == 'number' && typeof last == 'number'
180+
&& isNaN(value) && isNaN(last)))) {
181+
watch.last = watch.eq ? copy(value) : value;
182+
watch.fn(value, ((last === initWatchVal) ? value : last), self);
183+
}
184+
return watch.last;
185+
});
186+
}
149187
return result;
150188
} else {
151189
return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
@@ -168,8 +206,8 @@ defineScalyrAngularModule('gatedScope', [])
168206
// functions and should be evaluated at all. However, if a caller is invoking
169207
// $digest on a particular scope, we assume the caller is doing that because it
170208
// knows the watchers should be evaluated.
209+
var dirty = false;
171210
if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) {
172-
var dirty = false;
173211
var ttl = 5;
174212
do {
175213
dirty = this.$digestGated(this.$$parentGatingFunction);
@@ -181,7 +219,19 @@ defineScalyrAngularModule('gatedScope', [])
181219
}
182220
} while (dirty);
183221
}
184-
return scopePrototype.$digest.call(this) || dirty;
222+
223+
dirty = scopePrototype.$digest.call(this) || dirty;
224+
225+
var cleanUpQueue = this.$$cleanUpQueue;
226+
227+
while (cleanUpQueue.length)
228+
try {
229+
cleanUpQueue.shift()();
230+
} catch (e) {
231+
$exceptionHandler(e);
232+
}
233+
234+
return dirty;
185235
}
186236

187237
/**
@@ -198,8 +248,13 @@ defineScalyrAngularModule('gatedScope', [])
198248
* a new watcher will be gated using gatingFunction. It is evaluated with the
199249
* arguments to $watch and should return true if the watcher created by those
200250
* arguments should be gated
251+
* @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added
252+
* after the gating function has returned true on a previous digest cycle, the
253+
* the new watcher will be evaluated on the next digest cycle even if the
254+
* gating function is currently return false.
201255
*/
202-
methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction) {
256+
methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction,
257+
shouldEvalNewWatchers) {
203258
var changeCount = 0;
204259
var self = this;
205260

@@ -215,30 +270,36 @@ defineScalyrAngularModule('gatedScope', [])
215270
// true (which we can tell if the watcher we register here is evaluated), then
216271
// we always evaluate our watcher until our gating function returns true.
217272
var hasNestedGates = !isNull(this.$$gatingFunction);
218-
var promotedWatcher = null;
219-
220-
this.$watch(function() {
221-
if (gatingFunction()) {
222-
if (self.$digestGated(gatingFunction))
223-
++changeCount;
224-
} else if (hasNestedGates && isNull(promotedWatcher)) {
225-
promotedWatcher = scopePrototype.$watch.call(self, function() {
226-
if (gatingFunction()) {
227-
promotedWatcher();
228-
promotedWatcher = null;
229-
if (self.$digestGated(gatingFunction))
230-
++changeCount;
231-
}
232-
return changeCount;
233-
});
234-
}
235-
return changeCount;
236-
});
273+
274+
(function() {
275+
var promotedWatcher = null;
276+
277+
self.$watch(function() {
278+
if (gatingFunction()) {
279+
if (self.$digestGated(gatingFunction))
280+
++changeCount;
281+
} else if (hasNestedGates && isNull(promotedWatcher)) {
282+
promotedWatcher = scopePrototype.$watch.call(self, function() {
283+
if (gatingFunction()) {
284+
promotedWatcher();
285+
promotedWatcher = null;
286+
if (self.$digestGated(gatingFunction))
287+
++changeCount;
288+
}
289+
return changeCount;
290+
});
291+
}
292+
return changeCount;
293+
});
294+
})();
237295

238296

239297
if (isUndefined(shouldGateFunction))
240298
shouldGateFunction = null;
299+
if (isUndefined(shouldEvalNewWatchers))
300+
shouldEvalNewWatchers = false;
241301
this.$$gatingFunction = gatingFunction;
302+
this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers;
242303
this.$$shouldGateFunction = shouldGateFunction;
243304
};
244305

@@ -254,6 +315,7 @@ defineScalyrAngularModule('gatedScope', [])
254315
$rootScope.$$parentGatingFunction = null;
255316
$rootScope.$$shouldGateFunction = null;
256317
$rootScope.$$gatedWatchers = [];
318+
$rootScope.$$cleanUpQueue = [];
257319

258320
return $rootScope;
259321
}]);

src/tests/directives/slyEvaluateTest.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ describe('slyEvaluate.slyEvaluateOnlyWhen', function() {
6969

7070
expect(span.eq(0).text()).toEqual('12');
7171
});
72+
73+
it('should evaluate a new child no matter what',
74+
inject(function($rootScope, $compile) {
75+
scope = $rootScope;
76+
scope.dataObject = {
77+
value: 5,
78+
};
79+
scope.x = 12;
80+
81+
page = angular.element('<div sly-evaluate-only-when="dataObject"></div>');
82+
83+
$compile(page)(scope);
84+
scope.$digest();
85+
86+
// We simulate adding a new child to the div by just creating a new element
87+
// and compiling it in. This is a bit hacky and hopefully won't break.
88+
divScope = page.scope();
89+
span = angular.element('<span>{{x}}</span>');
90+
$compile(span)(divScope);
91+
92+
scope.$digest();
93+
94+
expect(span.eq(0).text()).toEqual('12');
95+
}));
7296
});
7397

7498
describe('slyEvaluate.slyAlwaysEvaluate', function() {

src/tests/lib/gatedScopeTest.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,55 @@ describe('GatedScope', function() {
312312
// When both gates are down, the watcher should not be evaluated.
313313
$rootScope.$digest();
314314
});
315+
316+
it('should evaluate new watchers when gating function has shouldEvalNewWatchers = true', function() {
317+
var gateClosed = false;
318+
319+
var child = $rootScope.$new();
320+
child.$addWatcherGate(function() {
321+
return !gateClosed;
322+
}, null, true);
323+
324+
$rootScope.$digest();
325+
326+
gateClosed = true;
327+
328+
var counter = 0;
329+
var watchedVal = 1;
330+
function watcher() {
331+
++counter;
332+
return watchedVal;
333+
}
334+
335+
child.$watch(watcher);
336+
$rootScope.$digest();
337+
338+
// Should have been evaluated twice, one for the first dirty cycle, and then for
339+
// cycle it was not dirty on.
340+
expect(counter).toEqual(2);
341+
});
342+
343+
it('should not evaluate new watchers when gating function has shouldEvalNewWatchers = false', function() {
344+
var gateClosed = false;
345+
346+
var child = $rootScope.$new();
347+
child.$addWatcherGate(function() {
348+
return !gateClosed;
349+
}, null, false);
350+
351+
$rootScope.$digest();
352+
353+
gateClosed = true;
354+
var counter = 0;
355+
var watchedVal = 1;
356+
function watcher() {
357+
++counter;
358+
return watchedVal;
359+
}
360+
361+
child.$watch(watcher);
362+
$rootScope.$digest();
363+
364+
expect(counter).toEqual(0);
365+
});
315366
});

0 commit comments

Comments
 (0)