Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 135cb3a

Browse files
devversionThomasBurleson
authored andcommitted
feat(dialog): allow to specify a content element
This allows developers, to specify a pre-rendered/compiled element, which will be not compiled each time a dialog opens. It is possible to specify: - A DOM Element - A String as CSS Selector When using an element, which is already present in the DOM, it will be anchored to the dialog, which means, that we temporary fetch it from the DOM into the dialog and move it back to its old DOM position upon close. Closes #7566. Closes #8491
1 parent 491e692 commit 135cb3a

File tree

5 files changed

+256
-8
lines changed

5 files changed

+256
-8
lines changed

src/components/dialog/demoBasicUsage/index.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<md-button class="md-primary md-raised" ng-click="showTabDialog($event)" >
2121
Tab Dialog
2222
</md-button>
23+
<md-button class="md-primary md-raised" ng-if="listPrerenderedButton" ng-click="showPrerenderedDialog($event)">
24+
Pre-Rendered Dialog
25+
</md-button>
2326
</div>
2427
<p class="footer">Note: The <b>Confirm</b> dialog does not use <code>$mdDialog.clickOutsideToClose(true)</code>.</p>
2528
<div hide-gt-sm layout="row" layout-align="center center" flex>
@@ -32,4 +35,26 @@
3235
</b>
3336
</div>
3437

38+
<div class="dialog-demo-prerendered">
39+
<md-checkbox ng-model="listPrerenderedButton">Show Pre-Rendered Dialog</md-checkbox>
40+
<p class="md-caption">Sometimes you may not want to compile the dialogs template on each opening.</p>
41+
</div>
42+
43+
44+
<div style="visibility: hidden">
45+
<div class="md-dialog-container" id="myDialog">
46+
<md-dialog layout-padding>
47+
<h2>Pre-Rendered Dialog</h2>
48+
<p>
49+
This is a pre-rendered dialog, which means that <code>$mdDialog</code> doesn't compile its
50+
template on each opening.
51+
<br/><br/>
52+
The Dialog Element is a static element in the DOM, which is just visually hidden.<br/>
53+
Once the dialog opens, we just fetch the element from the DOM into our dialog and upon close
54+
we restore the element back into its old DOM position.
55+
</p>
56+
</md-dialog>
57+
</div>
58+
</div>
59+
3560
</div>

src/components/dialog/demoBasicUsage/script.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ angular.module('dialogDemo1', ['ngMaterial'])
9696
$scope.status = 'You cancelled the dialog.';
9797
});
9898
};
99+
100+
$scope.showPrerenderedDialog = function(ev) {
101+
$mdDialog.show({
102+
controller: DialogController,
103+
contentElement: '#myDialog',
104+
parent: angular.element(document.body),
105+
targetEvent: ev,
106+
clickOutsideToClose: true
107+
});
108+
};
99109
});
100110

