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

Commit

Permalink
fix(ng:include): prevent race conditions by ignoring stale http callb…
Browse files Browse the repository at this point in the history
…acks

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.
  • Loading branch information
IgorMinar committed Nov 30, 2011
1 parent baa7af0 commit 1d14760
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 31 deletions.
49 changes: 24 additions & 25 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,42 +94,38 @@ angularWidget('ng:include', function(element){
function($http, $templateCache, $autoScroll, element) {
var scope = this,
changeCounter = 0,
releaseScopes = [],
childScope,
oldScope;
childScope;

function incrementChange() { changeCounter++;}
this.$watch(srcExp, incrementChange);
this.$watch(function(scope){
var newScope = scope.$eval(scopeExp);
if (newScope !== oldScope) {
oldScope = newScope;
incrementChange();
}
});
this.$watch(function() {return changeCounter;}, function(scope) {
this.$watch(function() {
var includeScope = scope.$eval(scopeExp);
if (includeScope) return includeScope.$id;
}, incrementChange);
this.$watch(function() {return changeCounter;}, function(scope, newChangeCounter) {
var src = scope.$eval(srcExp),
useScope = scope.$eval(scopeExp);

function clearContent() {
childScope = null;
element.html('');
// if this callback is still desired
if (newChangeCounter === changeCounter) {
if (childScope) childScope.$destroy();
childScope = null;
element.html('');
}
}

while(releaseScopes.length) {
releaseScopes.pop().$destroy();
}
if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) {
element.html(response);
if (useScope) {
childScope = useScope;
} else {
releaseScopes.push(childScope = scope.$new());
// if this callback is still desired
if (newChangeCounter === changeCounter) {
element.html(response);
if (childScope) childScope.$destroy();
childScope = useScope ? useScope : scope.$new();
compiler.compile(element)(childScope);
$autoScroll();
scope.$eval(onloadExp);
}
compiler.compile(element)(childScope);
$autoScroll();
scope.$eval(onloadExp);
}).error(clearContent);
} else {
clearContent();
Expand Down Expand Up @@ -574,7 +570,10 @@ angularWidget('ng:view', function(element) {
var template = $route.current && $route.current.template;

function clearContent() {
element.html('');
// ignore callback if another route change occured since
if (newChangeCounter == changeCounter) {
element.html('');
}
}

if (template) {
Expand Down
36 changes: 30 additions & 6 deletions test/widgetsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('widget', function() {
it('should include on external file', inject(putIntoCache('myUrl', '{{name}}'),
function($rootScope, $compile, $browser) {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var element = $compile(element)($rootScope);
element = $compile(element)($rootScope);
$rootScope.childScope = $rootScope.$new();
$rootScope.childScope.name = 'misko';
$rootScope.url = 'myUrl';
Expand All @@ -81,7 +81,7 @@ describe('widget', function() {
putIntoCache('myUrl', '{{name}}'),
function($rootScope, $compile, $browser) {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var element = $compile(element)($rootScope);
element = $compile(element)($rootScope);
$rootScope.childScope = $rootScope.$new();
$rootScope.childScope.name = 'igor';
$rootScope.url = 'myUrl';
Expand All @@ -100,7 +100,7 @@ describe('widget', function() {
it('should allow this for scope', inject(putIntoCache('myUrl', '{{"abc"}}'),
function($rootScope, $compile, $browser) {
var element = jqLite('<ng:include src="url" scope="this"></ng:include>');
var element = $compile(element)($rootScope);
element = $compile(element)($rootScope);
$rootScope.url = 'myUrl';
$rootScope.$digest();
$browser.defer.flush();
Expand All @@ -119,7 +119,7 @@ describe('widget', function() {
putIntoCache('myUrl', 'my partial'),
function($rootScope, $compile, $browser) {
var element = jqLite('<ng:include src="url" onload="loaded = true"></ng:include>');
var element = $compile(element)($rootScope);
element = $compile(element)($rootScope);

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

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

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

Expand Down Expand Up @@ -199,6 +199,30 @@ describe('widget', function() {
$browser.defer.flush();
expect(element.text()).toBe('my partial');
}));

it('should discard pending xhr callbacks if a new template is requested before the current ' +
'finished loading', inject(function($rootScope, $compile, $httpBackend) {
var element = jqLite("<ng:include src='templateUrl'></ng:include>"),
log = [];

$rootScope.templateUrl = 'myUrl1';
$rootScope.logger = function(msg) {
log.push(msg);
}
$compile(element)($rootScope);
expect(log.join('; ')).toEqual('');

$httpBackend.expect('GET', 'myUrl1').respond('<div>{{logger("url1")}}</div>');
$rootScope.$digest();
expect(log.join('; ')).toEqual('');
$rootScope.templateUrl = 'myUrl2';
$httpBackend.expect('GET', 'myUrl2').respond('<div>{{logger("url2")}}</div>');
$rootScope.$digest();
$httpBackend.flush(); // now that we have two requests pending, flush!

expect(log.join('; ')).toEqual('url2; url2'); // it's here twice because we go through at
// least two digest cycles
}));
}));


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

expect($rootScope.$element.text()).toEqual('2');
}));
Expand Down

0 comments on commit 1d14760

Please sign in to comment.