Responding to AJAX Rails request with .js.erb and Rails instance variable - ruby-on-rails

I am struggling w/ JS AJAX requests in Rails. There is an official guide here, but I am having slight difficulties matching it with ES6 JS. I am having troubles passing things back to my frontend after making my requests.
I have a JS window.onload call made, because I am trying to find the user’s screen size (among other things) and pass it back to Rails:
let xhttp = new XMLHttpRequest();
const url = "/users";
xhttp.open("POST", url);
// Some other things added to it...
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 201) {
console.log(this.responseText);
}
};
xhttp.send(JSON.stringify({user_info: userInfo}));
It is posting to /users some information about the session. This is going through fine. Note the console.log that keeps track of the response, we will get to this later.
In my Rails controller:
def create
user_info = params[:user_info].permit!
user_info = user_info.to_s
#fingerprint_user = User.find_or_create_by(fingerprint: user_info)
respond_to do |format|
# NOTE: I have tried a few things here
# format.html { redirect_to #fingerprint_user, notice: "Successfully identified user by fingerprint." }
# format.js
format.json { render json: #fingerprint_user, status: :created, head: :ok }
end
end
The JSON sender is working correctly. The console.log in the JS above correctly console.logs the received JSON. The request responds with 201, and the #fingerprint_user instance variable in JSON form.
My problem is with returning ERB JS with the instance variable. As shown in the guide, I have tried adding format.js. Then, the request returns a 200, and the contents of my views/users/create.js.erb file:
console.log("hello");
However, it is not actually logging to console.
Lastly, I tried with all format fields (js, html, and json). Here is my show.html.erb:
<p>Got user: <%= #fingerprint_user.to_s %> </p>
Here is a better views/users/create.js.erb file, where fingerprint is a div in my index.html.erb:
console.log("hello");
$("<%= escape_javascript(render #fingerprint_user) %>").appendTo("#fingerprint");
Once again, the response is 200, and the appropriate html, but this is not rendered on the page.

Doing requests for AJAX requests for JavaScript is different then requesting JSON. Instead of requesting some data and parsing it you actually load the data and then eval it into the current page context through various tricks like appending script tags into the document. This is the actual Rails UJS implementation:
processResponse = (response, type) ->
if typeof response is 'string' and typeof type is 'string'
if type.match(/\bjson\b/)
try response = JSON.parse(response)
else if type.match(/\b(?:java|ecma)script\b/)
script = document.createElement('script')
script.setAttribute('nonce', cspNonce())
script.text = response
document.head.appendChild(script).parentNode.removeChild(script)
else if type.match(/\b(xml|html|svg)\b/)
parser = new DOMParser()
type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
try response = parser.parseFromString(response, type)
response
This is basically how we used to do AJAX calls cross domain ten years ago with JSONP to get around the limitations of the browsers of the day.
You can emulate the same thing in a "raw ajax request" with:
let xhttp = new XMLHttpRequest();
const url = "/users";
xhttp.open("POST", url);
// Some other things added to it...
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 201) {
let script = document.createElement('script');
script.innerHTML = data;
document.querySelector('head').appendChild(script);
}
};
But quite frankly js.erb is a horrible idea. It makes an absolute mess out of the server and client side responibilities and makes your code very difficult to follow and reason about and it moves JS out of the assets/webpack pipeline and into a smattering of proceedural junk script views. The only possible reason to use it is how lazy you can be with Rails UJS and still add some ajax to your application.
If you're writing an ajax handler anyways just return a chunk of html (in a json object or as html) and append it to the DOM instead.

Related

Rails4: Passing non-forum data from front end to the back end, and processing it

