Beautiful confirm window with React

… and check why 5600+ Rails engineers read also this

Beautiful confirm window with React

When designing your web application, you would like the user to confirm some actions sometimes. For example, you may want the user to confirm deletion of his data. There is window.confirm JavaScript method that might be useful in this case but it could not be styled and just displays native browser’s dialog window. In this article I would like to show you how to create React component as a replacement for window.confirm that can have similar behaviour and your application’s look & feel. It has similar API to window.confirm so migration should be really easy.

Getting started

In this article I am using the latest React (0.13.2) and Bootstrap v3.3.4 for styling modal window. I am also using jQuery promises to handle confirm and abort actions.

Let’s start with creating React component for Bootstrap-styled modal window. We will use that component later in confirm window implementation. We will create modal with backdrop and lock the whole UI under backdrop until the user clicks on confirm action button.

{div} = React.DOM

Modal = React.createClass
  displayName: 'Modal'

  backdrop: ->
    div
      className: 'modal-backdrop in'

  modal: ->
    div
      className: 'modal in'
      tabIndex: -1
      role: 'dialog'
      'aria-hidden': false
      ref: 'modal'
      style:
        display: 'block'
      div
        className: 'modal-dialog'
        div
          className: 'modal-content'
          @props.children

  render: ->
    div null,
      @backdrop()
      @modal()

The division with modal-backdrop will be used to cover and lock everything on the page. We would not close modal on backdrop click in this case.

Confirm modal

Now it’s time to implement the confirm dialog component. It will use Modal component created in previous step. We will add title, two buttons (confirm and abort) and optional descriptive text.

Promise = $.Deferred
{div, button, h4} = React.DOM

Confirm = React.createClass
  displayName: 'Confirm'

  getDefaultProps: ->
    confirmLabel: 'OK'
    abortLabel: 'Cancel'

  abort: ->
    @promise.reject()

  confirm: ->
    @promise.resolve()

  componentDidMount: ->
    @promise = new Promise()
    React.findDOMNode(@refs.confirm).focus()

  render: ->
    React.createElement Modal, null,
      div
        className: 'modal-header'
        h4 className: 'modal-title', @props.message
      if @props.description
        div
          className: 'modal-body'
          @props.description
      div
        className: 'modal-footer'
        div
          className: 'text-right'
          button
            role: 'abort'
            type: 'button'
            className: 'btn btn-default'
            onClick: @abort
            @props.abortLabel
          ' '
          button
            role: 'confirm'
            type: 'button'
            className: 'btn btn-primary'
            ref: 'confirm'
            onClick: @confirm
            @props.confirmLabel

We are using promises in confirm and abort methods. If you are not familiar with the concept of promises, I recommend you read our beginners guide to jQuery Deferred and Promises. In short, using promises would allow us to asynchronously decide what code should be called after clicking confirm or abort button in our dialog window.

You can also notice we are using componentDidMount lifecycle method. This method is called right after the component was mounted (its representation was added to the DOM tree). We are creating a promise object in that method - you may not be familiar with using instance variables instead of state in react components. Since that promise has no effect on the rendering of our component, it should not be placed in state, because adding it to state would cause unnecessary calls of render method. There is also one more line in componentDidMount - React.findDOMNode(@refs.confirm).focus(). We are using it for better UX, similar to the native window.confirm behaviour, so you can just press Enter when confirm dialog appears. You can also easily extend this component to enable aborting dialog when pressing Escape.

If you would like to know more about using React especially in your Rails application, take a look at React meets Rails book we have written.

Making it work

We have created modal and confirm dialog components. Now it’s time to make it work. We will create a method that will render our confirm dialog and return a promise. Once the promise is resolved or rejected, the dialog will be unmounted from DOM.

confirm = (message, options = {}) ->
  props = $.extend({message: message}, options)
  wrapper = document.body.appendChild(document.createElement('div'))
  component = React.render(React.createElement(Confirm, props), wrapper)
  cleanup = ->
    React.unmountComponentAtNode(wrapper)
    setTimeout -> wrapper.remove()
  component.promise.always(cleanup).promise()

When resolving or rejecting a promise, we are unmounting the whole Confirm component to cleanup the DOM (I prefer removing nodes from DOM than just hiding them via CSS). We are also removing the wrapper node since it’s not needed anymore after the dialog is closed - each time we call the confirm method, new wrapper node would be created and added to DOM (you may also create a confirm target node upfront - then you would only need to mount and unmount the component in confirm).

OK, we have now all parts of our window.confirm replacement. How to use it in your code? Very simple, you should only change your conditional:

if confirm('Are you sure?')
  handleConfirmed()
else
  handleAborted()

that would produce something like this:

to promise version:

confirm('Are you sure?')
  .then -> handleConfirmed()
  .fail -> handleAborted()

that looks like this:

But since we have a React component, you can add more descriptive information to your confirm dialog or change button labels (with window.confirm you can only set description, the dialog title is generated by your browser):

confirm(
  'Are you sure?',
  description: 'Would you like to remove this item from the list?',
  confirmLabel: 'Yes',
  abortLabel: 'No'
)
  .then -> handleConfirmed()
  .fail -> handleAborted()

This is the final look:

Summary

Replacing native window.confirm with custom solution gives you ability to have the same behaviour but without restrictions - you can have beautifully styled dialog with custom button labels or dialog title. You can grab the demo on jsfiddle.

You might also like