Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 1bba8b4

Browse files
Nathan Williamswesleycho
Nathan Williams
authored andcommitted
feat(modal): complete modal open resolution in order
- Ensure modals resolve in order they were opened Closes #2443 Closes #4302 Resolves #2404
1 parent 937a1f3 commit 1bba8b4

File tree

2 files changed

+143
-49
lines changed

2 files changed

+143
-49
lines changed

src/modal/modal.js

+59-47
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,11 @@ angular.module('ui.bootstrap.modal', [])
611611
return promisesArr;
612612
}
613613

614+
var promiseChain = null;
615+
$modal.getPromiseChain = function() {
616+
return promiseChain;
617+
};
618+
614619
$modal.open = function (modalOptions) {
615620

616621
var modalResultDeferred = $q.defer();
@@ -642,63 +647,70 @@ angular.module('ui.bootstrap.modal', [])
642647
var templateAndResolvePromise =
643648
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
644649

650+
// Wait for the resolution of the existing promise chain.
651+
// Then switch to our own combined promise dependency (regardless of how the previous modal fared).
652+
// Then add to $modalStack and resolve opened.
653+
// Finally clean up the chain variable if no subsequent modal has overwritten it.
654+
var samePromise;
655+
samePromise = promiseChain = $q.all([promiseChain])
656+
.then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; })
657+
.then(function resolveSuccess(tplAndVars) {
658+
659+
var modalScope = (modalOptions.scope || $rootScope).$new();
660+
modalScope.$close = modalInstance.close;
661+
modalScope.$dismiss = modalInstance.dismiss;
662+
663+
modalScope.$on('$destroy', function() {
664+
if (!modalScope.$$uibDestructionScheduled) {
665+
modalScope.$dismiss('$uibUnscheduledDestruction');
666+
}
667+
});
645668

646-
templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
647-
648-
var modalScope = (modalOptions.scope || $rootScope).$new();
649-
modalScope.$close = modalInstance.close;
650-
modalScope.$dismiss = modalInstance.dismiss;
651-
652-
modalScope.$on('$destroy', function() {
653-
if (!modalScope.$$uibDestructionScheduled) {
654-
modalScope.$dismiss('$uibUnscheduledDestruction');
655-
}
656-
});
669+
var ctrlInstance, ctrlLocals = {};
670+
var resolveIter = 1;
657671

658-
var ctrlInstance, ctrlLocals = {};
659-
var resolveIter = 1;
672+
//controllers
673+
if (modalOptions.controller) {
674+
ctrlLocals.$scope = modalScope;
675+
ctrlLocals.$modalInstance = modalInstance;
676+
angular.forEach(modalOptions.resolve, function(value, key) {
677+
ctrlLocals[key] = tplAndVars[resolveIter++];
678+
});
660679

661-
//controllers
662-
if (modalOptions.controller) {
663-
ctrlLocals.$scope = modalScope;
664-
ctrlLocals.$modalInstance = modalInstance;
665-
angular.forEach(modalOptions.resolve, function(value, key) {
666-
ctrlLocals[key] = tplAndVars[resolveIter++];
667-
});
680+
ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
681+
if (modalOptions.controllerAs) {
682+
if (modalOptions.bindToController) {
683+
angular.extend(ctrlInstance, modalScope);
684+
}
668685

669-
ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
670-
if (modalOptions.controllerAs) {
671-
if (modalOptions.bindToController) {
672-
angular.extend(ctrlInstance, modalScope);
686+
modalScope[modalOptions.controllerAs] = ctrlInstance;
673687
}
674-
675-
modalScope[modalOptions.controllerAs] = ctrlInstance;
676688
}
677-
}
678689

679-
$modalStack.open(modalInstance, {
680-
scope: modalScope,
681-
deferred: modalResultDeferred,
682-
renderDeferred: modalRenderDeferred,
683-
content: tplAndVars[0],
684-
animation: modalOptions.animation,
685-
backdrop: modalOptions.backdrop,
686-
keyboard: modalOptions.keyboard,
687-
backdropClass: modalOptions.backdropClass,
688-
windowClass: modalOptions.windowClass,
689-
windowTemplateUrl: modalOptions.windowTemplateUrl,
690-
size: modalOptions.size,
691-
openedClass: modalOptions.openedClass
692-
});
690+
$modalStack.open(modalInstance, {
691+
scope: modalScope,
692+
deferred: modalResultDeferred,
693+
renderDeferred: modalRenderDeferred,
694+
content: tplAndVars[0],
695+
animation: modalOptions.animation,
696+
backdrop: modalOptions.backdrop,
697+
keyboard: modalOptions.keyboard,
698+
backdropClass: modalOptions.backdropClass,
699+
windowClass: modalOptions.windowClass,
700+
windowTemplateUrl: modalOptions.windowTemplateUrl,
701+
size: modalOptions.size,
702+
openedClass: modalOptions.openedClass
703+
});
704+
modalOpenedDeferred.resolve(true);
693705

694706
}, function resolveError(reason) {
695-
modalResultDeferred.reject(reason);
696-
});
697-
698-
templateAndResolvePromise.then(function() {
699-
modalOpenedDeferred.resolve(true);
700-
}, function(reason) {
701707
modalOpenedDeferred.reject(reason);
708+
modalResultDeferred.reject(reason);
709+
})
710+
.finally(function() {
711+
if (promiseChain === samePromise) {
712+
promiseChain = null;
713+
}
702714
});
703715

704716
return modalInstance;

src/modal/test/modal.spec.js

+84-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
describe('$modal', function () {
22
var $animate, $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
3-
var $modal, $modalProvider;
3+
var $modal, $modalStack, $modalProvider;
44

55
beforeEach(module('ngAnimateMock'));
66
beforeEach(module('ui.bootstrap.modal'));
@@ -11,7 +11,7 @@ describe('$modal', function () {
1111
$modalProvider = _$modalProvider_;
1212
}));
1313

14-
beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_) {
14+
beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_, _$modalStack_) {
1515
$animate = _$animate_;
1616
$rootScope = _$rootScope_;
1717
$document = _$document_;
@@ -20,6 +20,7 @@ describe('$modal', function () {
2020
$timeout = _$timeout_;
2121
$q = _$q_;
2222
$modal = _$modal_;
23+
$modalStack = _$modalStack_;
2324
}));
2425

2526
beforeEach(function() {
@@ -984,6 +985,87 @@ describe('$modal', function () {
984985

985986
element.remove();
986987
});
988+
989+
it('should open modals and resolve the opened promises in order', function() {
990+
// Opens a modal for each element in array order.
991+
// Order is an array of non-repeating integers from 0..length-1 representing when to resolve that modal's promise.
992+
// For example [1,2,0] would resolve the 3rd modal's promise first and the 2nd modal's promise last.
993+
// Tests that the modals are added to $modalStack and that each resolves its "opened" promise sequentially.
994+
// If an element is {reject:n} then n is still the order, but the corresponding promise is rejected.
995+
// A rejection earlier in the open sequence should not affect modals opened later.
996+
function test(order) {
997+
var ds = []; // {index, deferred, reject}
998+
var expected = ''; // 0..length-1
999+
var actual = '';
1000+
angular.forEach(order, function(x, i) {
1001+
var reject = x.reject !== undefined;
1002+
if (reject) {
1003+
x = x.reject;
1004+
} else {
1005+
expected += i;
1006+
}
1007+
ds[x] = {index:i, deferred:$q.defer(), reject:reject};
1008+
1009+
var scope = $rootScope.$new();
1010+
scope.index = i;
1011+
open({
1012+
template: '<div>' + i + '</div>',
1013+
scope: scope,
1014+
resolve: {
1015+
x: function() { return ds[x].deferred.promise; }
1016+
}
1017+
}).opened.then(function() {
1018+
expect($modalStack.getTop().value.modalScope.index).toEqual(i);
1019+
actual += i;
1020+
});
1021+
});
1022+
1023+
angular.forEach(ds, function(d, i) {
1024+
if (d.reject) {
1025+
d.deferred.reject('rejected:' + d.index );
1026+
} else {
1027+
d.deferred.resolve('resolved:' + d.index );
1028+
}
1029+
$rootScope.$digest();
1030+
});
1031+
1032+
expect(actual).toEqual(expected);
1033+
expect($modal.getPromiseChain()).toEqual(null);
1034+
}
1035+
1036+
// Calls emit n! times on arrays of length n containing all non-repeating permutations of the integers 0..n-1.
1037+
function permute(n, emit) {
1038+
if (n < 1 || typeof emit !== 'function') {
1039+
return;
1040+
}
1041+
var a = [];
1042+
function _permute(depth) {
1043+
index: for (var i = 0; i < n; i++) {
1044+
for (var j = 0; j < depth; j++) {
1045+
if (a[j] === i) {
1046+
continue index; // already used
1047+
}
1048+
}
1049+
1050+
a[depth] = i;
1051+
if (depth + 1 === n) {
1052+
emit(angular.copy(a));
1053+
} else {
1054+
_permute(depth + 1);
1055+
}
1056+
}
1057+
}
1058+
_permute(0);
1059+
}
1060+
1061+
permute(2, function(a) { test(a); });
1062+
permute(2, function(a) { test(a.map(function(x, i) { return {reject:x}; })); });
1063+
permute(2, function(a) { test(a.map(function(x, i) { return i === 0 ? {reject:x} : x; })); });
1064+
permute(3, function(a) { test(a); });
1065+
permute(3, function(a) { test(a.map(function(x, i) { return {reject:x}; })); });
1066+
permute(3, function(a) { test(a.map(function(x, i) { return i === 0 ? {reject:x} : x; })); });
1067+
permute(3, function(a) { test(a.map(function(x, i) { return i === 1 ? {reject:x} : x; })); });
1068+
});
9871069
});
9881070

9891071
describe('modal.closing event', function() {

0 commit comments

Comments
 (0)