Ok. So I have a small XHR request where json is returned. On success the json is passed like this.
var myArr = JSON.parse(xmlhttp.responseText);
myMainFunction(myArr);
function myMainFunction(arr) {
var vShipTypeID = arr[0].victim.shipTypeID;
}
I need to send vShipTypeID to rails. My goal is to be sending this value back to activerecord and the information returned will go within a js, or json file to the respond_to statement in the controller.
#shipName = InvType.find(vShipTypeID).name
Here's the main idea. The client sends out the ID of the ship to rails, and rails returns the ship's name.
I know how to make the Ajax request.
I don't know how to process the request on the server end. Meaning after rails receives the data, where do I find it, and how to I convert it to be usable so that I can make an activerecord statement out of the value I received from the client?
Anyone?
A simple solution could be defining an action on your ship controller and define a route to it for example define a route in your routes.rb
get "/ship/:id/getname" => "ship#getname"
in your js file
$.get( "/ship/"+vShipID+"/getname")
.done(function(data) {
alert( "Ship name: " + data.name );
})
.fail(function() {
alert( "error" );
});
in you ship_controller.rb
class ship_controller <ApplicationController
....... #other methods
def getname
#shipname = Ship.find(params[:id]).name
if request.xhr
render :json => {name: #shipname} and return
else
redirect_to ship_path(params[:id])
end
end
........ #some other methods
end
You need to handle json requests in your controller check this question for details
for a quick example in your controller create a new action getnamefromid and call that controller from your client.
respond_to do |format|
format.json { render :json => #shipName }
end

render geojson w/ respond_to rails & mapbox/leaflet

So I am currently trying to request some geojson data from my rails controller. I am using the loadURL method provided to me through mapbox/leaflet to make the ajax call to my controller.
$(document).on("ready", function() {
L.mapbox.accessToken = 'token is here';
var userMap = L.mapbox.map('user-map', 'mapbox.run-bike-hike')
.addControl(L.mapbox.geocoderControl('mapbox.places'))
.setView([37.7833, -122.4167], 12);
var featureLayer = L.mapbox.featureLayer().loadURL('http://localhost:3000/users/1/trails.geoJson').addTo(userMap)
console.log(featureLayer);
// getTrailPoints(userMap);
featureLayer.on('ready', function(){
userMap.fitBounds(featureLayer.getBounds());
});
});
The above code is able to hit my controller and my controller is able to retrieve the correct data. Here is what I have in my controller:
def index
user = User.find_by(id: params[:user_id])
#trails = user.trails
#geojson = Array.new
build_geojson(#trails, #geojson)
p "*" * 50
p #geojson
respond_to do |format|
format.html
format.geojson { render geojson: #geojson }
end
end
The build_geojson method works fine, you will have to trust that. However, what is not working is the format.geojson and rendering it as geojson. I'm pretty sure I need to create a Mime but I am unsure how to do so or in what way I should go about doing it with geojson. Any help would be greatly appreciated. I will also answer any questions.
I do currently have it formatted in just json because geojson is just json. However with mapbox when I do that, I get the following error:
http://a.tiles.mapbox.com/v4/marker/pin-l-tree+00607d.png?access_token=pk.e…hIjoiNDQ5Y2JiODdiZDZmODM0OWI0NmRiNDI5OGQzZWE4ZWIifQ.rVJW4H9TC1cknmRYoZE78w Failed to load resource: the server responded with a status of 400 (Bad Request)
The error basically results in the image not being loaded.
There is no tree icon in the currently-supported Mapbox icons. Use park instead, which is also a picture of a tree.

I'm a little unclear how the controller knows to return a JS file rather than HTML (from Railscast #174 Pagination)

In this Railscast #174 Pagination tutorial, the author changes...
$(function() {
$(".pagination a").click(function() { // e.g. a.href = /transactions?page=x
$.get(this.href, null, null, "script");
return false;
});
});
When my controller receives this request it returns the JS file (index.js.erb - I added this myself in accordance with the tutorial). I understand that the "script" paramater makes the difference but I can't find out what is happening.
Controller:
def index
# fetch arrays
#funds = current_user.funds
# set session vars
set_filter_date_session_variable(:transactions_filter_date_from) # to db format
set_filter_date_session_variable(:transactions_filter_date_to) # to db format
if #fund = current_fund
#transactions = #fund.transactions
.where("date >= ? AND date <= ? AND amount < 0", session[:transactions_filter_date_from], session[:transactions_filter_date_to])
.order("date DESC, id DESC")
## perform a paginated query:
#transactions = #transactions.paginate(:page => params[:page], :per_page => 5)
else
#transactions = []
end
#categories = current_user.categories.order(:name)
#transactions_filter_date_from = convert_date(session[:transactions_filter_date_from], "%Y-%m-%d", "%d/%m/%Y")
#transactions_filter_date_to = convert_date(session[:transactions_filter_date_to], "%Y-%m-%d", "%d/%m/%Y")
end
Does the "script" parameter get passed in the request header? Previously I thought the controller looked at the requesting file extension, perhaps default was .html (so if I request file.json it knows from the extension that it should return the json view). Could someone please clarify a little, or direct me somewhere I can understand how a controller handles which view to return in this case. I'd like to understand this part fully.
Thanks
With the
$.get(this.href, null, null, "script");
You're requesting from the serve that it returns the javascript file, hence why the "script"
Normally you will notice that at times people include the format.js in the controller so that when you put in the url + "." + "format type" (eg. reports.csv or reports.html) you'd need to include in the controller the code below (you can't just request json if your controller is not setup to bring back that information)
respond_to do |format|
format.html
format.js
end
In your case that you didn't include that format in the controller, the "script" in your get function above is asking to respond with the js file. Hence why your js code becomes executed and not your html. If you asked for html, see what happens.
Hope that makes sense?
I deduced to this conclusion because for my pagination I am using jQuery's function getScript which looks like this:
$.getScript(this.href);

WARNING: Can't verify CSRF token authenticity

I'm having trouble making a cross-domain request from my shopify site to my rails app, which is installed as a shopify app. The problem, as stated in the title, is that my server warns me that it Can't verify CSRF token authenticity I'm making the request from a form returned by my rails app, which includes the relevant CSRF token. The request is done with jQuery's ajax method, and the preflight OPTIONS request is being handled by rack-cors.
I've included the X-CSRF-Token in my headers as was suggested in this answer. My post request is being made from a form, so my question is not answered here. The options request (mentioned in this question) is indeed being handled, as I just confirmed by asking this question. I've been stuck on this for a while, and have done a bit of reading.
I'm going to try walking through the process code-snippet by code-snippet, and maybe by the time I finish writing this post I will have discovered the answer to my problem (if that happens, then you won't ever get a chance to read this paragraph).
Here are the new and create methods from my controller.
class AustraliaPostApiConnectionsController < ApplicationController
# GET /australia_post_api_connections/new
# GET /australia_post_api_connections/new.json
def new
# initializing variables
respond_to do |format|
puts "---------------About to format--------------------"
format.html { render layout: false } # new.html.erb
format.json { render json: #australia_post_api_connection }
end
end
# POST /australia_post_api_connections
# POST /australia_post_api_connections.json
def create
#australia_post_api_connection = AustraliaPostApiConnection.new(params[:australia_post_api_connection])
respond_to do |format|
if #australia_post_api_connection.save
format.js { render layout: false }
else
format.js { render layout: false }
end
end
end
end
(I wonder about the respond_to block in the create method, but I don't think that would cause the CSRF token to fail verification.)
Within my app, at /AUSController/index, I have an ajaxified GET request that brings back the form from /AUSController/new. My goal is to be able to make all the same calls from a cross-domain origin as I can from within my app. Right now the GET request works for both, and so I will neglect to include the 'new' form. When the HTML is finally rendered, the form element has the following:
<form method="post" id="new_australia_post_api_connection" data-remote="true" class="new_australia_post_api_connection" action="http://localhost:3000/australia_post_api_connections" accept-charset="UTF-8">
<!-- a bunch more fields here -->
<div class="field hidden">
<input type="hidden" value="the_csrf_token" name="authenticity_token" id="tokentag">
</div>
</div>
</div>
</form>
The CSRF token is generated by a call to form_authenticity_token as detailed in one of the references mentioned above.
The next step is done differently in the two cases:
My app successfully returns the new form to the shop upon an ajax request. I've tested this within the app, that is by making an ajax call to /controller/new from /controller/index, and then submitting the form. This works like a charm. The js that is returned from a successful POST within my app is as follows:
/ this is rendered when someone hits "calculate" and whenever the country select changes
:plain
$("#shipping-prices").html("#{escape_javascript(render(:partial => 'calculations', :object => #australia_post_api_connection))}")
Which renders the following partial,
= form_tag "/shipping_calculations", :method => "get" do
= label_tag :shipping_type
%br
- #service_list.each_with_index do |service, index|
- checked = true if index == 0
= radio_button_tag(:shipping_type, service[:code], checked)
= label_tag(:"shipping_type_#{service[:code]}", service[:name])
= " -- $#{service[:price]}"
%br
When I call it from the same domain, request.header contains the following:
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{
"session_id"=>"db90f199f65554c70a6922d3bd2b7e61",
"return_to"=>"/",
"_csrf_token"=>"the_token_I_expect=",
"shopify"=>#<ShopifyAPI::Session:0x000000063083c8 #url="some-shop.myshopify.com", #token="some_token">
}
And the HTML is rendered and displayed nicely.
From the cross domain source, however, things are understandibly more complicated. This is where CORS and CSRF tokens and routes and all these little details start creeping in. In particular, when I make the ajax call I use the following script (which does not live in my rails app, it lives on the cross-domain server). The action of this ajax request is attached to the submit button by the callback function from the GET request, and I've included the GET request for the sake of completion.
<script>
var host = "http://localhost:3000/"
var action = "australia_post_api_connections"
console.log("start")
$.ajax({
url: host + action,
type: "GET",
data: { weight: 20 },
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function(data) {
console.log("success");
$('#shipping-calculator').html(data);
$('#new_australia_post_api_connection')
.attr("action", host + action);
$('.error').hide();
$(".actions > input").click(function() {
console.log("click")
// validate and process form here
$('.error').hide();
var to_postcode = $("input#australia_post_api_connection_to_postcode").val();
// client side validation
if (to_postcode === "") {
$("#postcode > .error").show();
$("input#australia_post_api_connection_to_postcode").focus();
return false;
}
tokentag = $('#tokentag').val()
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
// included based on an SO answer
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-TOKEN', tokentag);
}
});
$.ajax({
type: "POST",
url: host + action,
data: dataHash,
success: function(data) {
$('#shipping-prices').html(data);
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
return false;
});
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
$(function() {
});
</script>
However, when I call it from this remote location (the distant slopes of Shopify), I find the following in my request headers,
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{ }
And I receive a very unpleasant NetworkError: 500 Internal Server Error rather than the 200 OK! that I would like... On the server side we find the logs complaining that,
Started POST "/australia_post_api_connections" for 127.0.0.1 at 2013-01-08 19:20:25 -0800
Processing by AustraliaPostApiConnectionsController#create as */*
Parameters: {"weight"=>"20", "to_postcode"=>"3000", "from_postcode"=>"3000", "country_code"=>"AUS", "height"=>"16", "width"=>"16", "length"=>"16", "authenticity_token"=>"the_token_I_expect="}
WARNING: Can't verify CSRF token authenticity
Completed 500 Internal Server Error in 6350ms
AustraliaPostApiConnection::InvalidError (["From postcode can't be blank", "The following errors were returned by the Australia Post API", "Please enter Country code.", "Length can't be blank", "Length is not a number", "Height can't be blank", "Height is not a number", "Width can't be blank", "Width is not a number", "Weight can't be blank", "Weight is not a number"]):
app/models/australia_post_api_connection.rb:78:in `save'
The lack of a rack.session seems suspicious like the cause of my misery... but I haven't been able to find a satisfying answer.
Finally I have seen fit to include my rack-cors setup, in case it is useful.
# configuration for allowing some servers to access the aus api connection
config.middleware.use Rack::Cors do
allow do
origins 'some-shop.myshopify.com'
resource '/australia_post_api_connections',
:headers => ['Origin', 'Accept', 'Content-Type', 'X-CSRF-Token'],
:methods => [:get, :post]
end
end
Thank you so much for even reading all of this. I hope the answer has to do with that empty rack.session. That would be satisfying, at least.
Well one of my coworkers figured it out. The problem was, the has I was sending didn't have the same structure as the hash I was expecting in my controller.
In my controller I instantiate a new API connection as follows,
AustraliaPostApiConnection.new(params[:australia_post_api_connection])
I am looking for params[:australia_post_api_connection], but there is no such index in my data hash, which looks like,
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
To fix this I changed the JS file to contain,
var dataHash = {
to_postcode: to_postcode,
}
var params = {
australia_post_api_connection: dataHash,
authenticity_token: tokentag // included based on an SO answer
}
And now it works! Thanks co-worker!

Rails is not rendering my .js.erb file

I have this code in my create.js.erb file:
pollingAJAX();
function pollingAJAX() {
$.ajax({
method: "POST",
url: "/status",
data: {uuids: '<%= #uuid_hash.to_json %>'},
},
success: function(data){
var obj = $.parseJSON(data);
if(obj.isDone == "yes"){
}else{
obj.each(function(result) {
if(result.status == "completed"){
$('a[href="#{result.url}"]').html('');
}
});
pollingAJAX();
}
}
});
}
This AJAX request is not being triggered because my create.js.erb file is not being rendered. I have this code in my create action:
result['items'].each do |r|
# escaped_url = URI.escape(r['link'], Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")).gsub("%","-")
#site_array << r['link']
if Result.where(:link => r['link']).present?
else
job_id = ImageGenerator.create(:url => r['link'])
#uuid_hash[r['link']] = job_id
end
end
respond_to do |format|
format.html
format.js
end
I think it may have to do with the fact that I am processing a bunch of background jobs with Resque before calling respond_to?
How can I get my create.js.erb file to be triggered?
UPDATE: Make sure you're actually including your JavaScript with a <script> tag, otherwise it will never be loaded.
Move the invocation of pollingAJAX() to after that function has been defined. Provided it (lexically) parses ok, JavaScript executes linearly, as it's interpreted, rather than compiled and then executed, so the pollingAJAX() function doesn't yet exist where you've put the invocation.
Also, always view your source code in the browser, to make sure the .erb has processed the way you intended. And make use of your JavaScript console in your browser to detect any errors.
Since it appears that you are using jQuery from your AJAX request syntax, you can bind your method to the DOM load event:
$(function() {
pollingAJAX();
});
This will prevent pollingAJAX() from executing until after the entire Javascript file has been parsed, allowing this function to find its definition.
See more here: http://www.learningjquery.com/2006/09/introducing-document-ready

Resources