Popup, not to be confused with Tether, is a modal element that does not tether with any other element. Common usage of a Popup would be a modal dialog or modal status display such as a loading indicator.
Getting Started
See Developing Modals in Invision for more extensive documentation and an example with complete code.
To declare any Popups, the most common setup will involve the following:
- Add a
<inv-popup-vault></inv-popup-vault>
directive to your component's template if it doesn't already exist. - Wrap your popup's markup in an
<inv-dialog></inv-dialog>
directive, integrating theunsaved-changes-prompt
directive when appropriate. The popup's contents isn't required to be within the dialog component, but that's a very common use case. - Wrap that dialog or comparable markup, including a
<inv-loading-indicator></inv-loading-indicator>
directive when appropriate, in a<div>
with theinv-popup
directive attached, which will register the DOM element with the PopupManager. - Insert this markup within the
<inv-popup-vault></inv-popup-vault>
so the popup will be hidden and any related$scope.$watch
statements are disabled while it is at rest.
Nearly all instances of the popup component will expect the following bindings:
config
: Theconfig
object should be a reference to the PopupInstanceApi returned from the PopupManager when the popup is registered. This object is rarely used by the popup directly, but does provide access to thecenter
andclose
functions directly if they are needed for some reason. These should be used sparingly, as it's preferred that the parent component be the one to manage these types of concerns whenever possible.onClose
: TheonClose
function is called when the component's "Cancel" or "Close" buttons are clicked.onSuccess
: TheonSuccess
function will be called by the component once it has successfully been "submitted" and the API does not result in a fault code being passed back. This callback often times passes the payload of the submitted action back to the parent component via this callback in case the parent requires it (e.g. the payload may contain the ID of the newly created entity).
If the popup instance contains a form, it should also integrate the Unsaved Changes feature. In most cases, use of the unsaved-changes-prompt
directive on the form element can be done with very little extra effort.
Each popup instance is responsible for managing a proper lifecycle, including cleaning up after itself. This is accomplished through the use of a ng-if
directive on your popup. For example:
<my-modal-popup ng-if="parentComponentController.shouldShowPopup" config="parentComponentController.popupConfig" on-close="parentComponentController.onPopupClose" on-success="parentComponentController.onPopupComplete"> </my-modal-popup>
Using the ng-if
causes the automatic registration/de-registration of the Unsaved Changes feature when the component's $onDestroy
and $onInit
methods are called. If the unsaved-changes-prompt
directive is not used on the form element, ensure the component is properly de-registered using the unregisterUnsavedChanges
action from Invision Core before closing the popup. Otherwise the next time you try to navigate to a new route within the application you will see the Unsaved Changes popup.
Usage of $timeout
in the parent component is important. This will wait for a digest cycle before evaluating the contained function, and in almost all cases this is important as we wait to open/close a modal until its contents have been properly disposed of. Without this, the Unsaved Changes prompt may be triggered from the popup instance, for example.
- The single digest cycle caused by using
$timeout
before opening the popup works for a large majority of use cases. If the popup instance is responsible for rendering content that takes longer than a single digest cycle to prepare and render (e.g. larger data tables), contact Tangent Lin for best practices that the Reporting module has used for these situations. - Similarly, if the popup does not have a definite height or takes more than one digest cycle to render, there may be issues with the
ng-if
approach. When the PopupManager attempts to open and center the popup, and the contents aren't ready, it may result in it being positioned off-center when fully rendered. Contact Tangent Lin for an alternate pattern that can cleanly address this without usingng-if
.
Best Practices
Use separate configurations if there are multiple popups on the same page. For example:
this.popupConfigX = { onRegisterApi: () => { } }; this.popupConfigY = { onRegisterApi: () => { } };
Within the controller of the popup, don't override or decorate the functions of the popup instance API. For example:
class ParentComponentController { constructor($ngRedux, $timeout) { // ... } $onInit() { // ... // BAD const onRegisterApi = this.popupConfig.onRegisterApi; this.popupConfig = { onRegisterApi = ({api: api}) => { const popupApi = Object.assign({}, api, { open: () => { this.resetForm(); $timeout(() => { api.open(); }); } }); this.popupApi = popupApi; onRegisterApi && onRegisterApi({api: popupApi}); } }; // GOOD this.popupConfig = { onRegisterApi: ({api: api}) => { this.popupApi = api; } }; } $onDestroy() { // ... } resetForm() { // ... } showPopup() { // Instead of overwriting open() in the API, externalize it to achieve the same result this.resetForm(); // NOTE: resetForm() would require a digest cycle, therefore using $timeout would guarantee the form gets reset this.$timeout(() => { this.popupApi.open(); }); } }
If the popup contains a form, then the ENTER
key should submit the form and the ESC
key should close the popup. For example:
class ModalComponentController { constructor($ngRedux, hotkeys) { // ... } $onInit() { // ... hotkeys .add({ callback: (event) => { event.preventDefault(); this.handleFormSubmit(); } combo: 'enter' }) .add({ callback: (event) => { event.preventDefault(); this.handleClose(); } combo: 'esc' }); } $onDestroy() { // ... this.hotkeys.del('enter'); this.hotkeys.del('esc'); } handleClose() { // ... } handleFormSubmit() { // ... } }