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

Commit 1d14760

Browse files
committed
fix(ng:include): prevent race conditions by ignoring stale http callbacks
This fix is similar to what I've done in ng:view, if a new template has been requested before the callback for the previous template returned, ignore it. Otherwise weird race conditions happen and users might end up getting the content for the previous include rendered instead of the most recent one.
1 parent baa7af0 commit 1d14760

File tree

2 files changed

+54
-31
lines changed

2 files changed

+54
-31
lines changed

src/widgets.js

+24-25
Original file line numberDiff line numberDiff line change
@@ -94,42 +94,38 @@ angularWidget('ng:include', function(element){
9494
function($http, $templateCache, $autoScroll, element) {
9595
var scope = this,
9696
changeCounter = 0,
97-
releaseScopes = [],
98-
childScope,
99-
oldScope;
97+
childScope;
10098

10199
function incrementChange() { changeCounter++;}
102100
this.$watch(srcExp, incrementChange);
103-
this.$watch(function(scope){
104-
var newScope = scope.$eval(scopeExp);
105-
if (newScope !== oldScope) {
106-
oldScope = newScope;
107-
incrementChange();
108-
}
109-
});
110-
this.$watch(function() {return changeCounter;}, function(scope) {
101+
this.$watch(function() {
102+
var includeScope = scope.$eval(scopeExp);
103+
if (includeScope) return includeScope.$id;
104+
}, incrementChange);
105+
this.$watch(function() {return changeCounter;}, function(scope, newChangeCounter) {
111106
var src = scope.$eval(srcExp),
112107
useScope = scope.$eval(scopeExp);
113108

114109
function clearContent() {
115-
childScope = null;
116-
element.html('');
110+
// if this callback is still desired
111+
if (newChangeCounter === changeCounter) {
112+
if (childScope) childScope.$destroy();
113+
childScope = null;
114+
element.html('');
115+
}
117116
}
118117

119-
while(releaseScopes.length) {
120-
releaseScopes.pop().$destroy();
121-
}
122118
if (src) {
123119
$http.get(src, {cache: $templateCache}).success(function(response) {
124-
element.html(response);
125-
if (useScope) {
126-
childScope = useScope;
127-
} else {
128-
releaseScopes.push(childScope = scope.$new());
120+
// if this callback is still desired
121+
if (newChangeCounter === changeCounter) {
122+
element.html(response);
123+
if (childScope) childScope.$destroy();
124+
childScope = useScope ? useScope : scope.$new();
125+
compiler.compile(element)(childScope);
126+
$autoScroll();
127+
scope.$eval(onloadExp);
129128
}
130-
compiler.compile(element)(childScope);
131-
$autoScroll();
132-
scope.$eval(onloadExp);
133129
}).error(clearContent);
134130
} else {
135131
clearContent();
@@ -574,7 +570,10 @@ angularWidget('ng:view', function(element) {
574570
var template = $route.current && $route.current.template;
575571

576572
function clearContent() {
577-
element.html('');
573+
// ignore callback if another route change occured since
574+
if (newChangeCounter == changeCounter) {
575+
element.html('');
576+
}
578577
}
579578

580579
if (template) {

test/widgetsSpec.js

+30-6
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('widget', function() {
6767
it('should include on external file', inject(putIntoCache('myUrl', '{{name}}'),
6868
function($rootScope, $compile, $browser) {
6969
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
70-
var element = $compile(element)($rootScope);
70+
element = $compile(element)($rootScope);
7171
$rootScope.childScope = $rootScope.$new();
7272
$rootScope.childScope.name = 'misko';
7373
$rootScope.url = 'myUrl';
@@ -81,7 +81,7 @@ describe('widget', function() {
8181
putIntoCache('myUrl', '{{name}}'),
8282
function($rootScope, $compile, $browser) {
8383
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
84-
var element = $compile(element)($rootScope);
84+
element = $compile(element)($rootScope);
8585
$rootScope.childScope = $rootScope.$new();
8686
$rootScope.childScope.name = 'igor';
8787
$rootScope.url = 'myUrl';
@@ -100,7 +100,7 @@ describe('widget', function() {
100100
it('should allow this for scope', inject(putIntoCache('myUrl', '{{"abc"}}'),
101101
function($rootScope, $compile, $browser) {
102102
var element = jqLite('<ng:include src="url" scope="this"></ng:include>');
103-
var element = $compile(element)($rootScope);
103+
element = $compile(element)($rootScope);
104104
$rootScope.url = 'myUrl';
105105
$rootScope.$digest();
106106
$browser.defer.flush();
@@ -119,7 +119,7 @@ describe('widget', function() {
119119
putIntoCache('myUrl', 'my partial'),
120120
function($rootScope, $compile, $browser) {
121121
var element = jqLite('<ng:include src="url" onload="loaded = true"></ng:include>');
122-
var element = $compile(element)($rootScope);
122+
element = $compile(element)($rootScope);
123123

124124
expect($rootScope.loaded).not.toBeDefined();
125125

@@ -135,7 +135,7 @@ describe('widget', function() {
135135
it('should destroy old scope', inject(putIntoCache('myUrl', 'my partial'),
136136
function($rootScope, $compile, $browser) {
137137
var element = jqLite('<ng:include src="url"></ng:include>');
138-
var element = $compile(element)($rootScope);
138+
element = $compile(element)($rootScope);
139139

140140
expect($rootScope.$$childHead).toBeFalsy();
141141

@@ -199,6 +199,30 @@ describe('widget', function() {
199199
$browser.defer.flush();
200200
expect(element.text()).toBe('my partial');
201201
}));
202+
203+
it('should discard pending xhr callbacks if a new template is requested before the current ' +
204+
'finished loading', inject(function($rootScope, $compile, $httpBackend) {
205+
var element = jqLite("<ng:include src='templateUrl'></ng:include>"),
206+
log = [];
207+
208+
$rootScope.templateUrl = 'myUrl1';
209+
$rootScope.logger = function(msg) {
210+
log.push(msg);
211+
}
212+
$compile(element)($rootScope);
213+
expect(log.join('; ')).toEqual('');
214+
215+
$httpBackend.expect('GET', 'myUrl1').respond('<div>{{logger("url1")}}</div>');
216+
$rootScope.$digest();
217+
expect(log.join('; ')).toEqual('');
218+
$rootScope.templateUrl = 'myUrl2';
219+
$httpBackend.expect('GET', 'myUrl2').respond('<div>{{logger("url2")}}</div>');
220+
$rootScope.$digest();
221+
$httpBackend.flush(); // now that we have two requests pending, flush!
222+
223+
expect(log.join('; ')).toEqual('url2; url2'); // it's here twice because we go through at
224+
// least two digest cycles
225+
}));
202226
}));
203227

204228

@@ -645,7 +669,7 @@ describe('widget', function() {
645669
$location.path('/bar');
646670
$httpBackend.expect('GET', 'myUrl2').respond('<div>{{1+1}}</div>');
647671
$rootScope.$digest();
648-
$httpBackend.flush(); // now that we have to requests pending, flush!
672+
$httpBackend.flush(); // now that we have two requests pending, flush!
649673

650674
expect($rootScope.$element.text()).toEqual('2');
651675
}));

0 commit comments

Comments
 (0)