I'm trying to understand AJAX requests in Rails. I have a form that I currently submit using remote: true. I want to respond with an HTML redirect if the request is successful, and run an error message with Javascript if it is unsuccessful. However, no matter what the outcome is, the request seems to expect a .html as the return.
respond_to do |format|
if conversation
format.html { redirect_to(conversation_path(conversation)) }
else
format.js
end
end
This is called after I save the conversation call on AJAX. On a successful save, the path HTML is correctly sent back, but is not rendered on the client. But on an unsuccessful save, it expects the .html and throws an error. How do I accept .js as a response? My goal is to just pop up an error if the call is unsuccessful and redirect_to on a successful call.
Edit: My form_for:
<%= form_for :conversation, url: :conversations, remote: true, html: { class: "conversation-form" } do |f| %>
Here's a suggested alternative to your end-goal - in the controller, drop the format.html entry in your respond_to block. Also, set conversation to an instance variable that the view template can access:
class YourController < ActionController::Base
def your_action
# awesome code doing stuff with a conversation object goes here
#conversation = conversation
respond_to do |format|
format.js # your_action.js.erb
end
end
end
Then, put the redirect logic in your javascript view template (for the above example: app/views/.../your_action.js.erb):
<% if #conversation.errors.any? # or whatever condition you want to show a popup for %>
// put your javascript popup code here
alert('Errors happened!');
<% else %>
// this is how you do a redirect using javascript:
window.location.href = "<%= conversation_path( #conversation ) %>";
<% end %>
Hope this helps!
Related
I have a working Ruby on Rails form that currently posts my results and redirects the user to another view and posts the results. The flow is:
views/tools/index.html.erb -> views/tools/ping.html.erb
Since I have it working now, I'd like to convert it to AJAX and keep the user on the views/tools/index.html.erb view, getting rid of the redirect to enhance the user experience. However, I'm unsure of how to proceed based on the way that my Tools controller is currently setup, and my incredibly lacking knowledge of AJAX.
So, here's what I currently have:
views/tools/index.html.erb (added 'remote: true' to form)
<h1> Tools </h1>
<h3> Ping </h3>
<%= form_tag ping_tool_path(1), method: "post", remote: true do %>
<%= text_field_tag :ip, params[:ip] %>
<%= submit_tag "Ping", name: nil %>
<% end %>
<!-- this is where I'd like to put the results via AJAX -->
<div id="output"></div>
controllers/tools_controller.rb
class ToolsController < ApplicationController
def index
end
def ping
ping_host(params[:ip])
save_host(params[:ip])
# Adds based on recommendations
respond_to do |format|
format.html { redirect_to tools_path }
format.js
end
end
protected
def ping_host(host)
f = IO.popen("ping -c 3 #{host}")
#output = f.readlines
tool_type = "ping"
tool = Tool.find_by(tool_type: tool_type)
tool.increment(:tool_hit_count, by = 1)
tool.save
#results = "<pre>#{#output.join}</pre>".html_safe
end
def save_host(host)
host = Host.find_or_create_by(host_ip: host)
host.increment(:host_hitcount, by = 1)
host.save
end
end
views/tools/ping.html.erb
<%= #results %>
views/tools/ping.js.erb (New file based on suggestion)
$("#output").html("<%= #results %>");
routes.rb
Rails.application.routes.draw do
root 'tools#index'
resources :tools do
member do
post 'ping'
end
end
end
This is what I see on the Network tab in Google Chrome after submitting the form:
So, what I know at this point is that I'll need to add remote: true to my form in views/tools/index.html.erb, and this is where I get lost.
It seems that I have an issue ATM with the fact that I've abstracted the form to use my ping method in the Tools controller, whereas all of the tutorials (and railscasts) I've gone through are doing AJAX on CRUD methods and a given model, not something like what I've build here so far. Please help me understand AJAX!
You're on the right track, now you need to modify the def ping action with a respond_to block.
def ping
ping_host(params[:ip])
save_host(params[:ip])
respond_to do |format|
format.html { redirect_to tools_path } ## if you still want to have html
format.js
end
end
and create a file called view/tools/ping.js.erb where you have javascript that will be run and returned asynchronously
$("#output").html("<%= j #results %>");
The <%= %> block will be evaluated first and replaced with the output of that ruby code. Then this will be inserted into the #output div.
I have a website that has a lot of AJAX forms and as it is now I have to make a new js.erb view for each one of them to just post a message that pretty much says completed but is unique to each action.
Is there a way that I can combine or forward one action to a message action in the controller so I would only need one view to handle all the JavaScript messages
Here is what I have:
Controller:
def some_action
{ do some things here }
respond_to do |format|
format.js
end
end
View:
some_action.js.erb
$('#messages').append("<%= escape_javascript(render :partial => 'flash_message') %>");
<% if #error == "True" %>
$('#flash').removeClass().addClass( "error" ).html('<%= escape_javascript(#message) %>');
<% else %>
$('#flash').removeClass().addClass( "success" ).html('<%= escape_javascript(#message) %>');
<% end %>
Would rather have one action in the controller to control all messages when no other changes are necessary.
You can abstract that functionality into instance methods, something like this
def ajax_success(message)
#message = message
render 'ajax/success'
end
def ajax_failure(message)
#message = message
render 'ajax/failure'
end
Then in your controller
def update
if (successful)
ajax_success
else
ajax_failure
end
end
You can either define these in your ApplicationController, or in a module that is included in any controllers where you wish to use them.
There are some issues here when you wish to have actions that respond to multiple content types, but for actions that should only respond to .js this should be fine.
Also, I think the standard practice for AJAX responses like this would be to return a JSON message, and then have some javascript function that could decode it and transform the page appropriately.
I have a pretty simple rails app where users can upvote pins. The system I implemented is working well, But I would like the number of votes updated after each votes through an ajax method.
Here is my upvote system as how it is now:
in app/controllers/pins_controller.rb:
def upvote
#pin = Pin.friendly.find(params[:id])
#pin.votes.create(user_id: current_user.id)
respond_to do |format|
format.json { render json: { count: #pin.votes_count } }
end
end
in my app/views/pins/index.html.erb:
<%= link_to upvote_pin_path(pin), method: :post, remote: true do %>
<% if pin.votes.where(user_id: current_user.id).empty? %>
<span class="text-decoration: none;"><i class="fa fa-star"></i>
<% else %>
<span class="text-decoration: none;"><i class="fa fa-star" style="color:#c0392b"></i>
<% end %>
<% end %>
<span class="votes-count">
<%= pluralize(pin.votes.count, "") %>
</span>
So everytime someone upvote a pin, the vote is visible only after refreshing the page. Any ideas?
I know I should call the ajax method inside an upvote.js.erb file in my views, but that is where I am lost.
I personally prefer to avoid those messy .js.erb files, though you can certainly do it that way. Once you're making a lot of Ajax calls, though, you end up with a whole lot of files. Ryan Bates has a phenomenal screencast on this technique here.
To do it without, here's the recipe. You'll need to put a respond_to block in your controller action. Right now, now matter how the request comes in, you're redirecting to the pin path, which I'm assuming is the current path in this case, which reloads the page. That's what you're trying to avoid.
Ajax request by default come in to controllers as javascript, or js, request types. You can switch that type if you want (JSON is a popular alternative), but let's keep it simple for now. So configure the respond_to block for a js request:
# app/controllers/pins_controller.rb
def upvote
#pin = Pin.friendly.find(params[:id])
if #pin.votes.create(user_id: current_user.id)
respond_to do |format|
format.html {
flash[:notice] = "Thanks for your recommendation."
redirect_to(pin_path)
}
format.js {
#count = #pin.votes.count.to_s
render nothing: true, count: #count # anything else you put in here will be available in the `success` callback on the JQuery ajax method.
}
end
else
format.html {
flash[:notice] = "Unable to process your request. Please try again."
redirect_to(pin_path)
}
format.js {
render nothing: true # anything else you put in here will be available in the `error` callback on the JQuery ajax method.
}
end
end
Now that the controller has passed back the value, you need to retrieve it in your Ajax request:
$.ajax({
.....
success:function(data){
$('span.votes-count').text(data.count)
}
Two things (at least) won't be available to you as you're using them with this method. The flash message, and the ruby pluralization method. To get around the second one you'll need to use the Ruby pluralization in the controller and return the text you want. To get around the first one, you'll need to set up some javascript that imitates the Rails flash message technique, as well as pass the flash message back to the Ajax call.
You should have your pinvotes inside an span with a class for searching after the ajax is complete
$(".fa.fa-star").click(function(){
var _this=$(this)
$.ajax({
.....
success:function(msg){
_this.parent().parent().find('.votes-count').html(msg); //Accesing the parent then you should acces a span to your votes
}
})
})
I have a sign up form that should redirect the user to their profile if sign up is successful. If registration fails I want to put up an error message without reloading the page using Javascript.
Here's my form header:
<%= form_for resource, as: resource_name, url: registration_path(resource_name), remote: true do |f| %>
My registration controller:
def create
respond_to do |format|
if resource.saved
sign_up(resource_name, resource)
format.html { redirect_to current_employer }
else
format.js { render 'employers/sign_up_failure' }
end
end
end
So the failure works. It displays an error message above the form without reloading the page.
The successful registration format.html { redirect_to current_employer } is not working.
My server log says the redirect was handled:
Redirected to http://localhost:3000/employers/27
But it looks like the show action on my Employers controller is being called as Javascript:
Processing by EmployersController#show as JS
So when I submit the form, it does not redirect me to my profile page. The page doesn't flicker. I am logged in though, so if I refresh the page it shows that I am logged in.
BTW, I am using Devise and my Registration controller is an amendment to what Devise uses.
Not sure why this isn't working. I've taken out the format.js {} part and it still sends the GET call to my Employers#Show action via JS. What I'd like is for the redirect to be formatted as HTML and take me to the profile page.
Any ideas?
SOLUTION
Thank you for you help. Face palm, I totally forgot about how remote: true works. Here is my implemented solution... hopefully someone finds this useful.
def create
respond_to do |format|
if resource.saved
sign_up(resource_name, resource)
#CHANGED
format.js { render js: "window.location.href = '/#{resource_name}s/#{resource.id}'" }
else
format.js { render "#{resource_name}s/sign_up_failure" }
end
end
end
This also improved the reusability of the code.
Because you have remote: true on the form it is submitted via ajax, you haven't said what to do if submitted via ajax and successful. The format requested is js not html which is why the redirect isn't working.
You'll need to create a create.js.erb file to serve if successful, handling the redirect through javascript with window.location = "wherever you want to go"
I have a signed_in? method in rails that will drive u back to the landing page if you have been inactive for 5 minutes.
How do I make this work when the user clicks on a form submitted via js after the 5 minutes have passed? Obviously when this happens, the server sends the landing page back to the user, but it is not shown. Here is my signed_in method
def signed_in?
unless current_user
flash[:notice] = "Please log in"
redirect_to root_url
else
if session[:expiry_time].nil? || session[:expiry_time] < 5.minutes.ago
reset_session
flash[:notice] = "Please log in"
redirect_to root_url
else
session[:expiry_time] = 300.seconds.from_now
end
end
end
PS: I know it's horrible code, I'll be sure to refactor it :)
But I'd start by implementing an alternate redirect method that does a full redirect even for XHR requests:
def redirect_with_js location
if request.xhr?
# HTML response is expected:
render :text => "<script>document.location = '#{view_context.escape_javascript location}'</script>"
# JS response is expected:
# render :text => "document.location = '#{view_context.escape_javascript location}'"
else
redirect_to location
end
end
Then use this method when redirecting:
redirect_with_js boomeranked_url
The returned text depends on what type of response the AJAX request expects to get back (HTML or JS). I've included both versions. This should give you something to start with.
This is the solution I came up with it, I find it simpler. In the signed_in method:
respond_to do |format|
format.js {
flash[:notice] = "Please log in"
render 'home/not_logged_in'
}
And then on the not_logged_in view
window.location = '<%= root_url %>';