101111
function DialogController($scope, $mdDialog) {

src/components/dialog/demoBasicUsage/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ button {
1919
div#status {
2020
color: #c60008;
2121
}
22+
23+
.dialog-demo-prerendered md-checkbox {
24+
margin-bottom: 0;
25+
}

src/components/dialog/dialog.js

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,40 @@ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
200200
* })(angular);
201201
* </hljs>
202202
*
203+
* ### Pre-Rendered Dialogs
204+
* By using the `contentElement` option, it is possible to use an already existing element in the DOM.
205+
*
206+
* <hljs lang="js">
207+
* $scope.showPrerenderedDialog = function() {
208+
* $mdDialog.show({
209+
* contentElement: '#myStaticDialog'
210+
* parent: angular.element(document.body)
211+
* });
212+
* };
213+
* </hljs>
214+
*
215+
* When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
216+
*
217+
* <hljs lang="html">
218+
* <div style="visibility: hidden">
219+
* <div class="md-dialog-container" id="myStaticDialog">
220+
* <md-dialog>
221+
* This is a pre-rendered dialog.
222+
* </md-dialog>
223+
* </div>
224+
* </div>
225+
* </hljs>
226+
*
227+
* **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
228+
* will not show up.
229+
*
230+
* It also possible to use a DOM element for the `contentElement` option.
231+
* - `contentElement: document.querySelector('#myStaticDialog')`
232+
* - `contentElement: angular.element(TEMPLATE)`
233+
*
234+
* When using a `template` as content element, it will be not compiled upon open.
235+
* This allows you to compile the element yourself and use it each time the dialog opens.
236+
*
203237
* ### JavaScript: promise API syntax, custom dialog template
204238
* <hljs lang="js">
205239
* (function(angular, undefined){
@@ -410,6 +444,11 @@ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
410444
* - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
411445
* with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
412446
* This template should **never** be constructed with any kind of user input or user data.
447+
* - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
448+
* dialog opens, you can also use a DOM element.<br/>
449+
* * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
450+
* the dialog and restores it at the old DOM position upon close.
451+
* * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
413452
* - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
414453
* `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
415454
* custom dialog directive.
@@ -491,7 +530,7 @@ function MdDialogProvider($$interimElementProvider) {
491530
return $$interimElementProvider('$mdDialog')
492531
.setDefaults({
493532
methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
494-
'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen'],
533+
'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'contentElement'],
495534
options: dialogDefaultOptions
496535
})
497536
.addPreset('alert', {
@@ -567,6 +606,7 @@ function MdDialogProvider($$interimElementProvider) {
567606
clickOutsideToClose: false,
568607
escapeToClose: true,
569608
targetEvent: null,
609+
contentElement: null,
570610
closeTo: null,
571611
openFrom: null,
572612
focusOnOpen: true,
@@ -613,6 +653,29 @@ function MdDialogProvider($$interimElementProvider) {
613653
function onShow(scope, element, options, controller) {
614654
angular.element($document[0].body).addClass('md-dialog-is-showing');
615655

656+
if (options.contentElement) {
657+
var contentEl = options.contentElement;
658+
659+
if (angular.isString(contentEl)) {
660+
contentEl = document.querySelector(contentEl);
661+
options.elementInsertionSibling = contentEl.nextElementSibling;
662+
options.elementInsertionParent = contentEl.parentNode;
663+
} else {
664+
contentEl = contentEl[0] || contentEl;
665+
// When the element is not visible in the DOM, then we can treat is as same
666+
// as a normal dialog would do. Removing it at close etc.
667+
// ---
668+
// When the element is visible in the DOM, then we restore it at close of the dialog.
669+
if (document.contains(contentEl)) {
670+
options.elementInsertionSibling = contentEl.nextElementSibling;
671+
options.elementInsertionParent = contentEl.parentNode;
672+
}
673+
}
674+
675+
options.elementInsertionEntry = contentEl;
676+
element = angular.element(contentEl);
677+
}
678+
616679
captureParentAndFromToElements(options);
617680
configureAria(element.find('md-dialog'), options);
618681
showBackdrop(scope, element, options);
@@ -690,12 +753,37 @@ function MdDialogProvider($$interimElementProvider) {
690753
return dialogPopOut(element, options);
691754
}
692755

756+
function removeContentElement() {
757+
if (!options.contentElement) return;
758+
759+
options.reverseContainerStretch();
760+
761+
if (!options.elementInsertionParent) {
762+
// When the contentElement has no parent, then it's a virtual DOM element, which should
763+
// be removed at close, as same as normal templates inside of a dialog.
764+
options.elementInsertionEntry.parentNode.removeChild(options.elementInsertionEntry);
765+
} else if (!options.elementInsertionSibling) {
766+
// When the contentElement doesn't have any sibling, then it can be simply appended to the
767+
// parent, because it plays no role, which index it had before.
768+
options.elementInsertionParent.appendChild(options.elementInsertionEntry);
769+
} else {
770+
// When the contentElement has a sibling, which marks the previous position of the contentElement
771+
// in the DOM, we insert it correctly before the sibling, to have the same index as before.
772+
options.elementInsertionParent.insertBefore(options.elementInsertionEntry, options.elementInsertionSibling);
773+
}
774+
}
775+
693776
/**
694777
* Detach the element
695778
*/
696779
function detachAndClean() {
697780
angular.element($document[0].body).removeClass('md-dialog-is-showing');
698-
element.remove();
781+
// Only remove the element, if it's not provided through the contentElement option.
782+
if (!options.contentElement) {
783+
element.remove();
784+
} else {
785+
removeContentElement();
786+
}
699787

700788
if (!options.$destroy) options.origin.focus();
701789
}
@@ -764,7 +852,7 @@ function MdDialogProvider($$interimElementProvider) {
764852
*/
765853
function activateListeners(element, options) {
766854
var window = angular.element($window);
767-
var onWindowResize = $mdUtil.debounce(function(){
855+
var onWindowResize = $mdUtil.debounce(function() {
768856
stretchDialogContainerToViewport(element, options);
769857
}, 60);
770858

@@ -993,12 +1081,22 @@ function MdDialogProvider($$interimElementProvider) {
9931081
var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
9941082
var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
9951083

1084+
var previousStyles = {
1085+
top: container.css('top'),
1086+
height: container.css('height')
1087+
};
1088+
9961089
container.css({
9971090
top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
9981091
height: height ? height + 'px' : '100%'
9991092
});
10001093

1001-
return container;
1094+
return function() {
1095+
// Reverts the modified styles back to the previous values.
1096+
// This is needed for contentElements, which should have the same styles after close
1097+
// as before.
1098+
container.css(previousStyles);
1099+
};
10021100
}
10031101

10041102
/**
@@ -1007,7 +1105,7 @@ function MdDialogProvider($$interimElementProvider) {
10071105
function dialogPopIn(container, options) {
10081106
// Add the `md-dialog-container` to the DOM
10091107
options.parent.append(container);
1010-
stretchDialogContainerToViewport(container, options);
1108+
options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
10111109

10121110
var dialogEl = container.find('md-dialog');
10131111
var animator = $mdUtil.dom.animator;
@@ -1023,7 +1121,7 @@ function MdDialogProvider($$interimElementProvider) {
10231121
return animator
10241122
.translate3d(dialogEl, from, to, translateOptions)
10251123
.then(function(animateReversal) {
1026-
// Build a reversal translate function synched to this translation...
1124+
// Build a reversal translate function synced to this translation...
10271125
options.reverseAnimate = function() {
10281126
delete options.reverseAnimate;
10291127

@@ -1038,14 +1136,24 @@ function MdDialogProvider($$interimElementProvider) {
10381136
}
10391137

10401138
return animateReversal(
1041-
animator.toTransformCss(
1139+
to = animator.toTransformCss(
10421140
// in case the origin element has moved or is hidden,
10431141
// let's recalculate the translateCSS
10441142
buildTranslateToOrigin(dialogEl, options.origin)
10451143
)
10461144
);
10471145

10481146
};
1147+
1148+
// Builds a function, which clears the animations / transforms of the dialog element.
1149+
// Required for contentElements, which should not have the the animation styling after
1150+
// the dialog is closed.
1151+
options.clearAnimate = function() {
1152+
delete options.clearAnimate;
1153+
return animator
1154+
.translate3d(dialogEl, to, animator.toTransformCss(''), {});
1155+
};
1156+
10491157
return true;
10501158
});
10511159
}
@@ -1054,7 +1162,13 @@ function MdDialogProvider($$interimElementProvider) {
10541162
* Dialog close and pop-out animation
10551163
*/
10561164
function dialogPopOut(container, options) {
1057-
return options.reverseAnimate();
1165+
return options.reverseAnimate().then(function() {
1166+
if (options.contentElement) {
1167+
// When we use a contentElement, we want the element to be the same as before.
1168+
// That means, that we have to clear all the animation properties, like transform.
1169+
options.clearAnimate();
1170+
}
1171+
});
10581172
}
10591173

10601174
/**

src/components/dialog/dialog.spec.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,101 @@ describe('$mdDialog', function() {
11481148
expect(parent[0].querySelectorAll('md-dialog.two').length).toBe(0);
11491149
}));
11501150

1151+
describe('contentElement', function() {
1152+
var $mdDialog, $rootScope, $compile, $timeout;
1153+
1154+
beforeEach(inject(function($injector) {
1155+
$mdDialog = $injector.get('$mdDialog');
1156+
$rootScope = $injector.get('$rootScope');
1157+
$compile = $injector.get('$compile');
1158+
$timeout = $injector.get('$timeout');
1159+
}));
1160+
1161+
it('should correctly move the contentElement', function() {
1162+
var contentElement = $compile(
1163+
'<div class="md-dialog-container">' +
1164+
'<md-dialog>Dialog</md-dialog>' +
1165+
'</div>'
1166+
)($rootScope);
1167+
var parentEl = angular.element('<div>');
1168+
1169+
// Add the contentElement to the DOM.
1170+
document.body.appendChild(contentElement[0]);
1171+
1172+
$mdDialog.show({
1173+
contentElement: contentElement,
1174+
parent: parentEl,
1175+
escapeToClose: true
1176+
});
1177+
1178+
$rootScope.$apply();
1179+
runAnimation();
1180+
1181+
expect(contentElement[0].parentNode).toBe(parentEl[0]);
1182+
1183+
$mdDialog.hide();
1184+
runAnimation();
1185+
1186+
expect(contentElement[0].parentNode).toBe(document.body);
1187+
1188+
document.body.removeChild(contentElement[0]);
1189+
});
1190+
1191+
it('should correctly query for a contentElement', function() {
1192+
var contentElement = $compile(
1193+
'<div class="md-dialog-container" id="myId">' +
1194+
'<md-dialog>Dialog</md-dialog>' +
1195+
'</div>'
1196+
)($rootScope);
1197+
var parentEl = angular.element('<div>');
1198+
1199+
// Add the contentElement to the DOM.
1200+
document.body.appendChild(contentElement[0]);
1201+
1202+
$mdDialog.show({
1203+
contentElement: '#myId',
1204+
parent: parentEl,
1205+
escapeToClose: true
1206+
});
1207+
1208+
$rootScope.$apply();
1209+
runAnimation();
1210+
1211+
expect(contentElement[0].parentNode).toBe(parentEl[0]);
1212+
1213+
$mdDialog.hide();
1214+
runAnimation();
1215+
1216+
expect(contentElement[0].parentNode).toBe(document.body);
1217+
1218+
document.body.removeChild(contentElement[0]);
1219+
});
1220+
1221+
it('should also work with a virtual pre-compiled element', function() {
1222+
var contentElement = $compile(
1223+
'<div class="md-dialog-container" id="myId">' +
1224+
'<md-dialog>Dialog</md-dialog>' +
1225+
'</div>'
1226+
)($rootScope);
1227+
var parentEl = angular.element('<div>');
1228+
1229+
$mdDialog.show({
1230+
contentElement: contentElement,
1231+
parent: parentEl,
1232+
escapeToClose: true
1233+
});
1234+
1235+
$rootScope.$apply();
1236+
runAnimation();
1237+
1238+
expect(contentElement[0].parentNode).toBe(parentEl[0]);
1239+
1240+
$mdDialog.hide();
1241+
runAnimation();
1242+
});
1243+
1244+
});
1245+
11511246
it('should have the dialog role', inject(function($mdDialog, $rootScope) {
11521247
var template = '<md-dialog>Hello</md-dialog>';
11531248
var parent = angular.element('<div>');

0 commit comments

Comments
 (0)