Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2404 Modal open calls should complete in order #1

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 59 additions & 47 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,11 @@ angular.module('ui.bootstrap.modal', [])
return promisesArr;
}

var promiseChain = null;
$modal.getPromiseChain = function() {
return promiseChain;
};

$modal.open = function (modalOptions) {

var modalResultDeferred = $q.defer();
Expand Down Expand Up @@ -637,63 +642,70 @@ angular.module('ui.bootstrap.modal', [])
var templateAndResolvePromise =
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));

// Wait for the resolution of the existing promise chain.
// Then switch to our own combined promise dependency (regardless of how the previous modal fared).
// Then add to $modalStack and resolve opened.
// Finally clean up the chain variable if no subsequent modal has overwritten it.
var samePromise;
samePromise = promiseChain = $q.all([promiseChain])
.then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; })
.then(function resolveSuccess(tplAndVars) {

var modalScope = (modalOptions.scope || $rootScope).$new();
modalScope.$close = modalInstance.close;
modalScope.$dismiss = modalInstance.dismiss;

modalScope.$on('$destroy', function() {
if (!modalScope.$$uibDestructionScheduled) {
modalScope.$dismiss('$uibUnscheduledDestruction');
}
});

templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {

var modalScope = (modalOptions.scope || $rootScope).$new();
modalScope.$close = modalInstance.close;
modalScope.$dismiss = modalInstance.dismiss;

modalScope.$on('$destroy', function() {
if (!modalScope.$$uibDestructionScheduled) {
modalScope.$dismiss('$uibUnscheduledDestruction');
}
});
var ctrlInstance, ctrlLocals = {};
var resolveIter = 1;

var ctrlInstance, ctrlLocals = {};
var resolveIter = 1;
//controllers
if (modalOptions.controller) {
ctrlLocals.$scope = modalScope;
ctrlLocals.$modalInstance = modalInstance;
angular.forEach(modalOptions.resolve, function(value, key) {
ctrlLocals[key] = tplAndVars[resolveIter++];
});

//controllers
if (modalOptions.controller) {
ctrlLocals.$scope = modalScope;
ctrlLocals.$modalInstance = modalInstance;
angular.forEach(modalOptions.resolve, function(value, key) {
ctrlLocals[key] = tplAndVars[resolveIter++];
});
ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
if (modalOptions.controllerAs) {
if (modalOptions.bindToController) {
angular.extend(ctrlInstance, modalScope);
}

ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
if (modalOptions.controllerAs) {
if (modalOptions.bindToController) {
angular.extend(ctrlInstance, modalScope);
modalScope[modalOptions.controllerAs] = ctrlInstance;
}

modalScope[modalOptions.controllerAs] = ctrlInstance;
}
}

$modalStack.open(modalInstance, {
scope: modalScope,
deferred: modalResultDeferred,
renderDeferred: modalRenderDeferred,
content: tplAndVars[0],
animation: modalOptions.animation,
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard,
backdropClass: modalOptions.backdropClass,
windowClass: modalOptions.windowClass,
windowTemplateUrl: modalOptions.windowTemplateUrl,
size: modalOptions.size,
openedClass: modalOptions.openedClass
});
$modalStack.open(modalInstance, {
scope: modalScope,
deferred: modalResultDeferred,
renderDeferred: modalRenderDeferred,
content: tplAndVars[0],
animation: modalOptions.animation,
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard,
backdropClass: modalOptions.backdropClass,
windowClass: modalOptions.windowClass,
windowTemplateUrl: modalOptions.windowTemplateUrl,
size: modalOptions.size,
openedClass: modalOptions.openedClass
});
modalOpenedDeferred.resolve(true);

}, function resolveError(reason) {
modalResultDeferred.reject(reason);
});

templateAndResolvePromise.then(function() {
modalOpenedDeferred.resolve(true);
}, function(reason) {
modalOpenedDeferred.reject(reason);
modalResultDeferred.reject(reason);
})
.finally(function() {
if (promiseChain === samePromise) {
promiseChain = null;
}
});

return modalInstance;
Expand Down
86 changes: 84 additions & 2 deletions src/modal/test/modal.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
describe('$modal', function () {
var $animate, $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
var $modal, $modalProvider;
var $modal, $modalStack, $modalProvider;

beforeEach(module('ngAnimateMock'));
beforeEach(module('ui.bootstrap.modal'));
Expand All @@ -11,7 +11,7 @@ describe('$modal', function () {
$modalProvider = _$modalProvider_;
}));

beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_) {
beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_, _$modalStack_) {
$animate = _$animate_;
$rootScope = _$rootScope_;
$document = _$document_;
Expand All @@ -20,6 +20,7 @@ describe('$modal', function () {
$timeout = _$timeout_;
$q = _$q_;
$modal = _$modal_;
$modalStack = _$modalStack_;
}));

beforeEach(function() {
Expand Down Expand Up @@ -925,6 +926,87 @@ describe('$modal', function () {

element.remove();
});

it('should open modals and resolve the opened promises in order', function() {
// Opens a modal for each element in array order.
// Order is an array of non-repeating integers from 0..length-1 representing when to resolve that modal's promise.
// For example [1,2,0] would resolve the 3rd modal's promise first and the 2nd modal's promise last.
// Tests that the modals are added to $modalStack and that each resolves its "opened" promise sequentially.
// If an element is {reject:n} then n is still the order, but the corresponding promise is rejected.
// A rejection earlier in the open sequence should not affect modals opened later.
function test(order) {
var ds = []; // {index, deferred, reject}
var expected = ''; // 0..length-1
var actual = '';
angular.forEach(order, function(x, i) {
var reject = x.reject !== undefined;
if (reject) {
x = x.reject;
} else {
expected += i;
}
ds[x] = {index:i, deferred:$q.defer(), reject:reject};

var scope = $rootScope.$new();
scope.index = i;
open({
template: '<div>' + i + '</div>',
scope: scope,
resolve: {
x: function() { return ds[x].deferred.promise; }
}
}).opened.then(function() {
expect($modalStack.getTop().value.modalScope.index).toEqual(i);
actual += i;
});
});

angular.forEach(ds, function(d, i) {
if (d.reject) {
d.deferred.reject('rejected:' + d.index );
} else {
d.deferred.resolve('resolved:' + d.index );
}
$rootScope.$digest();
});

expect(actual).toEqual(expected);
expect($modal.getPromiseChain()).toEqual(null);
}

// Calls emit n! times on arrays of length n containing all non-repeating permutations of the integers 0..n-1.
function permute(n, emit) {
if (n < 1 || typeof emit !== 'function') {
return;
}
var a = [];
function _permute(depth) {
index: for (var i=0; i < n; i++) {
for (var j=0; j < depth; j++) {
if (a[j] === i) {
continue index; // already used
}
}

a[depth] = i;
if (depth + 1 === n) {
emit(angular.copy(a));
} else {
_permute(depth+1);
}
}
}
_permute(0);
}

permute(2, function(a) { test(a); });
permute(2, function(a) { test(a.map(function(x, i) { return {reject:x}; })); });
permute(2, function(a) { test(a.map(function(x, i) { return i === 0 ? {reject:x} : x; })); });
permute(3, function(a) { test(a); });
permute(3, function(a) { test(a.map(function(x, i) { return {reject:x}; })); });
permute(3, function(a) { test(a.map(function(x, i) { return i === 0 ? {reject:x} : x; })); });
permute(3, function(a) { test(a.map(function(x, i) { return i === 1 ? {reject:x} : x; })); });
});
});

describe('modal.closing event', function() {
Expand Down