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

Commit

Permalink
feat(dialog): allow to specify a content element
Browse files Browse the repository at this point in the history
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
  • Loading branch information
devversion authored and ThomasBurleson committed May 25, 2016
1 parent 491e692 commit 135cb3a
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 8 deletions.
25 changes: 25 additions & 0 deletions src/components/dialog/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<md-button class="md-primary md-raised" ng-click="showTabDialog($event)" >
Tab Dialog
</md-button>
<md-button class="md-primary md-raised" ng-if="listPrerenderedButton" ng-click="showPrerenderedDialog($event)">
Pre-Rendered Dialog
</md-button>
</div>
<p class="footer">Note: The <b>Confirm</b> dialog does not use <code>$mdDialog.clickOutsideToClose(true)</code>.</p>
<div hide-gt-sm layout="row" layout-align="center center" flex>
Expand All @@ -32,4 +35,26 @@
</b>
</div>

<div class="dialog-demo-prerendered">
<md-checkbox ng-model="listPrerenderedButton">Show Pre-Rendered Dialog</md-checkbox>
<p class="md-caption">Sometimes you may not want to compile the dialogs template on each opening.</p>
</div>


<div style="visibility: hidden">
<div class="md-dialog-container" id="myDialog">
<md-dialog layout-padding>
<h2>Pre-Rendered Dialog</h2>
<p>
This is a pre-rendered dialog, which means that <code>$mdDialog</code> doesn't compile its
template on each opening.
<br/><br/>
The Dialog Element is a static element in the DOM, which is just visually hidden.<br/>
Once the dialog opens, we just fetch the element from the DOM into our dialog and upon close
we restore the element back into its old DOM position.
</p>
</md-dialog>
</div>
</div>

</div>
10 changes: 10 additions & 0 deletions src/components/dialog/demoBasicUsage/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ angular.module('dialogDemo1', ['ngMaterial'])
$scope.status = 'You cancelled the dialog.';
});
};

$scope.showPrerenderedDialog = function(ev) {
$mdDialog.show({
controller: DialogController,
contentElement: '#myDialog',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true
});
};
});

