now-confirm-dialog an OpenSource element

We needed a means to produce a “pretty” confirm dialog instead of the old and busted default JavaScript confirm dialog. While the JavaScript confirm dialog is functional and probably provides more functionality than this one, we’ve found this meets our needs quite nicely. For reference, here is a default JavaScript confirm dialog. This shows up centered right under the address bar:

Here is our our now-confirm-dialog which shows up in the center of the screen:

The goal of this element is to provide a simple confirmation dialog and then have the ability to do something based on which button the user clicked. It also needs to use Polymer 2.x.x in order to be used within the redpill-zion project. Pretty simple eh. So, lets dive right in.

First off we’ve got the HTML. We use the paper-dialog to drive this which has ok support. The biggest issue with paper-dialog is that it has problems determining it’s stacking order for a modal dialog. If you use the app-layout elements sometimes the dialog will appear underneath it’s mask. To me, this is kind-of unacceptable that you need to hack this to get it to work. But the solution is a problem that XPage developers should be familiar with. You need to move the dialog outside of the app-layout elements, usually as a child of the body tag (dojo used to do this with it’s dialogs). Also, it still depends on neon-animations which were deprecated in Polymer 2.x.x. This problem doesn’t affect the functionality of the dialog but does produce errors in the console.

<link rel="import" href="../polymer/polymer-element.html">

<link rel="import" href="../paper-button/paper-button.html">
<link rel="import" href="../paper-dialog/paper-dialog.html">
<!--
  neon-animations have been deprecated. However they are still present as a dependency of
  paper-dialog. If you define animation properties (i.e. entry-animation, exit-animation)
  then an error is thrown when an animation is attempted to be played. There doesn't currently
  seem to be a workaround (1/15/2018).
<link rel="import" href="../neon-animation/animations/scale-up-animation.html">
<link rel="import" href="../neon-animation/animations/fade-out-animation.html">
-->

<!--
`now-confirm-dialog`

Confirmation dialog for use in Red Pill Now applications

