Last Updated: September 29, 2021
·
192.5K
· sebastialonso

Rails 4: How to partials & AJAX, dead easy

Consider the basic following scenario...

A view with two columns:

  • one with links of Category model instances

  • the other empty but eager to show all the Item instances belonging to each Category.

And you want to show all of that without reloading the page. I had never played much with partials in Rails, but they are really really convenient.

So I have my index method

#items_controller.rb
def index
    @items = Item.all
    @categories = Category.all
end

For the sake of brevity, I will simplify the views. This way you can also understand the idea and apply it to your own views.

The index view contains two render calls

<!-- items/index.html.erb -->
<div class="grid">
  <%= render 'sidebar_menu' %>

  <%= render partial: 'item_grid', locals: { items: @items} %>
</div>

The category links in the sidebar_menu partial are something like the following:

<%= link_to cat.name, fetch_items_path(:cat_id => cat.id), :remote => true %>

fetch_items_path is the route that leads to our custom javascript method, which will be described next.

#config/routes.rb
...
get "/fetch_items" => 'items#from_category', as: 'fetch_items'

For more info on how to build custom routes, check Rails amazing documentation.

The :remote => true is the most important part here, it allows the whole Ajax business in the first place.

The item_grid partial looks like:

<div>
  <div id="items_grid" >
    <%= render partial: 'items_list', locals: {items: items}  %>
  </div>
</div>

The subpartial items_list just renders a list of div boxes to show our Item instances.

<% items.each do |item| %>
  <div class="item_box">
    ...
  </div>
<% end %>

Now we need the method that will do the AJAX magic. For simplicity you could have something like this:

#items_controller.rb
def from_category
    @selected = Item.where(:category_id => params[:cat_id])
    respond_to do |format|
        format.js
    end
end

Notice the type of format, there's no html view because we don't need it. We're going through JS.
Therefore, we need to create a javascript file, which will repopulate the div in the second column.

//views/items/from_category.js.erb
    $("#items_grid").html("<%= escape_javascript(render partial: 'items_list', locals: { items: @selected } ) %>"); 

Let's look at this line with care.
I'm rendering the same partial I was rendering in items#index, and the local variable for the partial is now the array of Item instances that match a given category. The difference is that I'm doing this through AJAX, so there's no need to reload the entire page.

27 Responses
Add your response

Hi, thanks for the article.
I don't understand the connection between 'fetchitemspath' and the 'fromcategory' action in your items controller.
In other words can you explain "fetch
items_path is the route that leads to our custom javascript method". I do not understand how that works.
Thank you!

over 1 year ago ·

In Rails, you control the routes in the routes.rb file. I will add it to the pro tip.

over 1 year ago ·

Thank you for writing this, it was super helpful. Just one question, what is the purpose of the :remote => true statement at the end of the link tag?

over 1 year ago ·

As it's pointed out just beneath the routes file, the remote: true hash tell Rails that the anchor element must respond in Javascript format (triggerin the Ajax logic), instead of the usual HTML.

Note that the controller method has to respond in this format also, otherwise is of no use.

over 1 year ago ·

This is the best tutorial / documentation I've seen for this. Making the content minimal and concentrating on the mechanism was a good idea. Well done!

over 1 year ago ·

Thanks nruth! I wrote this down because I couldn't find anything like it.
Hopefully, people won't have to do the same long research I had to do!

over 1 year ago ·

How would you do this if the target element wasn't a unique element with an id, but instead if there's multiple of them with the same class name, and you want to target just that one.

Somehow you have to get the clicked element. How do you do this when you use render js? It's possible if you do a full ajax.on click event, and call $(this) within that function, but otherwise, how do you do it using render js? Cause you may want to do something on the back end in the controller or model before executing the front end part.

over 1 year ago ·

This is where you have to get creative with your javascript. Instead of using the remote: trueoption, I'd use an ajax call, withdataType: 'script'` so I can capture the Javascript behaviour returned by the controller, and/or complementing it with the logic in the success function.
With the ajax call you have greater freedom for sending parameters with the call (maybe a data-something field with the id?). If you somehow decide to send an identifier, you'd have to capture it in the controller and then use it in the response to successfully identified the correct element.

over 1 year ago ·

i created a search page the problem is the search page has a partial which is not rendered in the start but when parameters filled and submitted then a table is rendered showing the result i am sending the parameters through AJAX but when the response comes back i am sending the response to the same controller for rendering adding an extra parameter so that it satisfies the if-else condition i placed in that controller so that the code which hit the back-end do not run ,in else block it renders the whole page search page again with form and table .i don't want to refresh the whole page,i want to render search table partial without refreshing. i tried to use respond_to |format| format.js and js.file for rendering in that div in search page but its not working ..so i am rendering whole page now. please help

over 1 year ago ·

This is nice, thank you.

over 1 year ago ·

Hi manikantasai, please write your question in StackOverflow, where you can add the code in context (which make the analysis easier) and reach a higher audience.

over 1 year ago ·

Thank you! I've seen a lot of tutorials, but this one is really dead simple!

over 1 year ago ·

Thanks for this useful tutorial.

over 1 year ago ·

Thanks! This was very helpful.

over 1 year ago ·

Beatiful post . It helped me so much. Thanks a lot !!

over 1 year ago ·

HI, and for the URL changing, nothing?

I have problem also with Turbolinks 3. How to fix?

History.pushstate? Like in the 246 Railscast?

over 1 year ago ·

Thanks man!

over 1 year ago ·

just finished implementing a dynamic form with partials, based on selecting from a set of links, this guide, first time, took me 15 minutes to follow and adapt. You've got my sincere thanks, it's amazingly clear.

over 1 year ago ·

Ruby is great but I hate using rails on the frontend because of this mess. Use angular and see how easy it is

over 1 year ago ·

You can use render partial: 'foo', collection: @items and then remove the manual iteration you do inside the item partial.

over 1 year ago ·

This was super helpful, thanks so much!

over 1 year ago ·

This is a great article. There is another method that I used with prototype helper that allows you to render the js response in the controller method without having to create a js.erb like so:
render update to |page|
page.replace_html div, partial...

Is there a possible way of doing something like this without prototype helper?

over 1 year ago ·

Really nice tutorial...............Good job

over 1 year ago ·

I am facing problem with is code: not really a problem but something that baffles me, something that i must have missed out, this code works fine in development mode ie. rails s but breaks when i on production environment ie, rails s -e production

http://stackoverflow.com/questions/40069866/getting-html-response-wrong-in-production-json-javascipt-response-correct

over 1 year ago ·

Need to add !!!!!!
format.js { render layout: false}

over 1 year ago ·

Sebastian, nicely done article. I'm encountering an "ActionController::UnknownFormat" error, however, on the respond_do |format| line. I've researched this and can't find a fix. Any ideas what might cause this?

over 1 year ago ·

Thanks for this!

over 1 year ago ·