How do you handle Rail's flash with Ajax requests?

I'm pretty happy with the solution that I came up with. Basically, I have a helper method that reloads the flash inline, and then I have an after_filter that clear out the flash if the request is xhr. Does anyone have a simpler solution than that?

Update: The solution above was written back in Rails 1.x and is no longer supported.

50014 次浏览

The only improvement I can think of is making the page.reload_flash default (not having to put it on every rjs file, and make it expicit if you don't want to reload the flash, something like page.keep_flash.

I wouldn't know where to start but knowing some rails I'm sure it's not that hard.

Looks like what you need is flash.now[:notice], which is only available in the current action and not in the next. You can take a look at the documentation here: http://api.rubyonrails.com/classes/ActionController/Flash/FlashHash.html#M000327

This is needed in the js response

If you are using RSJ:

page.replace_html :notice, flash[:notice]
flash.discard

If you are using jQuery:

$("#flash_notice").html(<%=escape_javascript(flash.delete(:notice)) %>');

Another way would be update/display the "notice" div with the message from the your Ajax requests "OnFailure" handler. It gives you the ability to show these flash messages with required effect. I used this

render :text => "Some error happened", :status => 444

in the Javascript

new AjaxRequest(...


,
OnFailure:function(transport) {
$("#notice").update(transport.responseText);
// show the message
}


);


HTH

You can also store the flash messages in the response headers using a after_filter block and display them using javascript:

class ApplicationController < ActionController::Base
after_filter :flash_to_headers


def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash[:error]  unless flash[:error].blank?
# repeat for other flash types...


flash.discard  # don't want the flash to appear when you reload page
end

And in application.js add a global ajax handler. For jquery do something like this:

$(document).ajaxError(function(event, request) {
var msg = request.getResponseHeader('X-Message');
if (msg) alert(msg);
});

Replace alert() with your own javascript flash function or try jGrowl.

Based on gudleik answer:

class ApplicationController < ActionController::Base
after_filter :flash_to_headers


def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash_message
response.headers["X-Message-Type"] = flash_type


flash.discard # don't want the flash to appear when you reload page
end


private


def flash_message
[:error, :warning, :notice].each do |type|
return flash[type] unless flash[type].blank?
end
end


def flash_type
[:error, :warning, :notice].each do |type|
return type unless flash[type].blank?
end
end

Then on your application.js (if you're using Rails native Prototype helpers) add:

Ajax.Responders.register({
onComplete: function(event, request) {
var msg = request.getResponseHeader('X-Message');
var type = request.getResponseHeader('X-Message-Type');
showAjaxMessage(msg, type); //use whatever popup, notification or whatever plugin you want
}
});

Assign the message in the controller like this:

  flash.now[:notice] = 'Your message'

app/views/layouts/application.js.erb - Layout for Ajax Requests. Here you can simply use

  <%= yield %>
alert('<%= escape_javascript(flash.now[:notice]) %>');

or with some rich animations using gritter: http://boedesign.com/demos/gritter/

  <%= yield %>
<% if flash.now[:notice] %>
$.gritter.add({
title: '--',
text: '<%= escape_javascript(flash.now[:notice]) %>'
});
<% end %>

Building on top of others -

(We pass the complete flash object as JSON, enabling us to reconstruct the complete flash object in the browser. This can be used to ensure that all flash messages are displayed in case multiple flash messages are generated by Rails.)

#application_controller.rb
class ApplicationController < ActionController::Base
after_filter :flash_to_headers


def flash_to_headers
if request.xhr?
#avoiding XSS injections via flash
flash_json = Hash[flash.map{|k,v| [k,ERB::Util.h(v)] }].to_json
response.headers['X-Flash-Messages'] = flash_json
flash.discard
end
end
end
//application.js
$(document).ajaxComplete(function(event, request){
var flash = $.parseJSON(request.getResponseHeader('X-Flash-Messages'));
if(!flash) return;
if(flash.notice) { /* code to display the 'notice' flash */ $('.flash.notice').html(flash.notice); }
if(flash.error) { /* code to display the 'error' flash */ alert(flash.error); }
//so forth
}

I did it this way..

controller:

respond_to do |format|
flash.now[:notice] = @msg / 'blah blah...'
format.html
format.js
end

view:

<div id='notice'>
<%= render :partial => 'layouts/flash' , :locals => { :flash => flash } %>
</div>

layouts/_flash.html.erb

<% flash.each do |name, msg| %>
<div class="alert-message info">
<a class="close dismiss" href="#">x</a>
<p><%= msg %></p>
</div>
<% end %>

post.js.erb

$("#notice").html("<%= escape_javascript(render :partial => 'layouts/flash' , :locals => { :flash => flash }).html_safe %>");

There is a gem called Unobtrusive Flash that automatically encodes flash messages into a cookie. A javascript at client end checks for flash and display it in whatever way you want. This works seamlessly in both normal and ajax requests.

And here is my version based on @emzero, with modifications to work with jQuery, tested on Rails 3.2

application_controller.rb

class ApplicationController < ActionController::Base
protect_from_forgery


after_filter :flash_to_headers


def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash_message
response.headers["X-Message-Type"] = flash_type.to_s


flash.discard # don't want the flash to appear when you reload page
end


private


def flash_message
[:error, :warning, :notice].each do |type|
return flash[type] unless flash[type].blank?
end
end


def flash_type
[:error, :warning, :notice].each do |type|
return type unless flash[type].blank?
end
end
end

application.js

// FLASH NOTICE ANIMATION
var fade_flash = function() {
$("#flash_notice").delay(5000).fadeOut("slow");
$("#flash_alert").delay(5000).fadeOut("slow");
$("#flash_error").delay(5000).fadeOut("slow");
};
fade_flash();


var show_ajax_message = function(msg, type) {
$("#flash-message").html('<div id="flash_'+type+'">'+msg+'</div>');
fade_flash();
};


$(document).ajaxComplete(function(event, request) {
var msg = request.getResponseHeader('X-Message');
var type = request.getResponseHeader('X-Message-Type');
show_ajax_message(msg, type); //use whatever popup, notification or whatever plugin you want
});

layout: application.html.haml

        #flash-message
- flash.each do |name, msg|
= content_tag :div, msg, :id => "flash_#{name}"

I build an engine that includes some behavior to the application_controller to send the flash message in the response header as some of you guys propose.

https://github.com/bonzofenix/flajax

In case you want to use AJAX calls redirect_to should not be used in the controller. Rather, flash message should be explicitly denoted:

In your_controller:

respond_to :js


def your_ajax_method
flash[:notice] = 'Your message!'
end

In the view that is named by your_ajax_method_in_the_controller

your_ajax_method_in_the_controller.js.haml

:plain
$("form[data-remote]")
.on("ajax:success", function(e, data, status, xhr) {
$('.messages').html("#{escape_javascript(render 'layouts/messages')}");
setTimeout(function(){ $(".alert").alert('close') }, 5000);
})

Please, notice, that the messages class is an anchor point for rendering messages. This class should be present in your view or application layout. If you use ERB the line becomes $('.messages').html("<%= j(render 'layouts/messages') %>");

The above JavaScript embedded into HAML/ERB is the key to displaying flash messages when using AJAX. All other components remain the same for non-AJAX calls.

You may use your_ajax_method_in_the_controller.js.coffee or plain .js but then the rails variables won't be available to JS/Coffee. Even though I don't use variables here I prefer to wrap JS in HAML to keep consistent codebase.

I leverage Twitter Bootstrap for styling messages, thus $(".alert").alert('close') fades away the notice. And here is the messages partial:

layouts/_messages.html.haml

- flash.each do |name, msg|
- if msg.is_a?(String)
.alert-messages
%div{class: "alert alert-#{name == :notice ? "success" : "error"} fade in"}
%a.close{"data-dismiss" => "alert"}
%i.icon-remove-circle
= content_tag :div, msg, id: "flash_#{name}"

Just in case, CSS for the alerts is below

.alert-messages {
position: fixed;
top: 37px;
left: 30%;
right: 30%;
z-index: 7000;
}

I modified Victor S' answer to fix some cases where flash[type].blank? didn't work as noted by few people in the comments.

after_filter :flash_to_headers


def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash_message
response.headers["X-Message-Type"] = flash_type.to_s


flash.discard # don't want the flash to appear when you reload page
end


private


def flash_message
[:error, :warning, :notice, nil].each do |type|
return "" if type.nil?
return flash[type] unless flash[type].blank?
end
end


def flash_type
[:error, :warning, :notice, nil].each do |type|
return "" if type.nil?
return type unless flash[type].blank?
end
end

Then rest is the same

// FLASH NOTICE ANIMATION


var fade_flash = function() {
$(".flash_notice").delay(5000).fadeOut("slow");
$(".flash_alert").delay(5000).fadeOut("slow");
$(".flash_error").delay(5000).fadeOut("slow");
};


var show_ajax_message = function(msg, type) {
$(".flash_message").html('<div class="flash_'+type+'">'+msg+'</div>');
fade_flash();
};


$( document ).ajaxComplete(function(event, request) {
var msg = request.getResponseHeader('X-Message');
var type = request.getResponseHeader('X-Message-Type');
show_ajax_message(msg, type); //use whatever popup, notification or whatever plugin you want


});

Here is my version (working with multiples flash notices and special characters UTF-8 encoding):

Inside ApplicationController:

after_filter :flash_to_headers
def flash_to_headers
return unless request.xhr?
[:error, :warning, :notice].each do |type|
if flash[type]
response.headers["X-Ajax-#{type.to_s.humanize}"] = flash[type]
end
end
flash.discard
end

Inside my coffee-script (twitter bootstrap version):

css_class = {
Notice: 'success',
Warning: 'warning',
Error: 'error'
}
$(document).ajaxComplete (event, request) ->
for type in ["Notice", "Warning", "Error"]
msg = request.getResponseHeader("X-Ajax-#{type}")
if msg?
$('#notices').append("<div class=\"alert #{css_class[type]}\">#{decodeURIComponent(escape(msg))}</div>")