@author Keith Strickland
@demo demo/index.html
-->
<dom-module id="now-confirm-dialog">
  <template>
    <style>
      :host {
        display: block;
        box-sizing: border-box;
        max-width: 450px;

        --paper-dialog: {
          max-width: 450px;
        };

        @apply --now-confirm-dialog;
      }
      h2 {
        @apply --now-confirm-dialog-header;
      }
      .cancelButton {
        background: var(--now-confirm-dialog-cancel-button-background, #c12e2a);
        color: var(--now-confirm-dialog-cancel-button-color, white);
      }
      .confirmButton {
        background: var(--now-confirm-dialog-confirm-button-background, #419641);
        color: var(--now-confirm-dialog-confirm-button-color, white);
      }
      .dialogText {
        padding: 15px;
        margin-top: 0;
      }
      .dialogText.preText {
        white-space: pre;
      }
    </style>

    <paper-dialog
      id="dialog"
      entry-animation="scale-up-animation"
      exit-animation="fade-out-animation"
      modal>
      <h2>{{dialogTitle}}</h2>
      <!-- Whitespace is rendered which is why the dialogText is not indented -->
      <div id="dialogText" class="dialogText">
{{dialogText}}
      </div>
      <div class="buttons">
        <paper-button
          class="cancelButton"
          hidden$="{{noCancelButton}}"
          dialog-dismiss>
          {{cancelButtonText}}
        </paper-button>
        <paper-button
          class="confirmButton"
          dialog-confirm
          autofocus>
          {{confirmButtonText}}
        </paper-button>
      </div>
    </paper-dialog>

  </template>
  <script type="text/javascript" src="now-confirm-dialog.js"></script>
</dom-module>

Really the there are only a few things of interest here and that’s the dialogTitle property (line 61), the dialogText property (line 64), the cancelButtonText property (line 69) and the confirmButtonText (line 71). These properties allow us to configure exactly what will be displayed in the dialog. Also, lines 34-45 I’ve defined some custom style properties so we can style this dialog to meet our branding needs.

Next up is the code for this element. We’ll start with the properties:

static get properties() {
  return {
    dialogTitle: String,
    dialogText: {
      type: String,
      observer: '_onDialogTextChange'
    },
    confirmButtonText: {
      type: String,
      value: 'OK'
    },
    cancelButtonText: {
      type: String,
      value: 'Cancel'
    },
    confirmButtonBackground: {
      type: String,
      observer: '_onConfirmBackgroundChange'
    },
    confirmButtonColor: {
      type: String,
      observer: '_onConfirmColorChange'
    },
    cancelButtonBackground: {
      type: String,
      observer: '_onCancelBackgroundChange'
    },
    cancelButtonColor: {
      type: String,
      observer: '_onCancelColorChange'
    },
    noCancelButton: {
      type: Boolean,
      value: false,
      reflectToAttribute: true
    },
    _cancelCallback: Function,
    _confirmCallback: Function,
    targetMoveCssSelector: String,
  };
}

_closeDialogListener: any;
connectedCallback() {
  super.connectedCallback();
  this._closeDialogListener = this._onDialogClosed.bind(this);
  this.$.dialog.addEventListener('iron-overlay-closed', this._closeDialogListener);
}
disconnectedCallback() {
  this.$.dialog.removeEventListener('iron-overlay-closed', this._closeDialogListener);
}
/**
 * Fired when the dialog closes. Fires dig-confirm-canceled and dig-confirm-confirmed. Also
 * runs any cancelCallback or confirmCallback methods
 * @param  {Event} evt    The event object
 * @param  {Object} detail The detail object
 * @listens #dialog.iron-overlay-closed
 */
private _onDialogClosed(evt: CustomEvent) {
  let detail = evt.detail;
  if (!detail.confirmed) {
    if (this.get('_cancelCallback')) {
      this.get('_cancelCallback').call(this);
    }
    let evt = new CustomEvent('now-confirm-canceled');
    this.dispatchEvent(evt);
  }else if (detail.confirmed) {
    if (this.get('_confirmCallback')) {
      this.get('_confirmCallback').call(this);
    }
    let evt = new CustomEvent('now-confirm-confirmed');
    this.dispatchEvent(evt);
  }
}
/**
 * Opens the dialog and sets the cancel and confirm callbacks
 * @param  {Function} confirmCallback Function to run when the dialog is confirmed
 * @param  {Function} cancelCallback  Function to run when the dialog is cancelled
 */
open(confirmCallback, cancelCallback) {
  // WARNING!!! TODO:
  // Moving this as a child of a top level element, outside of the app-*-layout element
  // is to fix this bug https://github.com/PolymerElements/paper-dialog/issues/7
  // which is kind of ridiculus for a production ready platform.
  // Luckily there really is no data binding with this component or that too
  // would be broken.
  if (this.get('targetMoveCssSelector')) {
    let elem = document.querySelector(this.get('targetMoveCssSelector'));
    if (elem) {
      elem.appendChild(this);
    }
  }
  (<any>this.$.dialog).open();
  this.set('_cancelCallback', cancelCallback);
  this.set('_confirmCallback', confirmCallback);
}
/**
 * Closes the dialog. Will cause the cancel callback to be run
 * in the iron-overlay-closed event handler
 */
close() {
  (<any>this.$.dialog).close();
  if (this.get('targetMoveCssSelector')) {
    let movedDialog = document.querySelector(this.get('targetMoveCssSelector') + ' > now-confirm-dialog');
    if (movedDialog) {
      movedDialog.parentNode.removeChild(movedDialog);
    }
  }
}

The code above isn’t complete, but does contain all the relevant parts of this dialog. But notice we have pretty much all of the properties identified above including the custom styles for button background and text colors. The things of interest here are the _cancelCallback and _confirmCallback properties. One of the cool features of JavaScript is the ability to pass around a function as a variable. So here we’re taking advantage of that and these properties will hold the callback functions that will be ran when the confirm or cancel button is clicked.

Other things of interest here is the connectedCallback where we setup a listener (_onDialogClosed) for the iron-overlay-closed event which is a CustomEvent (contains a detail property). This event is fired when the paper-dialog closes. In the detail of that event is a boolean property called confirmed. This will be true if the confirm button is clicked and false if the cancel button is clicked. This allows us to determine which callback to run in the _onDialogClosed function. If confirmed is true, run the _confirmCallback function, if confirmed is false, run the _cancelCallback function.

Also above, is the open and close functions. These are called programmatically and start off the process. The open function accepts 2 parameters, our _confirmCallback and _cancelCallback. Calling open will move the now-confirm-dialog to a higher level element (preferably outside the app-layout element), open the paper-dialog and set the _confirmCallback and _cancelCallback properties.

The close function will close the paper-dialog which in turn causes the iron-overlay-closed event to fire with the confirmed property set to false.

And that’s it really. If you’re interested you can find the code on GitHub in Red Pill Now‘s repository space. Until next time…. happy coding.

Share This: