Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle Rails flash messages after AJAX calls using ReactJS

Several posts describe displaying flash messages after AJAX calls (for instance, Rails doesn't display flash messages after ajax call and How do you handle Rail's flash with Ajax requests?).

I'm using ReactJS with react-rails and want to do this, and want to take advantage of React's dynamic display rendering.

So I'm answering my own question of how to render flash messages after AJAX calls when using ReactJS below as a way to answer others who might encounter this and get any advice for improving it.

like image 230
rdnewman Avatar asked Mar 18 '23 21:03

rdnewman


1 Answers

This implementation is based largely on the other SO posts on this subject (see references in question). The code assumes Rails 4.x with the asset pipeline, HAML for the views, Bootstrap for styling, and use of the react-rails gem.

(update: a slightly longer discussion can be found at my blog).

Layout
Here's the app/views/layouts/application.html.haml file:

!!!
%html
  %head
    %title
      Demo

    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true
    = javascript_include_tag 'application', 'data-turbolinks-track' => true
    = csrf_meta_tags

  %body
    %div.container
      %div#flash_messages
      = yield

Note that the div has an id of #flash_messages. The placement of this container div determines where the flash message appear.

Controller
The private routine below simply converts the native array-of-arrays uses to be parseable JSON in the header.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  after_filter :flash_to_http_header

private
  def flash_to_http_header
    return unless request.xhr?
    return if flash.empty?
    response.headers['X-FlashMessages'] = flash.to_hash.to_json
    flash.discard  # don't want the flash to appear when you reload page
  end
end

Javascript (ReactJS)
The following javascript is made up of a React class, a global function to process flash messages out of the response header, and a JQuery handler for completion of AJAX calls. It's put into a single JX file (I used app/assets/javascripts/react/flash_messages.js.jsx) to keep the relevant JS functionality together. I discuss this more below.

/** @jsx React.DOM */

var FlashMessages = React.createClass({
  getInitialState: function() {
    return {messages: this.props.messages};
  },

  messages: function (messageArray) {
    this.replaceState({messages: messageArray});
  },

  render: function() {
    return (
      <div className='flash_messages_component'>
        {this.state.messages.map(function(message, index) {
          _level = message[0];
          _text  = message[1];
          return (
            <div key={index} className={this._flash_class(_level)}>
              {_text}
            </div>
          );
        }.bind(this))}
      </div>
    )
  },

  _flash_class: function(level) {
    var _result = 'alert alert-error';
    if (level === 'notice') {
      _result = 'alert alert-info';
    } else if (level === 'success') {
      _result = 'alert alert-success';
    } else if (level === 'error') {
      _result = 'alert alert-error';
    } else if (level === 'alert') {
      _result = 'alert alert-error';
    }
    return _result;
  }

});

function handleFlashMessagesHeader(node, xhr) {
  var _message_array = new Array();
  var _raw_messages = xhr.getResponseHeader("X-FlashMessages")
  if (_raw_messages) {
    var _json_messages = JSON.parse(_raw_messages);
    count = 0
    for (var key in _json_messages) {
      _message_array[count] = new Array();
      _message_array[count][0] = key;
      _message_array[count][1] = _json_messages[key];
      count += 1;
    }
  }
  node.messages(_message_array);
}

$(document).ready(function() {
  var dummy = new Array();
  var flashDiv = React.render(<FlashMessages messages={dummy} />, $('#flash_messages')[0]);

  $(document).ajaxComplete(function(event, xhr, settings) {
    handleFlashMessagesHeader(flashDiv, xhr);
  });
});

The FlashMessages React class does a few things. First, it moves the props into state. Generally this would be an anti-pattern, but doing so enables non-React code to trigger changes when needed. The messages function is that trigger and is meant to be called by external JS code. The main processing for render assumes the Rails flash-native array-of-2-element-arrays data structure to keep processing to a minimum and allow the component to be used directly from a view instead of just AJAX calls. Finally, the local _flash_class method supports Bootstrap styling (which of course could be adjusted for other styling as desired).

The handleFlashMessagesHeader is a global function for converting the JSON back to an array-of-arrays done by the Rails controller filter method. Note that it takes the DOM element with the id marker from the Rails app view/layout.

The last section is meant to be run on page load and so depends on JQuery ready. The React.render (formally React.renderComponent) is saved to a global variable to allow direct calling into the FlashMessages object's message method (the dummy array used is just to seed an empty flashes array when first invoked -- this could probably be handled within the React class too by a null test). Whenever ajaxComplete is triggered, the handleFlashMessageHeader function is invoked and passed the React object for updating.

Non-AJAX
Because the React class assumes native array-of-arrays, the view could have simply called a

= react-component 'FlashMessages', flash, :div

instead to display it, but then of course it's of less use since dynamic response isn't supported. However, the ability to invoke it either way gives more flexibility for some use cases.

like image 88
rdnewman Avatar answered Mar 26 '23 01:03

rdnewman