function DialogController($scope, $mdDialog) {
Expand Down
4 changes: 4 additions & 0 deletions src/components/dialog/demoBasicUsage/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ button {
div#status {
color: #c60008;
}

.dialog-demo-prerendered md-checkbox {
margin-bottom: 0;
}
130 changes: 122 additions & 8 deletions src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,40 @@ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
* })(angular);
* </hljs>
*
* ### Pre-Rendered Dialogs
* By using the `contentElement` option, it is possible to use an already existing element in the DOM.
*
* <hljs lang="js">
* $scope.showPrerenderedDialog = function() {
* $mdDialog.show({
* contentElement: '#myStaticDialog'
* parent: angular.element(document.body)
* });
* };
* </hljs>
*
* When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
*
* <hljs lang="html">
* <div style="visibility: hidden">
* <div class="md-dialog-container" id="myStaticDialog">
* <md-dialog>
* This is a pre-rendered dialog.
* </md-dialog>
* </div>
* </div>
* </hljs>
*
* **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
* will not show up.
*
* It also possible to use a DOM element for the `contentElement` option.
* - `contentElement: document.querySelector('#myStaticDialog')`
* - `contentElement: angular.element(TEMPLATE)`
*
* When using a `template` as content element, it will be not compiled upon open.
* This allows you to compile the element yourself and use it each time the dialog opens.
*
* ### JavaScript: promise API syntax, custom dialog template
* <hljs lang="js">
* (function(angular, undefined){
Expand Down Expand Up @@ -410,6 +444,11 @@ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
* - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
* with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
* This template should **never** be constructed with any kind of user input or user data.
* - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
* dialog opens, you can also use a DOM element.<br/>
* * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
* the dialog and restores it at the old DOM position upon close.
* * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
* - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
* `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
* custom dialog directive.
Expand Down Expand Up @@ -491,7 +530,7 @@ function MdDialogProvider($$interimElementProvider) {
return $$interimElementProvider('$mdDialog')
.setDefaults({
methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen'],
'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'contentElement'],
options: dialogDefaultOptions
})
.addPreset('alert', {
Expand Down Expand Up @@ -567,6 +606,7 @@ function MdDialogProvider($$interimElementProvider) {
clickOutsideToClose: false,
escapeToClose: true,
targetEvent: null,
contentElement: null,
closeTo: null,
openFrom: null,
focusOnOpen: true,
Expand Down Expand Up @@ -613,6 +653,29 @@ function MdDialogProvider($$interimElementProvider) {
function onShow(scope, element, options, controller) {
angular.element($document[0].body).addClass('md-dialog-is-showing');

if (options.contentElement) {
var contentEl = options.contentElement;

if (angular.isString(contentEl)) {
contentEl = document.querySelector(contentEl);
options.elementInsertionSibling = contentEl.nextElementSibling;
options.elementInsertionParent = contentEl.parentNode;
} else {
contentEl = contentEl[0] || contentEl;
// When the element is not visible in the DOM, then we can treat is as same
// as a normal dialog would do. Removing it at close etc.
// ---
// When the element is visible in the DOM, then we restore it at close of the dialog.
if (document.contains(contentEl)) {
options.elementInsertionSibling = contentEl.nextElementSibling;
options.elementInsertionParent = contentEl.parentNode;
}
}

options.elementInsertionEntry = contentEl;
element = angular.element(contentEl);
}

captureParentAndFromToElements(options);
configureAria(element.find('md-dialog'), options);
showBackdrop(scope, element, options);
Expand Down Expand Up @@ -690,12 +753,37 @@ function MdDialogProvider($$interimElementProvider) {
return dialogPopOut(element, options);
}

function removeContentElement() {
if (!options.contentElement) return;

options.reverseContainerStretch();

if (!options.elementInsertionParent) {
// When the contentElement has no parent, then it's a virtual DOM element, which should
// be removed at close, as same as normal templates inside of a dialog.
options.elementInsertionEntry.parentNode.removeChild(options.elementInsertionEntry);
} else if (!options.elementInsertionSibling) {
// When the contentElement doesn't have any sibling, then it can be simply appended to the
// parent, because it plays no role, which index it had before.
options.elementInsertionParent.appendChild(options.elementInsertionEntry);
} else {
// When the contentElement has a sibling, which marks the previous position of the contentElement
// in the DOM, we insert it correctly before the sibling, to have the same index as before.
options.elementInsertionParent.insertBefore(options.elementInsertionEntry, options.elementInsertionSibling);
}
}

/**
* Detach the element
*/
function detachAndClean() {
angular.element($document[0].body).removeClass('md-dialog-is-showing');
element.remove();
// Only remove the element, if it's not provided through the contentElement option.
if (!options.contentElement) {
element.remove();
} else {
removeContentElement();
}

if (!options.$destroy) options.origin.focus();
}
Expand Down Expand Up @@ -764,7 +852,7 @@ function MdDialogProvider($$interimElementProvider) {
*/
function activateListeners(element, options) {
var window = angular.element($window);
var onWindowResize = $mdUtil.debounce(function(){
var onWindowResize = $mdUtil.debounce(function() {
stretchDialogContainerToViewport(element, options);
}, 60);

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

var previousStyles = {
top: container.css('top'),
height: container.css('height')
};

container.css({
top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
height: height ? height + 'px' : '100%'
});

return container;
return function() {
// Reverts the modified styles back to the previous values.
// This is needed for contentElements, which should have the same styles after close
// as before.
container.css(previousStyles);
};
}

/**
Expand All @@ -1007,7 +1105,7 @@ function MdDialogProvider($$interimElementProvider) {
function dialogPopIn(container, options) {
// Add the `md-dialog-container` to the DOM
options.parent.append(container);
stretchDialogContainerToViewport(container, options);
options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);

var dialogEl = container.find('md-dialog');
var animator = $mdUtil.dom.animator;
Expand All @@ -1023,7 +1121,7 @@ function MdDialogProvider($$interimElementProvider) {
return animator
.translate3d(dialogEl, from, to, translateOptions)
.then(function(animateReversal) {
// Build a reversal translate function synched to this translation...
// Build a reversal translate function synced to this translation...
options.reverseAnimate = function() {
delete options.reverseAnimate;

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

return animateReversal(
animator.toTransformCss(
to = animator.toTransformCss(
// in case the origin element has moved or is hidden,
// let's recalculate the translateCSS
buildTranslateToOrigin(dialogEl, options.origin)
)
);

};

// Builds a function, which clears the animations / transforms of the dialog element.
// Required for contentElements, which should not have the the animation styling after
// the dialog is closed.
options.clearAnimate = function() {
delete options.clearAnimate;
return animator
.translate3d(dialogEl, to, animator.toTransformCss(''), {});
};

return true;
});
}
Expand All @@ -1054,7 +1162,13 @@ function MdDialogProvider($$interimElementProvider) {
* Dialog close and pop-out animation
*/
function dialogPopOut(container, options) {
return options.reverseAnimate();
return options.reverseAnimate().then(function() {
if (options.contentElement) {
// When we use a contentElement, we want the element to be the same as before.
// That means, that we have to clear all the animation properties, like transform.
options.clearAnimate();
}
});
}

/**
Expand Down
95 changes: 95 additions & 0 deletions src/components/dialog/dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,101 @@ describe('$mdDialog', function() {
expect(parent[0].querySelectorAll('md-dialog.two').length).toBe(0);
}));

describe('contentElement', function() {
var $mdDialog, $rootScope, $compile, $timeout;

beforeEach(inject(function($injector) {
$mdDialog = $injector.get('$mdDialog');
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
$timeout = $injector.get('$timeout');
}));

it('should correctly move the contentElement', function() {
var contentElement = $compile(
'<div class="md-dialog-container">' +
'<md-dialog>Dialog</md-dialog>' +
'</div>'
)($rootScope);
var parentEl = angular.element('<div>');

// Add the contentElement to the DOM.
document.body.appendChild(contentElement[0]);

$mdDialog.show({
contentElement: contentElement,
parent: parentEl,
escapeToClose: true
});

$rootScope.$apply();
runAnimation();

expect(contentElement[0].parentNode).toBe(parentEl[0]);

$mdDialog.hide();
runAnimation();

expect(contentElement[0].parentNode).toBe(document.body);

document.body.removeChild(contentElement[0]);
});

it('should correctly query for a contentElement', function() {
var contentElement = $compile(
'<div class="md-dialog-container" id="myId">' +
'<md-dialog>Dialog</md-dialog>' +
'</div>'
)($rootScope);
var parentEl = angular.element('<div>');

// Add the contentElement to the DOM.
document.body.appendChild(contentElement[0]);

$mdDialog.show({
contentElement: '#myId',
parent: parentEl,
escapeToClose: true
});

$rootScope.$apply();
runAnimation();

expect(contentElement[0].parentNode).toBe(parentEl[0]);

$mdDialog.hide();
runAnimation();

expect(contentElement[0].parentNode).toBe(document.body);

document.body.removeChild(contentElement[0]);
});

it('should also work with a virtual pre-compiled element', function() {
var contentElement = $compile(
'<div class="md-dialog-container" id="myId">' +
'<md-dialog>Dialog</md-dialog>' +
'</div>'
)($rootScope);
var parentEl = angular.element('<div>');

$mdDialog.show({
contentElement: contentElement,
parent: parentEl,
escapeToClose: true
});

$rootScope.$apply();
runAnimation();

expect(contentElement[0].parentNode).toBe(parentEl[0]);

$mdDialog.hide();
runAnimation();
});

});

it('should have the dialog role', inject(function($mdDialog, $rootScope) {
var template = '<md-dialog>Hello</md-dialog>';
var parent = angular.element('<div>');
Expand Down

0 comments on commit 135cb3a

Please sign in to comment.