Skip to content

Commit 421fed4

Browse files
devversionjelbourn
authored andcommitted
feat(interimElement): properly handle multiple interims. (angular#9053)
* feat(interimElement): properly handle multiple interims. * Adds support for multiple interim elements (like dialog) * When single interim (default) is enabled, then interims should hide properly (as in toasts). Fixes angular#8624. References angular#8630. * When hiding wait for currenlty opening interims * Address EladBezalel's feedback * Wait for interim element for cancel as well * Address EladBezalel's feedback
1 parent 1f32ccb commit 421fed4

File tree

4 files changed

+174
-51
lines changed

4 files changed

+174
-51
lines changed

src/components/dialog/dialog.spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,8 @@ describe('$mdDialog', function() {
16401640
document.body.appendChild(parent);
16411641

16421642
$mdDialog.show({template: template, parent: parent});
1643+
runAnimation();
1644+
16431645
$rootScope.$apply();
16441646

16451647
// It should add two focus traps to the document around the dialog content.

src/components/menu/js/menuServiceProvider.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function MenuProvider($$interimElementProvider) {
3434
disableParentScroll: true,
3535
skipCompile: true,
3636
preserveScope: true,
37-
skipHide: true,
37+
multiple: true,
3838
themable: true
3939
};
4040

src/core/services/interimElement/interimElement.js

+97-50
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,20 @@ function InterimElementProvider() {
257257
* A service used to control inserting and removing an element into the DOM.
258258
*
259259
*/
260-
var service, stack = [];
260+
261+
var service;
262+
263+
var showPromises = []; // Promises for the interim's which are currently opening.
264+
var hidePromises = []; // Promises for the interim's which are currently hiding.
265+
var showingInterims = []; // Interim elements which are currently showing up.
261266

262267
// Publish instance $$interimElement service;
263268
// ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect
264269

265270
return service = {
266271
show: show,
267-
hide: hide,
268-
cancel: cancel,
272+
hide: waitForInterim(hide),
273+
cancel: waitForInterim(cancel),
269274
destroy : destroy,
270275
$injector_: $injector
271276
};
@@ -286,26 +291,35 @@ function InterimElementProvider() {
286291
function show(options) {
287292
options = options || {};
288293
var interimElement = new InterimElement(options || {});
294+
289295
// When an interim element is currently showing, we have to cancel it.
290296
// Just hiding it, will resolve the InterimElement's promise, the promise should be
291297
// rejected instead.
292-
var hideExisting = !options.skipHide && stack.length ? service.cancel() : $q.when(true);
293-
294-
// This hide()s only the current interim element before showing the next, new one
295-
// NOTE: this is not reversible (e.g. interim elements are not stackable)
298+
var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises);
299+
300+
if (!options.multiple) {
301+
// Wait for all opening interim's to finish their transition.
302+
hideAction = hideAction.then(function() {
303+
// Wait for all closing and showing interim's to be completely closed.
304+
var promiseArray = hidePromises.concat(showingInterims.map(service.cancel));
305+
return $q.all(promiseArray);
306+
});
307+
}
296308

297-
hideExisting.finally(function() {
309+
var showAction = hideAction.then(function() {
298310

299-
stack.push(interimElement);
300-
interimElement
311+
return interimElement
301312
.show()
302-
.catch(function( reason ) {
303-
//$log.error("InterimElement.show() error: " + reason );
304-
return reason;
313+
.catch(function(reason) { return reason; })
314+
.finally(function() {
315+
showPromises.splice(showPromises.indexOf(showAction), 1);
316+
showingInterims.push(interimElement);
305317
});
306318

307319
});
308320

321+
showPromises.push(showAction);
322+
309323
// Return a promise that will be resolved when the interim
310324
// element is hidden or cancelled...
311325

@@ -325,27 +339,30 @@ function InterimElementProvider() {
325339
*
326340
*/
327341
function hide(reason, options) {
328-
if ( !stack.length ) return $q.when(reason);
329342
options = options || {};
330343

331344
if (options.closeAll) {
332-
var promise = $q.all(stack.reverse().map(closeElement));
333-
stack = [];
334-
return promise;
345+
// We have to make a shallow copy of the array, because otherwise the map will break.
346+
return $q.all(showingInterims.slice().reverse().map(closeElement));
335347
} else if (options.closeTo !== undefined) {
336-
return $q.all(stack.splice(options.closeTo).map(closeElement));
337-
} else {
338-
var interim = stack.pop();
339-
return closeElement(interim);
348+
return $q.all(showingInterims.slice(options.closeTo).map(closeElement));
340349
}
341350

351+
// Hide the latest showing interim element.
352+
return closeElement(showingInterims.pop());
353+
342354
function closeElement(interim) {
343-
interim
355+
356+
var hideAction = interim
344357
.remove(reason, false, options || { })
345-
.catch(function( reason ) {
346-
//$log.error("InterimElement.hide() error: " + reason );
347-
return reason;
358+
.catch(function(reason) { return reason; })
359+
.finally(function() {
360+
hidePromises.splice(hidePromises.indexOf(hideAction), 1);
348361
});
362+
363+
showingInterims.splice(showingInterims.indexOf(interim), 1);
364+
hidePromises.push(hideAction);
365+
349366
return interim.deferred.promise;
350367
}
351368
}
@@ -363,46 +380,76 @@ function InterimElementProvider() {
363380
*
364381
*/
365382
function cancel(reason, options) {
366-
var interim = stack.pop();
367-
if ( !interim ) return $q.when(reason);
368-
369-
interim
370-
.remove(reason, true, options || { })
371-
.catch(function( reason ) {
372-
//$log.error("InterimElement.cancel() error: " + reason );
373-
return reason;
383+
var interim = showingInterims.pop();
384+
if (!interim) {
385+
return $q.when(reason);
386+
}
387+
388+
var cancelAction = interim
389+
.remove(reason, true, options || {})
390+
.catch(function(reason) { return reason; })
391+
.finally(function() {
392+
hidePromises.splice(hidePromises.indexOf(cancelAction), 1);
374393
});
375394

395+
hidePromises.push(cancelAction);
396+
376397
// Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise
377398
// is not handling the rejection. We create a pseudo catch handler, which will prevent the
378399
// promise from being logged to the $exceptionHandler.
379400
return interim.deferred.promise.catch(angular.noop);
380401
}
381402

403+
/**
404+
* Creates a function to wait for at least one interim element to be available.
405+
* @param callbackFn Function to be used as callback
406+
* @returns {Function}
407+
*/
408+
function waitForInterim(callbackFn) {
409+
return function() {
410+
var fnArguments = arguments;
411+
412+
if (!showingInterims.length) {
413+
// When there are still interim's opening, then wait for the first interim element to
414+
// finish its open animation.
415+
if (showPromises.length) {
416+
return showPromises[0].finally(function () {
417+
return callbackFn.apply(service, fnArguments);
418+
});
419+
}
420+
421+
return $q.when("No interim elements currently showing up.");
422+
}
423+
424+
return callbackFn.apply(service, fnArguments);
425+
};
426+
}
427+
382428
/*
383429
* Special method to quick-remove the interim element without animations
384430
* Note: interim elements are in "interim containers"
385431
*/
386-
function destroy(target) {
387-
var interim = !target ? stack.shift() : null;
388-
var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null;
389-
390-
if (cntr) {
391-
// Try to find the interim element in the stack which corresponds to the supplied DOM element.
392-
var filtered = stack.filter(function(entry) {
393-
var currNode = entry.options.element[0];
394-
return (currNode === cntr);
395-
});
432+
function destroy(targetEl) {
433+
var interim = !targetEl ? showingInterims.shift() : null;
396434

397-
// Note: this function might be called when the element already has been removed, in which
398-
// case we won't find any matches. That's ok.
399-
if (filtered.length > 0) {
400-
interim = filtered[0];
401-
stack.splice(stack.indexOf(interim), 1);
402-
}
435+
var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode;
436+
437+
if (parentEl) {
438+
// Try to find the interim in the stack which corresponds to the supplied DOM element.
439+
var filtered = showingInterims.filter(function(entry) {
440+
return entry.options.element[0] === parentEl;
441+
});
442+
443+
// Note: This function might be called when the element already has been removed,
444+
// in which case we won't find any matches.
445+
if (filtered.length) {
446+
interim = filtered[0];
447+
showingInterims.splice(showingInterims.indexOf(interim), 1);
448+
}
403449
}
404450

405-
return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED);
451+
return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) :
452+
$q.when(SHOW_CANCELLED);
406453
}
407454

408455
/*

src/core/services/interimElement/interimElement.spec.js

+74
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,80 @@ describe('$$interimElement service', function() {
342342

343343
}));
344344

345+
it('should show multiple interim elements', function() {
346+
var showCount = 0;
347+
348+
showInterim();
349+
expect(showCount).toBe(1);
350+
351+
showInterim();
352+
expect(showCount).toBe(2);
353+
354+
function showInterim() {
355+
Service.show({
356+
template: '<div>First Interim</div>',
357+
onShow: function() {
358+
showCount++;
359+
},
360+
onRemove: function() {
361+
showCount--;
362+
},
363+
multiple: true
364+
});
365+
}
366+
});
367+
368+
369+
it('should not show multiple interim elements by default', function() {
370+
var showCount = 0;
371+
372+
showInterim();
373+
expect(showCount).toBe(1);
374+
375+
showInterim();
376+
expect(showCount).toBe(1);
377+
378+
function showInterim() {
379+
Service.show({
380+
template: '<div>First Interim</div>',
381+
onShow: function() {
382+
showCount++;
383+
},
384+
onRemove: function() {
385+
showCount--;
386+
}
387+
});
388+
}
389+
});
390+
391+
it('should cancel a previous interim after a second shows up', inject(function($q, $timeout) {
392+
var hidePromise = $q.defer();
393+
var isShown = false;
394+
395+
Service.show({
396+
template: '<div>First Interim</div>',
397+
onRemove: function() {
398+
return hidePromise.promise;
399+
}
400+
});
401+
402+
// Once we show the second interim, the first interim should be cancelled and new interim
403+
// will successfully show up after the first interim hides completely.
404+
Service.show({
405+
template: '<div>Second Interim</div>',
406+
onShow: function() {
407+
isShown = true;
408+
}
409+
});
410+
411+
expect(isShown).toBe(false);
412+
413+
hidePromise.resolve();
414+
$timeout.flush();
415+
416+
expect(isShown).toBe(true);
417+
}));
418+
345419
it('should cancel a previous shown interim element', inject(function() {
346420
var isCancelled = false;
347421

0 commit comments

Comments
 (0)