What is the best (i.e, most maintainable, DRY) way to display a message to the user after an ajax request?
Obviously, the easiest way to achieve this for a single JS controller action is simply to use the associated JS partial. e.g.,
#create.js
$('.flash-container').html('<p>SUCCESS!</p>');
But, if the app contains a large number of ajaxed actions, this quickly becomes unmaintainable, requiring many partials to be updated if changes need to be made.
The approach I'm currently using is below. But this always seems very brittle and 'hacky' - I must be overlooking the Rails convention?
This is returning That page doesn't exist! the first time an ajax request is triggered on the page. All subsequent requests return the expected result until the page is reloaded. What is going on?
#my_controller.rb
def create
if #object.save
format.js { flash[:notice] = t('.notice') }
else
format.js { flash[:error] = t('.error') }
end
end
# application_controller.rb
after_action :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
end
def flash_message
[:alert, :error, :notice, :success].each do |type|
return flash[type] unless flash[type].blank?
end
return nil
end
def flash_type
[:alert, :error, :notice, :success].each do |type|
return type unless flash[type].blank?
end
return :empty
end
#flash.js.coffee
$(document).ajaxComplete (event, request) ->
msg = request.getResponseHeader("X-Message")
type = request.getResponseHeader("X-Message-Type")
if msg
alert(msg)
Why not change your js partial to a js.erb partial and embed the message you want to send before it is parsed into standard js instead of sending through the header?
in the same way you can have .html.erb files you can also have .js.erb files:
# create.js.erb
<% if flash[:notice] %>
$('.flash-container').html('<p><%= flash[:notice] %></p>');
<% end %>
in terms of keeping it maintainable and dry, that is just down to code design, create a shared js.erb partial that you render inside other .js.erb parials:
#shared/_ajax_messages.js.erb
<% if flash[:notice] %>
$('.flash-container').html('<p><%= flash[:notice] %></p>');
<% end %>
<% if flash[:error] %>
$('.flash-container').html('<p><%= flash[:error] %></p>');
<% end %>
# create.js.erb
<%= render(partial: 'ajax_messages') %>
create a shared helper_method in a top-level controller:
# some_controller.rb
helper_method :handle_ajax_messages
# create.js.erb
$('.flash-container').html('<p><%= handle_ajax_messages %></p>');
You could extract a ServiceObject handler, or a factory if your into DDD to create custom NoticeObjects or Value Objects with #to_html #to_js #to_json etc methods to keep things DRY for all types of requests. There are a huge variety of other options and ways to keep code DRY and maintainable if you break out of some so-called "Rails conventions".
Related
I would like to know if there's any way to know when you come from a redirect_back. I would like to show a popup on my html if I come from here:
class PaymentsController < ApplicationController
def addproduct
(session[:products] ||= []) << params[:item]
redirect_back(fallback_location: "/")
end
end
There is any way?
This is a bit hacky but it can work, the definition of redirect_back accepts a fallback_location and args. So maybe you can use flash to pass an argument like:
# controller
redirect_back(fallback_location: "/", flash: { from_add_product: true } )
# view
<% if flash[:from_add_product] %>
# show popup
<% end %>
Another option is to use request.referer to check the url like:
# view or a helper
<% if request.referer && request.referer =~ /payments\/addproduct/ %>
# show popup
<% end %>
You can use request.referrer.
You can see the controller and action (method) which you come from using request[:controller] and/or request[:action].
Put it in your app/layouts/application.html.erb and you'll be able to see each referrer:
<%= request[:controller] %>
<%= request[:action] %>
I have the following code in a Rails partial being used in some mailers but am not happy with my solution and have the feeling this is far from optimal.
I have an email which
From my mailer:
def the_email_i_am_sending(user, inquiry, params = {})
get_variables(inquiry) #This also provides access to my `#user` object
#contact_name = [params[:guest_last_name].to_s, " ", params[:guest_first_name].to_s].join
I always have #user but on occasion a specific partner will call our API with additional params of [:guest_last_name] and [:guest_first_name] as defined above. This allows me to define #contact_name as a separate instance variable.
When this is .present? i.e. not nil, I want to render #contact_name in a field on the email rather than the #user.login that would pull from our DB.
My mailer view then uses the following code to decide which partial it will render.
<% if #contact_name.present? %>
<%= render 'meet_your_guest_v3', tujia_guest: #contact_name %>
<% else %>
<%= render 'meet_your_guest_v3' %>
<% end %>
My solution is then to utilise this code in the partial being rendered in the mailer. It seems a little verbose but I am unsure about the correct usage of local_assigns.has_key?
<% if local_assigns.has_key?(:partner_guest) %>
<%= partner_guest %> <p>(via our partner</p>
<% else %>
<%= #user.login %>
<% end %>
Is there a better way?
You should definitely follow the advice from #Jon regarding dealing with params in your controller/mailer. Additionally you should just pass #contact_name every time to the underlying partial, regardless if it is present or not, then check only where you want to render it, if it is present. This way you would skip one conditional:
#email_view.html.erb
render 'meet_your_guest_v3', parnter_guest: #contact_name
_contact_name.html.erb
<% partner_guest.present? %>
...
A further step could be using a special decorator object, which would deal with the presentation logick. It would check wether contact_name was provided from outside or from the model and render the desired html tag for the contact_name (or it could just return it as string). See following pseudocode using the draper gem:
class MyController < ApplicationController
def send_mail
#user = User.find(...).decorate(
contact_name: [params[:guest_last_name].to_s, " ", params[:guest_first_name].to_s].join
)
MyMailer.the_email_i_am_sending(#user)
end
end
class MyMailer < ApplicationMailer
def the_email_i_am_sending(user)
#user = user
mail(to: ..., subject: ...)
end
end
class UserDecorator < Draper::Decorator
def contact_name_tag
if (contact_name.present?)
h.content_tag(:div, contact_name)
else
h.content_tag(:div, user_name)
end
end
end
#email_view.html.erb
<%= #user.contact_name_tag %>
However if the presentation logic isn't very complicated, going with a couple conditionals and perhaps extracting them into basic rails helpers is fine and using a presenter may be an overkill
In my Rails app I already have the following code:
<% %w(number_of_students edit_class_name tech_help).each do |modal| %>
<%= render "common/modals/#{modal}" %>
<% end %>
There will be a few more modals added into app/views/common/modals and instead of explicitly listing them out in the %w() I was wanting to loop through the common/modals directory and just render each file.
Here is what I came up with:
def render_modals
files = Dir.glob("#{Rails.root}/app/views/common/modals/*").collect { |file| File.basename(file, ".html.erb").sub("_", "") }.flatten
files.collect do |modal|
render partial: "common/modals/#{modal}"
end.join.html_safe
end
define a simple method in where is more appropriate (maybe app helper?) like this:
def modals
%w(number_of_students edit_class_name tech_help)
end
if you need these modals in a controller/model too, maybe you should define this method in an appropriate class? For example
class Modal
def self.types
%w(number_of_students edit_class_name tech_help)
end
end
Also, if you are rendering the templates often, then also define
def render_modals
modals.map do |modal| # Modals here should be the method that you just defined, example, Modal.types
render partial: "common/modals/#{modal}"
end.join
end
I have a couple different user types (buyers, sellers, admins).
I'd like them all to have the same account_path URL, but to use a different action and view.
I'm trying something like this...
class AccountsController < ApplicationController
before_filter :render_by_user, :only => [:show]
def show
# see *_show below
end
def admin_show
...
end
def buyer_show
...
end
def client_show
...
end
end
This is how I defined render_by_user in ApplicationController...
def render_by_user
action = "#{current_user.class.to_s.downcase}_#{action_name}"
if self.respond_to?(action)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(action)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end
It calls the correct *_show method in the controller. But still tries to render "show.html.erb" and doesn't look for the correct template I have in there named "admin_show.html.erb" "buyer_show.html.erb" etc.
I know I can just manually call render "admin_show" in each action but I thought there might be a cleaner way to do this all in the before filter.
Or has anyone else seen a plugin or more elegant way to break up actions & views by user type? Thanks!
Btw, I'm using Rails 3 (in case it makes a difference).
Depending on how different the view templates are, it might be beneficial to move some of this logic into the show template instead and do the switching there:
<% if current_user.is_a? Admin %>
<h1> Show Admin Stuff! </h1>
<% end %>
But to answer your question, you need to specify which template to render. This should work if you set up your controller's #action_name. You could do this in your render_by_user method instead of using a local action variable:
def render_by_user
self.action_name = "#{current_user.class.to_s.downcase}_#{self.action_name}"
if self.respond_to?(self.action_name)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(self.action_name)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end
One thing you can do on my rap lyric explanation site is "like" explanations (once you're logged in):
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1645.png
I'd like to show the "Like" links to users who aren't logged in, and then, when a non-logged in user clicks "Like", show him a lightbox with a "Login or Register" form (like Digg / Reddit)
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1650.png
What's the best way to accomplish this?
Currently I'm using this approach:
Clicking "Like" POSTs to /annotations/:id/vote (the POST body indicates whether the user is liking or "unliking").
The vote Annotation controller action has a require_user before_filter that looks like this:
def require_user
unless current_user
store_desired_location
flash[:notice] = "You'll need to login or register to do that"
redirect_to login_path # map.login '/login', :controller => 'user_sessions', :action => 'new'
return false
end
end
user_sessions#new looks like this:
def new
#user_session = UserSession.new
respond_to do |format|
format.html {}
format.js {
render :layout => false
}
end
end
The problem is that the redirect doesn't seem to work correctly over javascript:
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1700.png
How do I get this to redirect correctly?
Also, is this the right general approach? Another thought I had was to attach a different handler to the "Like" links in javascript when there was no logged in user (but I don't think this method scales well to other actions that I'd like to handle the same way)
There's a few problems to overcome here.
Browsers in general do not allow redirecting to a POST request.
redirect_to doesn't preserve format without additional input.
Store location does not preserve form data.
All these problems can be solved by eliminating redirects.
Here is how I've handed it in the past:
Instead of redirecting in required_user, render. If a before filter redirects or renders the pending action is cancelled. (No need to return false either). Unfortunately going this route blurs controller boundaries. But allows for simple html fallback, and lends its self to DRYness.
The high level view of the new work flow will be:
Request to annotations#vote (POST)
required_user filter fails
render new session
submit login information and original POST data back to annotations#vote (POST)
new filter in vote captures session information and logs in. vote proceeds as expected. If login fails return to 3.
annotations#vote redirects/renders as it should
Start by reworking the require_user to render the user_sessions#new template.
def require_user
unless current_user
flash[:notice] = "You'll need to login or register to do that"
#user_session ||= UserSession.new
respond_to do |format|
format.html {render :template => 'user_sessions/new'}
format.js {
render :template => 'user_sessions/new', :layout => false
}
end
end
end
The #user_session ||= UserSession.new ensures we can return validation errors to the form.
Now we've got to beef up your user_session#new template so that it can remember the action. Also if you plan on using lightboxes, this should be a partial rendered rendered by relevant RJS or the new.html.erb.
First we create a partial to create hidden fields preserving the POST data that would have been lost in a redirect:
<% if params[:controller] == "annotations" %>
<% content_for :old_form do %>
<%= hidden_field_tag "annotation[song_id]", params[:annotation][:song_id] %>
<%= hidden_field_tag "annotation[vote]", params[:annotation][:vote] %>
<% end %>
<% end %>
Then render that partial in the login partial that will occupy your lightbox:
<%= render :partial => vote_form_replica %>
<% url = params[:controller] == "user_sessions ? user_sessions_url : {} %>
<% form_tag #user_session, :url => url do |f| %>
<%= yield :old_form %>
<%= f.label :user_name %>
<%= f.text_field :user_name %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= submit_tag %>
<%end%>
The empty hash for url in the form_tag looks like an error, but isn't. It ensures that the form data is posted to the url that rendered the form. Which at this point should be annotations/:id/vote
Now for the new filter to login. Essentially it will be doing what ever UserSessionsController#create does without the render/redirect. The following is copied from the RESTful authentication plugin.
def authenticate
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
if params[:remember_me] == "1"
current_user.remember_me unless current_user.remember_token?
cookies[:auth_token] = { :value => self.current_user.remember_token,
:expires => self.current_user.remember_token_expires_at }
end
end
end
All that's left is to make sure the filter order is right.
before_filter :authenticate, :require_user, :only => :vote
N.B.: You're probably not going to use this version of require_user without this version of authenticate so it makes sense to combine them into a single filter.
And that's it. The way this has been set up allows for robust DRY easily reuseable code. By placing the new filters into ApplicationController they're available in any controller. From this point, adding this functionality to any other controllers/actions takes only 3 simple steps:
Create a new partial modelled after the vote_form_replica partial.
Add the corresponding render statement to the new session template.
Apply the filters to your actions.
I would approach this in the way you describe at the bottom of your question. Before displaying the page initially, check if the user is logged in. If they are, the "Like" links should use their normal behavior. If not, bind a click event to show the register/login panel. There's nothing about this that can't be reused. In fact, we use this exact method at my job. Any user action that requires authentication either follows its normal behavior or pops up a generic login panel depending on login state at the time the page loads.