Skip to content

Commit 77a34bd

Browse files
fix(interimElement): support scope.$destroy events
when navigation (eg $location) changes occur, the `scope.$destroy` event is triggered; which should remove/close/cleanup the following components: menu, select, dialog, toast, and bottomSheet. Fixes angular#3741. Fixes angular#4405. Fixes angular#4504. Fixes angular#4151. Closes angular#4659.
1 parent 3d0b418 commit 77a34bd

File tree

14 files changed

+424
-250
lines changed

14 files changed

+424
-250
lines changed

src/components/bottomSheet/bottom-sheet.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,17 @@ angular
1212
.directive('mdBottomSheet', MdBottomSheetDirective)
1313
.provider('$mdBottomSheet', MdBottomSheetProvider);
1414

15-
function MdBottomSheetDirective() {
15+
/* @ngInject */
16+
function MdBottomSheetDirective($mdBottomSheet) {
1617
return {
17-
restrict: 'E'
18+
restrict: 'E',
19+
link : function postLink(scope, element, attr) {
20+
// When navigation force destroys an interimElement, then
21+
// listen and $destroy() that interim instance...
22+
scope.$on('$destroy', function() {
23+
$mdBottomSheet.destroy();
24+
});
25+
}
1826
};
1927
}
2028

src/components/bottomSheet/bottom-sheet.spec.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ describe('$mdBottomSheet service', function() {
22
beforeEach(module('material.components.bottomSheet'));
33

44
describe('#build()', function() {
5-
it('should escapeToClose == true', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
5+
it('should close when `escapeToClose == true`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
66
var parent = angular.element('<div>');
77
$mdBottomSheet.show({
88
template: '<md-bottom-sheet>',
@@ -24,7 +24,7 @@ describe('$mdBottomSheet service', function() {
2424
expect(parent.find('md-bottom-sheet').length).toBe(0);
2525
}));
2626

27-
it('should escapeToClose == false', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
27+
it('should not close when `escapeToClose == false`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
2828
var parent = angular.element('<div>');
2929
$mdBottomSheet.show({
3030
template: '<md-bottom-sheet>',
@@ -42,6 +42,23 @@ describe('$mdBottomSheet service', function() {
4242
expect(parent.find('md-bottom-sheet').length).toBe(1);
4343
}));
4444

45+
it('should close when navigation fires `scope.$destroy()`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
46+
var parent = angular.element('<div>');
47+
$mdBottomSheet.show({
48+
template: '<md-bottom-sheet>',
49+
parent: parent,
50+
escapeToClose: false
51+
});
52+
53+
$rootScope.$apply();
54+
$animate.triggerCallbacks();
55+
56+
expect(parent.find('md-bottom-sheet').length).toBe(1);
57+
58+
$rootScope.$destroy();
59+
expect(parent.find('md-bottom-sheet').length).toBe(0);
60+
}));
61+
4562
it('should focus child with md-autofocus', inject(function($rootScope, $animate, $document, $mdBottomSheet) {
4663
jasmine.mockElementFocus(this);
4764
var parent = angular.element('<div>');

src/components/bottomSheet/demoBasicUsage/script.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ angular.module('bottomSheetDemo1', ['ngMaterial'])
2222
controller: 'ListBottomSheetCtrl',
2323
targetEvent: $event
2424
}).then(function(clickedItem) {
25-
$scope.alert = clickedItem.name + ' clicked!';
25+
$scope.alert = clickedItem['name'] + ' clicked!';
2626
});
2727
};
2828

@@ -35,7 +35,7 @@ angular.module('bottomSheetDemo1', ['ngMaterial'])
3535
}).then(function(clickedItem) {
3636
$mdToast.show(
3737
$mdToast.simple()
38-
.content(clickedItem.name + ' clicked!')
38+
.content(clickedItem['name'] + ' clicked!')
3939
.position('top right')
4040
.hideDelay(1500)
4141
);

src/components/dialog/dialog.js

+36-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ angular
1010
.directive('mdDialog', MdDialogDirective)
1111
.provider('$mdDialog', MdDialogProvider);
1212

13-
function MdDialogDirective($$rAF, $mdTheming) {
13+
function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
1414
return {
1515
restrict: 'E',
1616
link: function(scope, element, attr) {
@@ -25,9 +25,19 @@ function MdDialogDirective($$rAF, $mdTheming) {
2525
//-- delayed image loading may impact scroll height, check after images are loaded
2626
angular.element(images).on('load', addOverflowClass);
2727
}
28+
29+
scope.$on('$destroy', function() {
30+
$mdDialog.destroy();
31+
});
32+
33+
/**
34+
*
35+
*/
2836
function addOverflowClass() {
2937
element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
3038
}
39+
40+
3141
});
3242
}
3343
};
@@ -530,16 +540,30 @@ function MdDialogProvider($$interimElementProvider) {
530540
function onRemove(scope, element, options) {
531541
options.deactivateListeners();
532542
options.unlockScreenReader();
543+
options.hideBackdrop(options.$destroy);
533544

534-
options.hideBackdrop();
545+
// For navigation $destroy events, do a quick, non-animated removal,
546+
// but for normal closes (from clicks, etc) animate the removal
535547

536-
return dialogPopOut(element, options)
537-
.finally(function() {
538-
angular.element($document[0].body).removeClass('md-dialog-is-showing');
539-
element.remove();
548+
return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
540549

541-
options.origin.focus();
542-
});
550+
/**
551+
* For normal closes, animate the removal.
552+
* For forced closes (like $destroy events), skip the animations
553+
*/
554+
function animateRemoval() {
555+
return dialogPopOut(element, options);
556+
}
557+
558+
/**
559+
* Detach the element
560+
*/
561+
function detachAndClean() {
562+
angular.element($document[0].body).removeClass('md-dialog-is-showing');
563+
element.remove();
564+
565+
if (!options.$destroy) options.origin.focus();
566+
}
543567
}
544568

545569
/**
@@ -661,10 +685,12 @@ function MdDialogProvider($$interimElementProvider) {
661685
/**
662686
* Hide modal backdrop element...
663687
*/
664-
options.hideBackdrop = function hideBackdrop() {
688+
options.hideBackdrop = function hideBackdrop($destroy) {
665689
if (options.backdrop) {
666-
$animate.leave(options.backdrop);
690+
if ( !!$destroy ) options.backdrop.remove();
691+
else $animate.leave(options.backdrop);
667692
}
693+
668694
if (options.disableParentScroll) {
669695
options.restoreScroll();
670696
delete options.restoreScroll;
@@ -755,7 +781,6 @@ function MdDialogProvider($$interimElementProvider) {
755781

756782
var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
757783
var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
758-
759784
var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
760785

761786
container.css({

src/components/dialog/dialog.spec.js

+22
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,28 @@ describe('$mdDialog', function() {
158158
expect(container.length).toBe(0);
159159
}));
160160

161+
it('should remove `md-dialog-container` on scope.$destroy()', inject(function($mdDialog, $rootScope, $timeout) {
162+
var container, parent = angular.element('<div>');
163+
164+
$mdDialog.show(
165+
$mdDialog.alert({
166+
template: '' +
167+
'<md-dialog>' +
168+
' <md-dialog-content tabIndex="0">' +
169+
' <p>Muppets are the best</p>' +
170+
' </md-dialog-content>' +
171+
'</md-dialog>',
172+
parent: parent
173+
})
174+
);
175+
176+
runAnimation(parent.find('md-dialog'));
177+
$rootScope.$destroy();
178+
container = angular.element(parent[0].querySelector('.md-dialog-container'));
179+
180+
expect(container.length).toBe(0);
181+
}));
182+
161183
});
162184

163185
describe('#confirm()', function() {

src/components/menu/js/menuController.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) {
100100
preserveElement: self.isInMenuBar || self.nestedMenus.length > 0,
101101
parent: self.isInMenuBar ? $element : 'body'
102102
});
103-
}
103+
};
104104

105105
// Expose a open function to the child scope for html to use
106106
$scope.$mdOpenMenu = this.open;
@@ -133,19 +133,24 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) {
133133
this.containerProxy && this.containerProxy(ev);
134134
};
135135

136+
this.destroy = function() {
137+
return $mdMenu.destroy();
138+
};
139+
136140
// Use the $mdMenu interim element service to close the menu contents
137141
this.close = function closeMenu(skipFocus, closeOpts) {
138142
if ( !self.isOpen ) return;
139143
self.isOpen = false;
140144

141145
$scope.$emit('$mdMenuClose', $element);
142146
$mdMenu.hide(null, closeOpts);
147+
143148
if (!skipFocus) {
144149
var el = self.restoreFocusTo || $element.find('button')[0];
145150
if (el instanceof angular.element) el = el[0];
146151
el.focus();
147152
}
148-
}
153+
};
149154

150155
/**
151156
* Build a nice object out of our string attribute which specifies the

src/components/menu/js/menuDirective.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,11 @@ function MenuDirective($mdUtil) {
191191
mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
192192

193193
scope.$on('$destroy', function() {
194-
menuContainer.remove();
195-
mdMenuCtrl.close();
194+
mdMenuCtrl
195+
.destroy()
196+
.finally(function(){
197+
menuContainer.remove();
198+
});
196199
});
197200

198201
}

src/components/menu/js/menuServiceProvider.js

+25-17
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function MenuProvider($$interimElementProvider) {
2222
});
2323

2424
/* @ngInject */
25-
function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate, $timeout) {
25+
function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) {
2626
var animator = $mdUtil.dom.animator;
2727

2828
return {
@@ -63,14 +63,8 @@ function MenuProvider($$interimElementProvider) {
6363
* Hide and destroys the backdrop created by showBackdrop()
6464
*/
6565
return function hideBackdrop() {
66-
if (options.backdrop) {
67-
// Override duration to immediately remove invisible backdrop
68-
options.backdrop.off('click');
69-
$animate.leave(options.backdrop, {duration:0});
70-
}
71-
if (options.disableParentScroll) {
72-
options.restoreScroll();
73-
}
66+
if (options.backdrop) options.backdrop.remove();
67+
if (options.disableParentScroll) options.restoreScroll();
7468
};
7569
}
7670

@@ -83,14 +77,28 @@ function MenuProvider($$interimElementProvider) {
8377
opts.cleanupResizing();
8478
opts.hideBackdrop();
8579

86-
return $animateCss(element, {addClass: 'md-leave'})
87-
.start()
88-
.then(function() {
89-
element.removeClass('md-active');
80+
// For navigation $destroy events, do a quick, non-animated removal,
81+
// but for normal closes (from clicks, etc) animate the removal
82+
83+
return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
84+
85+
/**
86+
* For normal closes, animate the removal.
87+
* For forced closes (like $destroy events), skip the animations
88+
*/
89+
function animateRemoval() {
90+
return $animateCss(element, {addClass: 'md-leave'}).start();
91+
}
92+
93+
/**
94+
* Detach the element
95+
*/
96+
function detachAndClean() {
97+
element.removeClass('md-active');
98+
detachElement(element, opts);
99+
opts.alreadyOpen = false;
100+
}
90101

91-
detachElement(element, opts);
92-
opts.alreadyOpen = false;
93-
});
94102
}
95103

96104
/**
@@ -361,7 +369,7 @@ function MenuProvider($$interimElementProvider) {
361369
}
362370

363371
/**
364-
* Use browser to remove this element without triggering a $destory event
372+
* Use browser to remove this element without triggering a $destroy event
365373
*/
366374
function detachElement(element, opts) {
367375
if (!opts.preserveElement) {

src/components/menu/menu.scss

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ $max-dense-menu-height: 2 * $baseline-grid + $max-visible-items * $dense-menu-it
1717
margin-top: $baseline-grid / 2;
1818
margin-bottom: $baseline-grid / 2;
1919
height: 1px;
20-
min-height: 1px;
2120
width: 100%;
2221
}
2322

src/components/menu/menu.spec.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ describe('material.components.menu', function() {
8686
});
8787

8888
it('closes on backdrop click', inject(function($document) {
89+
8990
openMenu(setup());
9091

9192
expect(getOpenMenuContainer().length).toBe(1);
@@ -96,6 +97,7 @@ describe('material.components.menu', function() {
9697
expect(getOpenMenuContainer().length).toBe(0);
9798
}));
9899

100+
99101
it('closes on escape', inject(function($document, $mdConstant) {
100102
openMenu(setup());
101103
expect(getOpenMenuContainer().length).toBe(1);
@@ -108,6 +110,16 @@ describe('material.components.menu', function() {
108110
expect(getOpenMenuContainer().length).toBe(0);
109111
}));
110112

113+
it('closes on $destroy', inject(function($document, $rootScope) {
114+
var scope = $rootScope.$new();
115+
openMenu( setup(null,false,scope) );
116+
117+
expect(getOpenMenuContainer().length).toBe(1);
118+
scope.$destroy();
119+
120+
expect(getOpenMenuContainer().length).toBe(0);
121+
}));
122+
111123
describe('closes with -', function() {
112124
it('closes on normal option click', function() {
113125
expect(getOpenMenuContainer().length).toBe(0);
@@ -164,7 +176,7 @@ describe('material.components.menu', function() {
164176
}
165177
});
166178

167-
function setup(triggerType, noEvent) {
179+
function setup(triggerType, noEvent, scope) {
168180
var menu,
169181
template = $mdUtil.supplant('' +
170182
'<md-menu>' +
@@ -180,7 +192,7 @@ describe('material.components.menu', function() {
180192
$rootScope.doSomething = function($event) {
181193
menuActionPerformed = true;
182194
};
183-
menu = $compile(template)($rootScope);
195+
menu = $compile(template)(scope || $rootScope);
184196
});
185197

186198
attachedElements.push(menu);
@@ -193,7 +205,6 @@ describe('material.components.menu', function() {
193205
// Internal methods
194206
// ********************************************
195207

196-
197208
function getOpenMenuContainer() {
198209
var res;
199210
inject(function($document) {

0 commit comments

Comments
 (0)