Skip to main content

53 jQuery UJS and AJAX

Episode 17 · July 27, 2014

A quick introduction to jQuery UJS and how we can use it to make an AJAX request to render a javascript template from the Rails server

Javascript Frontend


Notes

These examples all show the raw HTML that is needed. You can generate these data attributes using Rails form, form_tag and link_to helpers by adding the data: {} option.


"data-confirm": Confirmation dialogs for links and forms
This displays a confirmation dialog from the browser asking the user to confirm they want to execute an action.

<form data-confirm="Are you sure you want to submit?">...</form>

"data-disable-with": Automatic disabling of links and submit buttons in forms
This option disables and changes the value of the button or link it's added to when submitting the form or clicking on the link. You can learn more about this option in Button Loading Animations with jQuery UJS

<input type="submit" value="Save" data-disable-with="Saving...">

"data-method": Links that result in POST, PUT, PATCH or DELETE requests
This option allows you to change the request type from the default. With links, you can change them to submit other requests than the default GET.

<a href="..." data-method="delete" rel="nofollow">Delete this entry</a>

"data-remote": Make links and forms submit asynchronously with Ajax
This option submits the form or link asynchronously and, by default, expects the server to render a Javascript response which then gets executed in the browser.

<form data-remote="true" action="...">
  ...
</form>

"data-type": Set Ajax request type for "data-remote" requests
This option allows you to set the remote dataType option when using remote: true

<form data-remote="true" data-type="json">...</form>

Resources

jQuery UJS on Github
List of supported options

Transcripts

In this episode, we're going to take a look at AJAX, and how we can do that with the jQuery UJS that rails provides, so let's dive in. I have crated a rails app with a scaffold for books, and books have names, descriptions and published at dates, what we're going to do is go into the show action and add a "Publish" button here that will talk to the application, it will write the published at date, and then we'll add it to the page with JavaScript. We're going to first start with the jQuery UJS version, which should be the simpler of the two, but it's definitely more rails specific, and you'll probably not want to use this for your bigger applications, so let's dive in. This page, the show action for the books is where we want to add the publish link to the page. If we just go in here and we add <%= link_to "Publish", publish_book_path(@book) %> and then we'll dive into the routes to make the publish_book_path actually exist. We want to create:

routes.rb

Rails.application.routes.draw do 
    resources :books do 
        member do 
            path :publish
        end 
    end 
end 

Let's add a placeholder

app/views/books/show.html.erb

<strong>Published At:</strong>
<%= @book.published_at %>

Now that we have the links, when we click on this, we'll get taken as a get request to the pusblished path, so that's incorrect, we made a patch and we want it to actually submit and change the record. When we click on a link by default, those are going to be handled as GET request. Now the next setp is to add the ability to make this link a PATCH request. Rails provides unintrusive JavaScript that gets triggered by the method attribute here and we can set it to PATCH. When we open up our application.js file, you'll see that there's a require jquery_ujs, which stands for "jQuery unintrusive JavaScript". That JavaScript file basically provides the ability to look for things like this method attribute, and then change the way, the behavior of this link to actually submit a patch request rather than just a GET request. When we refresh our page here, if we inspect this attribute, on the link you can see that there's a data method that is equal to PATCH, and of course we defined that right here. Now this JavaScript will intercept this link, and when we click on it, we no longer get the GET request in route problem, but we're missing the action. Rails mapped this correctly to the route, we sent the PATCH request and now we just have to fill out the rest of this action. If we open up our books_controller.rb now

before_action :set_book, only: [:show, :edit, :update, :destroy, :publish]

#Rest of controller code 

def publish 
    @book = Book.find(params[:id])
end 

Now if we refresh and submit this again, we'll get a template missing. This is submitting a PATCH request, but it's still expecting to render some html, and we're actually loading a new page. This isn't actual AJAX, because we are submitting the request, but we're moving, and not actually taking the result, we're going to another page. In that sense we are still submitting data over, but we're not actually taking the result and modifying the current page with that. The way we can do that is to go back to our show action, and also add a remote: true option. Here, now when we submit this, let's go back to the show action, nothing happens. That's kind of interesting. What is going on here? If we go into our rails logs, we can see a big stack trace, and we can see that it published it, but this time it was expecting a JavaScript response, and last time that we did it, it expected an html response. In your rails logs, you can see when it says "Processing by", you can see the controller, the action and the response it expects to receive back. Now that we've added remote :true option. Rather than looking for an html response, we're expecting a JavaScript response, and when this crashed, you didn't see anything go wrong on the front end side because the JavaScript here on the front end was expecting the JavaScript response to come back, and then it would execute that JavaScript here on the browser, and the browser was not being taken to a different page to load. Everything that will crash in this remote :true links or forms, those crashes will happen here in the console in your browser, so you'll be able to see that when we submit this link, you will get another error and another and another, and you will see that there's a 500 error, which means that there's an internal server error. Something on the server happened that crashed when we submitted a PATCH request to our url.

