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.

Are you or your company looking for React.js developers? - your job listing can be here and reach thousands of programmers within a month.

Getting started

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

Modal window

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.

var Modal = React.createClass({
  displayName: 'Modal',

  backdrop: function() {
    return <div className='modal-backdrop in' />;
  },

  modal: function() {
    var style = {display: 'block'};
    return (
      <div
        className='modal in'
        tabIndex='-1'
        role='dialog'
        aria-hidden='false'
        ref='modal'
        style={style}
      >
        <div className='modal-dialog'>
          <div className='modal-content'>
            {this.props.children}
          </div>
        </div>
      </div>
    );
  },

  render: function() {
    return (
      <div>
        {this.backdrop()}
        {this.modal()}
      </div>
    );
  }
});

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.

var Confirm = React.createClass({
  displayName: 'Confirm',

  getDefaultProps: function() {
    return {
      confirmLabel: 'OK',
      abortLabel: 'Cancel'
    };
  },

  abort: function() {
    return this.promise.reject();
  },

  confirm: function() {
    return this.promise.resolve();
  },

  componentDidMount: function() {
    this.promise = new $.Deferred();
    return React.findDOMNode(this.refs.confirm).focus();
  },

  render: function() {
    var modalBody;
    if (this.props.description) {
      modalBody = (
        <div className='modal-body'>
          {this.props.description}
        </div>
      );
    }

    return (
      <Modal>
        <div className='modal-header'>
          <h4 className='modal-title'>
            {this.props.message}
          </h4>
        </div>
        {modalBody}
        <div className='modal-footer'>
          <div className='text-right'>
            <button
              role='abort'
              type='button'
              className='btn btn-default'
              onClick={this.abort}
            >
              {this.props.abortLabel}
            </button>
            {' '}
            <button
              role='confirm'
              type='button'
              className='btn btn-primary'
              ref='confirm'
              onClick={this.confirm}
            >
              {this.props.confirmLabel}
            </button>
          </div>
        </div>
      </Modal>
    );
  }
});

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.

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.

var confirm = function(message, options) {
  var cleanup, component, props, wrapper;
  if (options == null) {
    options = {};
  }
  props = $.extend({
    message: message
  }, options);
  wrapper = document.body.appendChild(document.createElement('div'));
  component = React.render(<Confirm {...props}/>, wrapper);
  cleanup = function() {
    React.unmountComponentAtNode(wrapper);
    return setTimeout(function() {
      return wrapper.remove();
    });
  };
  return 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(function() {
  return handleConfirmed();
})
.fail(function() {
  return 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(function() {
  return handleConfirmed();
})
.fail(function() {
  return 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.

Or if you are interested in React in your Rails application, take a look at React meets Rails book we have written.

comments powered by Disqus