Skip to content

Commit 72e887d

Browse files
committed
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.
1 parent 2b98560 commit 72e887d

File tree

4 files changed

+149
-47
lines changed

4 files changed

+149
-47
lines changed

src/components/dialog/dialog.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,6 +1607,8 @@ describe('$mdDialog', function() {
16071607
document.body.appendChild(parent);
16081608

16091609
$mdDialog.show({template: template, parent: parent});
1610+
runAnimation();
1611+
16101612
$rootScope.$apply();
16111613

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

src/components/menu/js/menuServiceProvider.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,12 @@ 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
@@ -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.when(true) : $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,34 @@ function InterimElementProvider() {
325339
*
326340
*/
327341
function hide(reason, options) {
328-
if ( !stack.length ) return $q.when(reason);
342+
if (!showingInterims.length) {
343+
return $q.when(reason);
344+
}
345+
329346
options = options || {};
330347

331348
if (options.closeAll) {
332-
var promise = $q.all(stack.reverse().map(closeElement));
333-
stack = [];
334-
return promise;
349+
// We have to make a shallow copy of the array, because otherwise the map will break.
350+
return $q.all(showingInterims.slice().reverse().map(closeElement));
335351
} else if (options.closeTo !== undefined) {
336-
return $q.all(stack.splice(options.closeTo).map(closeElement));
352+
return $q.all(showingInterims.slice(options.closeTo).map(closeElement));
337353
} else {
338-
var interim = stack.pop();
354+
var interim = showingInterims.pop();
339355
return closeElement(interim);
340356
}
341357

342358
function closeElement(interim) {
343-
interim
359+
360+
var hideAction = interim
344361
.remove(reason, false, options || { })
345-
.catch(function( reason ) {
346-
//$log.error("InterimElement.hide() error: " + reason );
347-
return reason;
362+
.catch(function(reason) { return reason; })
363+
.finally(function() {
364+
hidePromises.splice(hidePromises.indexOf(hideAction), 1);
348365
});
366+
367+
showingInterims.splice(showingInterims.indexOf(interim), 1);
368+
hidePromises.push(hideAction);
369+
349370
return interim.deferred.promise;
350371
}
351372
}
@@ -363,16 +384,20 @@ function InterimElementProvider() {
363384
*
364385
*/
365386
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;
387+
var interim = showingInterims.pop();
388+
if (!interim) {
389+
return $q.when(reason);
390+
}
391+
392+
var cancelAction = interim
393+
.remove(reason, true, options || {})
394+
.catch(function(reason) { return reason; })
395+
.finally(function() {
396+
hidePromises.splice(hidePromises.indexOf(cancelAction), 1);
374397
});
375398

399+
hidePromises.push(cancelAction);
400+
376401
// Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise
377402
// is not handling the rejection. We create a pseudo catch handler, which will prevent the
378403
// promise from being logged to the $exceptionHandler.
@@ -383,26 +408,27 @@ function InterimElementProvider() {
383408
* Special method to quick-remove the interim element without animations
384409
* Note: interim elements are in "interim containers"
385410
*/
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-
});
411+
function destroy(targetEl) {
412+
var interim = !targetEl ? showingInterims.shift() : null;
396413

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-
}
414+
var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode;
415+
416+
if (parentEl) {
417+
// Try to find the interim in the stack which corresponds to the supplied DOM element.
418+
var filtered = showingInterims.filter(function(entry) {
419+
return entry.options.element[0] === parentEl;
420+
});
421+
422+
// Note: This function might be called when the element already has been removed,
423+
// in which case we won't find any matches.
424+
if (filtered.length > 0) {
425+
interim = filtered[0];
426+
showingInterims.splice(showingInterims.indexOf(interim), 1);
427+
}
403428
}
404429

405-
return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED);
430+
return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) :
431+
$q.when(SHOW_CANCELLED);
406432
}
407433

408434
/*

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

Lines changed: 74 additions & 0 deletions
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 it completes hiding', 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)