diff --git a/src/modal/modal.js b/src/modal/modal.js index 4843490fad..ca9a939455 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -611,6 +611,11 @@ angular.module('ui.bootstrap.modal', []) return promisesArr; } + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + $modal.open = function (modalOptions) { var modalResultDeferred = $q.defer(); @@ -642,63 +647,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; diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index 66ae1a91fe..db0d40158b 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -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')); @@ -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_; @@ -20,6 +20,7 @@ describe('$modal', function () { $timeout = _$timeout_; $q = _$q_; $modal = _$modal_; + $modalStack = _$modalStack_; })); beforeEach(function() { @@ -984,6 +985,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: '
' + i + '
', + 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() {