I recently spent a while learning how to do custom AJAX queries with jQuery and Rails 3.2.14. While I'm well within the realm of a Junior Rails developer, I knew NOTHING about jQuery and only slightly more about AJAX. This post is going to tell a story, but I'm also going to show you some things I learned and haven't seen well documented elsewhere.
While researching and rapid prototyping code, I realized that there are a lot of pitfalls when you're glueing rails and jquery's AJAX functionality together. The biggest pitfall is silent failure in your .js.erb file, but there are plenty of others along the way, which
Steve Schwartz over at AlfaJango.com has a great guide on. As I compiled information from the various sources, I realized the documentation for rails AJAX functionality is firmly couched in
form_for and "remote: true" in particular. In fact, "remote: true" works for links, buttons, and even form_for's cousin, form_tag. Sadly "remote: true" doesn't work for anything custom, so almost all the documentation out there leaves you wondering. Sure there are some good guides out there like
Steve Schwartz's over at AlfaJango.com but even they leave some of the components out.
Custom AJAX methods rely on several components in rails 3.2.14; the parent view, the jQuery & AJAX component itself, the controller method, the supporting .js.erb file and the partial being injected into the page. I won't go so far as to say these are all necessary and this is the only way, but these are the pieces that had to come together for me. These combined to form a nice little loop complete with silent failures and some very loud failures.
Using jQuery, an AJAX call may be made by any function for any reason, this is delightfully powerful, but can also be troublesome. I was trying to fire an AJAX call when the user changed the value in a select box. This required that the parent view containing the select boxes render first. You can see an example select box below taken directly from my app.
1: <select id="station_select">
2: <% station_list = current_user.market_item_summaries.pluck('station_id').uniq %>
3: <option value="All">All Stations</option> <!-- The top option is the default option -->
4: <% station_list.each do |s|%>
5: <option value="<%= s.to_s %>"> <%=Station.where("station_id = ?", s).pluck("name")[0]%> </option>
6: <% end %>
7: </select>
So this select box is rendered when the user visits the parent view. You'll see that it has an 'id' value and each option has a dynamic value. The 'id' value is standard HTML and is used by the jQuery to determine what to watch. jQuery can also watch css classes and a few other things, but I prefer ids personally. In order to fire off a jQuery AJAX call I elected to use the 'change' bind, though jQuery
has a pile of really nifty events you can use for your functions. So when a user selects an option, the jQuery below is invoked by the browser.
1: $(document).ready(function(){
2: //On change, trigger this function
3: $("#station_select, #listing_character, #owner, #type").change(function(){
4: //Load all select's values into variables
5: var ss = $("#station_select").val();
6: var ls = $("#listing_character").val();
7: var ow = $("#owner").val();
8: var ty = $("#type").val();
9: //Fire an AJAX call to marketsummaries/filter
10: $.ajax({
11: url: "marketsummaries/filter", type: "GET",
12: //Pass in each variable as a parameter.
13: data: { station_id: ss,
14: listing_character_id: ls,
15: owner_id: ow,
16: type: ty }
17: });
18: });
19: });
You can see the comments explaining the cycle of events in rough detail, but let's break it down even more.
1: $(document).ready(function(){
2: });
The above function wraps our entire jQuery function set. This ensures that the document is fully loaded before any functions are called. While this may seem ancillary at first, ensuring the page is fully loaded is an excellent means of heading off bugs at the pass.
1: $("#station_select, #listing_character, #owner, #type").change(function(){
2: });
After the document is ready, you see the function declaration. jQuery distinguishes itself from JavaScript with a dollar sign before all functions. Knowing this you can see that my jQuery function actually includes several plain JavaScript elements. That aside, within my function declaration you can see that I am calling the selector "#station_select", jQuery uses the number sign to distinguish an id from a class, which would be represented with a dot. ".station_select" for class vs "#station_select" for id. You also see that a comma may be used as a delineator between multiple selectors. Each selector in the above snippet selects an id. The function is then bound to the change event, so when the value of either of the <select>'s changes, this function will fire.
1: var ss = $("#station_select").val();
2: var ls = $("#listing_character").val();
3: var ow = $("#owner").val();
4: var ty = $("#type").val();
In the above snippet you can see a combination of regular Javascript and jQuery. "var ss =" is JavaScript, declaring a variable, meanwhile everything right of the equals operator is jQuery. Each variable is assigned the current value of the <select> with the listed id. While this may be rather straight forward, the fact that jQuery and JavaScript can be so easily intermingled may not be as forthcoming as one might expect.
1: $.ajax({
2: url: "marketsummaries/filter", type: "GET",
3: //Pass in each variable as a parameter.
4: data: { station_id: ss,
5: listing_character_id: ls,
6: owner_id: ow,
7: type: ty }
8: });
Here you can see the actual AJAX call itself. You'll notice that ajax is actually a
function within jQuery. The ajax function has a series of arguments that it takes, the primary arguments are; 'url', 'type', 'data' and 'success', the last of which I will explain in greater detail at the end. All arguments are comma delineated. The url arg determines where the AJAX call will be pointed towards, you'll notice this makes it very simple to dovetail it with a controller method. The type arg supports all four REST types and defaults to a GET call, I chose to explicitly define it. The data arg takes a comma delineated hash and will pass the data as query string parameters to the target url. Once again, you'll notice that this dovetails quite well with rails controllers.
1: data: { station_id: ss,
2: listing_character_id: ls,
3: owner_id: ow,
4: type: ty }
Each variable is passed as an element of the hash, this means that 'ss' will be accessible in the controller as params[:station_id]. The exact query passed to the server will look like;
1: marketsummaries/filter?station_id=60000025&listing_character_id=All&owner_id=All&type=All
Once the AJAX call is made, your controller must be waiting to receive it. This requires that you have a supporting route in your routes.rb file and a properly named method in your controller.
1: routes.rb
2: match "/marketsummaries/filter", to: 'MarketItemSummaries#filter', via: :get
1: market_item_summaries_controller.rb
2: def filter()
3: @input = {"station_id" => nil, "listing_character_id" => nil, "owner_id" => nil, "type" => nil}
4: @input["station_id"] = params[:station_id]
5: @input["listing_character_id"] = params[:listing_character_id]
6: @input["owner_id"] = params[:owner_id]
7: @input["type"] = params[:type]
8:
9: (Whatever your method needs to do, build a query, what have you.)
10:
11: respond_to do |format|
12: format.js
13: end
14: end
Without the routes entry, the rails router will reject the AJAX call with a 403 forbidden leaving your controller's method untouched. The controller method itself must share the name specified in the AJAX call's url. Above you can see me taking each parameter and passing it to an instance hash. From there I can work with the hash more easily then individual parameters.
1: respond_to do |format|
2: format.js
3: end
This last function is extremely important and often missed. "respond_to" allows you to specify what type of response the controller is to send to a given call. You can specify html, xml, pretty much any format. In this case, the controller is going to respond with JavaScript. Because there is no additional block after "format.js", rails defaults to checking for a file "<method_name>.js.erb", in this case "filter.js.erb".
1: filter.js.erb
2: $('#corporationtable').empty().append("<%= escape_javascript(render(:partial => 'corporation_table', locals: {mis: @mis})) %>");
Like .html.erb files, .js.erb files are run through the embedded ruby pre-processor. This means you can dynamically embed different JavaScript commands into the page before it is actually run through the user's browser. This particular .js.erb file has some important parts though;
1: $('#corporationtable')
The above component uses jQuery to target a specific id. While it is possible to target an id on a partial, the jQuery selectors break when dom elements are reloaded through AJAX. For this reason "#corporationtable" is a div on the parent view, not on the partial that is going to be loaded.
1: .empty().append()
Normally when you go looking for proper syntax in a jQuery/AJAX .js.erb file, you'll see .html(), not .empty().append(). During my research I stumbled across an excellent, if tangential,
blog post by Joe Zim explaining that while .html() does work, the reason it works is not officially supported. This means the jQuery team may randomly patch that functionality out and not tell anyone, so like him, I've elected to use .empty().append() directly rather then through .html(). It is important to note that .empty() destroys all child elements within the specified dom element.
1: .append("<%= escape_javascript(render(:partial => 'corporation_table', locals: {mis: @mis})) %>");
The
.append() method takes html dom elements as well as html strings as arguments. Combining this with erb to use rails' render to generate a partial and append it to the div specified earlier allows for any element of the page to be updated. You'll notice that the render itself is wrapped in "escape_javascript()". This is done because the majority of partials being rendered will contain html elements that would otherwise escape the double quotes that .append() uses to delineate between start and finish.
Kikito over at Stack Overflow does an excellent job explaining this.
1: locals: {mis: @mis}
The last crucial element within the .js.erb file is "locals:". The above code takes the variable "@mis" from my controller and makes it available to the partial as "mis". There are two key reasons to use locals, firstly; when rendering a partial in multiple places locals allow you to ensure you're passing it all necessary variables. Secondly; a partial being loaded into an existing view will not have access to the variables that the parent view does.
With filter.js.erb in place, the last necessary element is the partial itself. I want my users to be able to see the partial before selecting options from the <select> boxes, so the partial is actually loaded as an element within the parent view; show.html.erb.
1: show.html.erb
2: <div id="corporationtable">
3: <%= render :partial => 'corporation_table', locals: {mis: @mis}%>
4: </div>
As discussed earlier in filter.js.erb, "corporationtable" is the target of the jQuery append() function. This means the new partial is rendered in place of the old one. The partial itself can be seen below.
1:
2: <div class="corptable">
3: <table class="table table-bordered">
4: <thead>
5: <tr>
6: <h2>Corporation Item Summaries</h2>
7: </tr>
8: </thead>
9: <tr>
10: <table class="table table-condensed" style="border-collapse:collapse;"> <!-- Accordioned Table -->
11: <tbody id="corp_tbody">
12: <% mis.each do |n|%>
13: <% if n.entity == 1 %>
14: <tr data-toggle="collapse" data-target=".demo<%=mis.index(n)%>" data-target="#data4" class="accordion-toggle"> <!-- First 2 rows are the mis -->
15: <td><b> <%= Item.where("type_id = ?", n.type_id).pluck('name')[0] %> </b></td>
16: <td> <%= Station.where("station_id = ?", n.station_id).pluck('name')[0] %> </td>
17: <td> <%= Character.where("char_id = ?", n.char_id).pluck('name')[0] %> </td>
18: <td class="text-success"> <% char = current_user.characters.where("char_id = ?", n.char_id) %>
19: <%= Corporation.where("character_id = ?", char).pluck('name')[0] %> </td>
20: <td <% if n.bid == false %>
21: class="text-success">Sell
22: <% else %>
23: class="text-error">Buy
24: <% end %> </td>
25: </tr>
26: <tr data-toggle="collapse" data-target=".demo<%=mis.index(n)%>" data-target="#data4" class="accordion-toggle"> <!-- Second line of the mis -->
27: <td> APP <%= n.average_purchase_price %> </td>
28: <td> ASP <%= n.average_sale_price %> </td>
29: <td> APM <%= n.average_percent_markup %> </td>
30: <td> TVE <%= n.total_vol_entered %> </td>
31: <td> TVR <%= n.total_vol_remaining %> </td>
32: </tr>
33: <% mo = MarketOrder.where("market_item_summary_id = ?", n.id) %>
34: <% mo.each do |m| %>
35: <tr> <!-- Each Row after that with TD's classed as hiddenRow are individual orders -->
36: <td class="hiddenRow">
37: <div class="accordian-body collapse demo<%=mis.index(n)%>"> <%= Item.where("type_id = ?", m.type_id).pluck('name')[0] %></div>
38: </td>
39: <td class="hiddenRow">
40: <div class="accordian-body collapse demo<%=mis.index(n)%>"> <%= m.price %></div>
41: </td>
42: <td class="hiddenRow">
43: <div class="accordian-body collapse demo<%=mis.index(n)%>">%markup</div>
44: </td>
45: <td class="hiddenRow">
46: <div class="accordian-body collapse demo<%=mis.index(n)%>"> <%= m.vol_entered %></div>
47: </td>
48: <td class="hiddenRow">
49: <div class="accordian-body collapse demo<%=mis.index(n)%>"> <%= m.vol_remaining %></div>
50: </td>
51: </tr>
52: <% end %>
53: <% end %>
54: <% end %>
55: </tbody>
56: </table>
57: </tr>
58: </table>
59: </div>
While the partial itself is nothing special, there is one element I would like to draw attention to. Line 11 contains a table body element with the id "corp_tbody". If you look over the code, you'll realize that if there are no valid mis values, corp_tbody will be entirely blank. The architecture of my app does not allow this entire partial to be easily removed. However it is possible to use jQuery and the .ajax() function's success method to hide the entire partial any time corp_tbody is empty.
1: $(document).ready(function(){
2: //On change, trigger this function
3: $("#station_select, #listing_character, #owner, #type").change(function(){
4: //Load all select's values into variables
5: var ss = $("#station_select").val();
6: var ls = $("#listing_character").val();
7: var ow = $("#owner").val();
8: var ty = $("#type").val();
9: //Fire an AJAX call to marketsummaries/filter
10: $.ajax({
11: url: "marketsummaries/filter", type: "GET",
12: //Pass in each variable as a parameter.
13: data: { station_id: ss,
14: listing_character_id: ls,
15: owner_id: ow,
16: type: ty },
17: success: hider
18: });
19: });
20: function hider() {
21: //Hide or Show table divs based on whether the table's tbody has content.
22: if ($.trim($("#corp_tbody").html())=='') {
23: $("#corporationtable").hide()
24: }
25: if ($.trim($("#corp_tbody").html())!='') {
26: $("#corporationtable").show()
27: }
28: if ($.trim($("#char_tbody").html())=='') {
29: $("#charactertable").hide()
30: }
31: if ($.trim($("#char_tbody").html())!='') {
32: $("#charactertable").show()
33: }
34: }
35: });
At line 20 you can see I've built a JavaScript function and taken advantage of jQuery's easy interoperability with JavaScript. The hider() function calls a series of JavaScript if branches (jQuery actually
lacks an if/else mechanism entirely), within the first branch at line 22 I check for corp_tbody being empty. While jQuery does have
couple variants on an empty method, according to
Serge Shultz over at Stack Overflow, Chrome and Firefox don't play nice with those.
Up at line 17 you can see a very simple addition to the AJAX args list. "success: hider" calls the hider function when the AJAX request completes successfully. That is when the server returns an HTTP 200 to the client. Should the client not receive a 200 code, hider() will never fire. It is important to note that there are no parenthesis in "success: hider", I don't know why that is, but I do know that the function fails to perform reliably with parenthesis.
TL;DR
Asynchronously rendering a partial with jQuery/AJAX/Rails 3.2.14 requires a jQuery AJAX call, supporting routes, a controller with configured method, a properly formatted .js.erb file, a partial, and a target div on the parent view.
The AJAX call must be triggered, the URL it hits must be a valid route, the targeted controller method must be configured to respond with a JavaScript response, this will return a .js.erb file named <method>.js.erb which must contain a target div id and a properly escaped render command. The target div id should be present on the view that triggered the AJAX call, if it is not the partial might not reliably generate.
You can view the full code for my app's jQuery/AJAX/Rails interactions at the following links
routes.rb
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/config/routes.rb
Market Item Summaries Controller
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/controllers/market_item_summaries_controller.rb
Market Item Summaries JavaScript
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/assets/javascripts/market_item_summaries.js
Parent View
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/views/market_item_summaries/show_msi.html.erb
Corporation Table Partial
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/views/market_item_summaries/_corporation_table.html.erb
Character Table Partial
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/views/market_item_summaries/_character_table.html.erb
filter.js.erb
https://github.com/islador/market_monitor/blob/viewsMarketSummaries/app/views/market_item_summaries/filter.js.erb