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

Commit

Permalink
feat(modal): complete modal open resolution in order
Browse files Browse the repository at this point in the history
- Ensure modals resolve in order they were opened

Closes #2443
Closes #4302
Resolves #2404
  • Loading branch information
Nathan Williams authored and wesleycho committed Aug 28, 2015
1 parent 937a1f3 commit 1bba8b4
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 49 deletions.
106 changes: 59 additions & 47 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
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 @@ -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: '<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

0 comments on commit 1bba8b4

Please sign in to comment.