The way to fix that 500 error is to actually put in the template into our views. If we go into our app/views/books folder, we can see that the formats that we have are a few html ones, and a couple JSON responses, but we don't have any js or JavaScript responses, and that's what we actually need to build. When you look in the terminal, and you see that it says: "Missing Template", it is saying that we're missing the template in app/views/books/publish and publish needs to be English, it could be a js format, ECS format or a bunch of these other formats. Then, we also have handlers. These handlers are the extensions that you see when you have html.erb, you are using the html format here, and then the erb is the handler used to generate the html. You can mix and match these, and then in our case, we want to edit the app/views/books/publish.js.erb file, and here we can add in an alert just to say hello and see if it's working or not.

Now, if we refresh this page and click "Publish", we can see it says: "hello". That means that what happens is when you click this, JavaScript intercepts the click, it submits an AJAX PATCH request over, the application server, our rails app, returns this file, and that gets evaluated on the client's side. We're returning a string of JavaScript to the browser, and it evaluates the result. For convenience and learning this stuff, it's extremely simple, so you don't even have to write the AJAX request on the client side because jQuery UJS does that for you. With this, now, we can go into our

app/views/books/show.html.erb

<strong>Published At:</strong>
<span id="published-at"><%= @book.published_at %></span>

app/views/books/publish.js.erb

$("#published-at").text("<%= @book.published_at %>")

If we refresh this page once more, you'll see that if we click "Publish", it updates the time almost immediately, and we can keep doing that and see that it keeps getting published again. It's not loading a new page, it's loading this in the background through an AJAX request and updating the page with the JavaScript that we have. There's some gotchas with this, if you write incorrect or invalid JavaScript here, it's really hard to debug. When you click "Publish" and you have broken JavaScript there, nothing happens, you're not getting an error either. That means that the rails application is doing just fine, it's loading and rendering that publish.js.erb, but when it comes back to the browser to be evaluated, and you have bad JavaScript, it doesn't do anything wrong, it just doesn't work.

You also want to be careful here that the attributes on the model are being escaped properly, and that these don't actually insert JavaScript into your response, you don't want someone to add a malicious book in there and have a weird published_at date or description or name that actually has JavaScript in it, that gets rendered and executed in people's browser.

One last thing is a little fun bonus, if you add the data {disable_with: "Publishing..."} attribute that I covered in the last episode, if you add this to your link and you also make this publish take a little longer, if you refresh this page, now when you click "Publish", it changes to "Publishing" and when it's finished, it updates and the link goes back to normal. You're able to use this disable_with option the same way because UJS provides this functionality, and that's sort of the beauty of it, it allows you to do these sort of complicated things with only having to define what it should do, and it will automatically detect that and add that functionality into your rails app, which is really neat.

Discussion


Gravatar
Benjamin on

Thanks Chris, nice one


Gravatar
Maxime Sahroui on

Nice video, thanks!


Gravatar
Serguei Cambour (50 XP) on

Thanks a lot for sharing it! Very nice and clear!


Gravatar
David Lormor (10 XP) on

While most of this was review for me, it was nice to see a tutorial that went so far in depth on the inner workings of Rails. The details you included would have been so handy to know in my early days of learning Rails.

Also, looks like you're using the Yosemite beta for OS X...what do you think?

Gravatar
Chris Oliver (170,390 XP) on

Thanks David! I think learning how Rails works itself is the key that most people aren't teaching. Hopefully this approach helps a lot.


Gravatar
Victor Cortes on

One way to debug the JS response is in the Network tab of the web inspector. At least you can make sure the Javascipt is what you are expecting.

Gravatar
Chris Oliver (170,390 XP) on

Good point. That works well enough. I'm curious, do you know of any ways to actually have it throw Javascript exceptions to make debugging even easier?

I guess it does trigger the "ajax:error" callback with the error, so we could listen for that. Looks like this gives a few decent examples: http://blog.bigbinary.com/2...

Gravatar
Victor Cortes on

I didn't know.

That makes sense, will check it out!

Gravatar
NoInkling on

Yeah I think that's one of the main disadvantages when sending a .js.erb template that gets eval'd, rather than sending JSON (or an HTML or text snippet, or a header-only response, or whatever) and handling all the Javascript/Jquery stuff in your client-side assets.

This link says you can just take the response output (from the network tab as Victor said) and paste it into the console to see any Javascript errors:
http://www.alfajango.com/bl...

Gravatar
Jim Jones (10 XP) on

I'm looking to improve the js debugging in Rails 5 with this pull request:
https://github.com/rails/ra...

Gravatar
Chris Oliver (170,390 XP) on

Very cool. I really like this idea and certainly something that should be in Rails core if they continue supporting JS responses.


Gravatar
Dana Nourie (930 XP) on

Thank you so much, Chris. So, if I want all my articles to appear via ajax, can I do what you describe above, and use the remote: true in link_to?

Gravatar
Chris Oliver (170,390 XP) on

If you create a partial to render the list of articles, you can do that easily with this. If you have a div that contains all the articles:

<div id="articles"></div>

You can create a link to load them:

<%= link_to "Load Articles", articles_path, remote: true %>

And then your articles.js.erb can insert them into the page:

$("#articles").append("<%= escape_javascript(render partial: "articles") %>");

That would render the _articles.html.erb partial, insert it into the JS response, and then when it gets executed, it will add it to the end of the #articles div.

Make sense?

Gravatar
Dana Nourie (930 XP) on

Yes, I'll try that! Thank you!!!!


Gravatar

Gravatar
Anthony Candaele on

Hi, I successfully added jquery_ujs functionality to ajaxify a controller action 'hide':

class ResearchersController < ApplicationController

...

def hide
@researcher = Researcher.find(params[:id])
@researcher.toggle_visibility!
end

....

class

my hide.js.erb file looks like this:

$("#researcher_<%= @researcher.id %>").text("<%= toggle_visibility(@researcher) %>");

This all works fine.

The problem however is that my integration test now fail with a 'missing template' error

How can I make the 'missing template' error in my tests go away?

greetings,

Anthony


Gravatar
Tony Ramirez on

Very well done. This concept never clicked for me as it has after watching this. I hope to subscribe soon!

Is there some sort of turbo links consideration here? Whenever I refresh the page it goes back to this date '2000-01-01 02:50:14 UTC' but the rails server output shows me that everything should have worked see picture. But if I check the record in the console I see the date above.

I have a feeling it's something obvious

Gravatar
Chris Oliver (170,390 XP) on

My guess is that you might have a typo in your published.js.erb file causing it not to get executed. You can inspect the response in your browser and verify if you have any invalid code. Turbolinks or something could be caching the page to the old value but a refresh *should* display it with the latest value in your show action.

Gravatar
Tony Ramirez on

thanks for the quick reply, I hope to be a customer soon. Looks like the year is just refreshing to 2000-01-01 oddly and the actual time is behaving as it should. data-type is :time for published_at. Will need to do some more research.

Gravatar
Chris Oliver (170,390 XP) on

Hmm, it definitely looks like it's working right. The date appears to have reset to a default value though. I'm going to guess the time column is similar to datetime which also stores the date (according to your logs, it looks right). If it doesn't though, then that could be why your dates keep resetting to 2000.


Gravatar
Ray Magiore (10 XP) on

Thanks for the video. Never really got the hang of AJAX requests in Rails until now.


Gravatar
Daniel Clark (20 XP) on

Woah! A JavaScript file, as the view, runs on the client after the UJS click-through!? That's awesome! I always thought I would have to write an active AJAX submitter/listener on the client side to wait for a response. This is so cool! Thank you for putting in the effort to make these videos!


Gravatar
Guest on

Is there any particular reason you use method: :patch over method: :get ?

Gravatar
Chris Oliver (170,390 XP) on

I'm using patch because we want to modify the record. You don't ever want a GET request to update a record because that would be unsafe so either a PUT or a PATCH is best in this case.


Gravatar
Kelvin Atawura (70 XP) on

You done a video on how to load different views in your app using ajax. Similar to what you have this site where you load the whole content of the page using page. Abit like youtube.


Gravatar
zorlu on

Hi Chris. I used this method and pushed my git to heroku. However in the production mode, link_to wont route to PATCH but it routes to GET instead and JS.ERB file wont load. any fix ? (rails 4.1.4 ruby 2.0.0)

view.
<%= link_to "Kabul", publish_proposal_path("accepted"), :method => :patch, remote: true %>

route.
patch '/publish_proposal/:id' => 'charges#publish_proposal', as: :publish_proposal

publish_proposal.js.erb
$("#publishproposal").html("<%= escape_javascript(render partial: "charges_list", object: @charges) %>");

charges controller.
if branduser_logged_in?
id=current_branduser.id
status=params[:id]
@charges=Charge.where(:branduser_id => id).where(:status => status)

elsif instauser_logged_in?
id=current_instauser.id
status=params[:id]
@charges=Charge.where(:instauser_id => id).where(:status => status)

else
redirect_to root_path

end


Gravatar
Jordano Moscoso on

data disable with only works with remote: true? because i used on "new book" (also using an sleep) but nothing happen.


Gravatar
Alejandro Ventura on

I have a radio buttons group and before everything happens I need to validate if any option is checked, If so the request go on and Publish the book but if there's no option checked I need to send an alert to the user and stop the process.
How can I do that @excid3:disqus?


Login or create an account to join the conversation.