2

I can't believe I haven't found other questions to answer this but I've searched high and low and can't find anything that really answers my question.

Basically, I have an Expenses page in which I want to display all Expenses in a given month in a table. I have a month and year select and right now I have it working by adding month and year parameters to the url and going to that href with a little javascript. I would like to avoid a page refresh however and just update the table when the select box value is changed. As I understand it, because I'm not using a form, I can't use :remote => true and thus have to use AJAX. My research has brought me to set things up as the following.

This is my JQuery with an AJAX call:

$(document).ready(function() {

    $('#monthSelect').change(function() {
        // alert("yay");

        var m = $(this).val();
        var y = $("#yearSelect").val();
        $.ajax({
            type: "GET",
            url: "/expenses",
            data: { month : m, year : y } ,
            success: function(data) {

            },
            dataType: "json"
        });
    });
});

#monthSelect and #yearSelect are the ids of my two select boxes. This part seems to work. When I change the value of the month select box, it sends the GET request to "/expenses" with the correct month and year in the params.

/expenses naturally routes to my index action. Here is the part where I'm confused.

def index
    @expenses = # Code that gets the expenses that correspond to the month and year in the params.
    # This part works. I've tested it and @expenses are all the records I want

    respond_to do |format|
        format.html
        format.json { render json: @expenses, status: :ok } #?????
    end
end

So I guess my primary question is how the json part works, and if I should even be using json in the AJAX. Tutorials and videos I've looked at all seem to imply that getting the response in json is the way you're supposed to do it, but I'm not sure what "rendering json" even means. My understanding is that passing @expenses, it basically will render what @expenses.to_json returns, which is just a bunch of json, key-value pairs. How do I take that and make a table?

A friend of mine that has done plenty of AJAX but no Ruby on Rails said that I can just write the HTML in the AJAX success function

...
success: function(data) {
    $("#expenses-table").html("HTML of my Table");
}

But this just doesn't seem like the Rails way, to put a bunch of Javascript code that just puts HTML into a string. And then what does the render: json do if I'm just putting the HTML in the AJAX? I did see an answer here on Stack Overflow that had

success: function(data) {
    $("#expenses-table").html(data);
}

which would be nice, since data does have all the json I want. But obviously that doesn't just work like that.

So if I could get some clarification on this whole mess, if I'm even approaching it right with JSON, or if I should just write the HTML in the AJAX, that would be much appreciated.

EDIT 1: Here's my current table code:

<table id="expenses">
    <thead>
      <tr>
        <th>Description</th>
        <th>Amount</th>
        <th>User</th>
        <th>Date</th>
        <th colspan="3"></th>
      </tr>
    </thead>

    <tbody>
      <% @expenses.each do |expense| %>
        <tr id="expenses-row">
          <td><%= expense.description %></td>
          <td><%= number_to_currency expense.amount %></td>
          <td><%= expense.user.name %></td>
          <td><%= expense.date.strftime("%m/%Y") %></td>
          <td><%= link_to 'Show', expense %></td>
          <% if expense.user == @user %>
            <td><%= link_to 'Edit', edit_expense_path(expense) %></td>
            <td><%= link_to 'Delete', expense, method: :delete, data: { confirm: 'Are you sure?' } %></td>
          <% else %>
            <td></td>
            <td></td>
          <% end %>
        </tr>
      <% end %>
    </tbody>
  </table>

Basically just the standard scaffold table. Most of the changes were just css. Here's what the json looks like for one record

[{"id":8,"description":"This is a test","amount":"35.0","user_id":1,"date":"2014-10-01","created_at":"2014-10-03T07:07:53.412Z","updated_at":"2014-10-03T07:07:53.412Z","receipt_file_name":null,"receipt_content_type":null,"receipt_file_size":null,"receipt_updated_at":null}]

I guess I'd rather do the code in ERB/ruby as I'm more immediately familiar with it. Though it almost feels a little pointless to have the json since @expenses is what I already use in my current table code. However, wouldn't render :file "expenses_table.html.erb" refresh the whole page? Or how would it know where to render just that table and replace what was previously there? It almost sounds easier to respond to format.js and make an index.js.erb because I know I could replace the html with my partial. But I haven't tried that as all the examples I've seen respond to format.json. If it sounds like I'm just confused and don't know what's going on it's because I am. I just need some clarification. Thanks for your help!

Naty722
  • 431
  • 4
  • 12
  • **However, wouldn't render :file "expenses_table.html.erb" refresh the whole page?** No because your browser is not making the request--the javascript is making the request, and therefore the javascript will receive the response, then the javascript can insert pieces of the data into the appropriate spots in the html. **It almost sounds easier to respond to format.js and make an index.js.erb** As far as I can tell, you don't want to return javascript code--you either want to return some data in the form of key/value pairs, or a complete html table. – 7stud Oct 04 '14 at 18:08
  • I edited my post to add answers to your questions. – 7stud Oct 05 '14 at 17:07

1 Answers1

4

if I should even be using json in the AJAX.

You can use any type of String you want. The problem is parsing the string on the javascript side to extract each piece of data. For instance, you could respond with a String such as:

"exp1=10, exp2=20, exp3=30"

Then your javascript code can split the String on ", ", then the "=" sign, then use the pieces to create an object, then you can use the object to refer to the data. On the other hand, if the data is sent as a JSON string, all you have to do in the javascript is:

var obj = JSON.parse(data);

...and obj will be something similar to a ruby Hash, where you can look up keys to get values.

So I guess my primary question is how the json part works...My understanding is that passing @expenses, it basically will render what @expenses.to_json returns, which is just a bunch of json, key-value pairs. How do I take that and make a table?

That's correct. You make the table using your programming skills.

A friend of mine that has done plenty of AJAX but no Ruby on Rails said that I can just write the HTML in the AJAX success function

Sure, but the trick is still to programmatically take the data in the json and insert it into an html table. If you want to go that route, you could return a complete table as the response, so that you can use ERB/Nokogiri/ruby on the server side to insert the data in the table. Then you would write something like:

@expenses = ....
render :file "/some/dir/expenses_table.html.erb"

Another approach might be to give each <td> in the table an id attribute that is equal to a key in your json data. Then you can use javascript to loop over each key in the json data, look up the <td> with that id, then replace the entry with the value in the json data corresponding to the key.

If you want more concrete suggestions, you'll have to post a small example of your table's html as well as what @expenses.to_json looks like.

By the way, the jQuery ajax() function has too many features, so all of the common ajax requests have shortcut functions, where the relevant options are filled in for you, e.g. getJSON(), where you just specify the url, data, and the success function. In this case, that doesn't save you much work, but maybe the function name is more descriptive than 'ajax'.

Response to comment questions:

I guess my confusion then is which javascript is this? Is this the original AJAX call?

Yes. That is the only javascript you need to code.

Should that be in a js.erb file then for me to call some ruby code?

No. You would use a js.erb file if you want to create some js using ruby (that is what erb is for), AND you want to return js code as the response. We've established that you don't want to return javascript as the response. However, using rails to setup ajax is very confusing, so I may not be understanding the gist of your questions. It's much simpler not to use rails to generate ajax javascript.

As far as I understand things, if you put remote: true in your rails html helper when you create an html form, then when the form is submitted, a request is sent to your rails app with an Accept header of text/javascript. That means if your action has a respond_to block with a format.js line, that line will execute, and you can send some javascript back to the browser, e.g. a .js.erb file that contains code that performs an ajax request to get your expense data. Your browser will immediately execute the returned js, which will send an ajax request back to the server asking for the expense data. Note that the rails way is inefficient: the browser has to send two requests to the server to accomplish one ajax request: one request is sent to the server to get the js code that will make an ajax request for the expense data, then the browser executes the ajax code which sends another request to the server to get the expense data.

Or can I call a javascript file in the respond to?

It looks to me like you want to replace the existing table with a whole new table, i.e. you are not changing two or three <td>'s in the existing table, which should makes things very easy. In fact, as far as I can tell, you can use the same erb code that you used to create the existing table to create the new table. However, your index action's response to an ajax request should only return the table--not the whole page. That suggests you should put the code that creates the table into a separate template, a partial:

views/shared/_table.html.erb:

<table id="expenses">
<thead><th>Description</th><th>Amount</th><th>Date</th></thead>
<% @expenses.each do |expense| %>
<tr> 
  <!-- I used an abbreviated Expense model: -->
  <td><%= expense.description %></td>
  <td><%= number_to_currency expense.amount %></td>
  <td><%= expense.date %></td>
</tr>
<% end %>
</table>

Then you include the partial in views/expenses/index.html.erb:

<h1>Expenses#index</h1>
<p>Find me in app/views/expenses/index.html.erb</p>

<div>
<select id='expense_day'>
<option value="all">All</option>
<option value="2014-10-5">10/5/14</option>
<option value="2014-10-6">10/6/14</option>
</select>
</div>

<%= render "shared/table" %>   <!-- HERE -->

By default, render() will look for a partial in the app/views directory called shared/_table.html.erb.

Those two files combined (inserted into the application layout) make up the index.html.erb view. Because the table is now a separate template, just the table can be rendered in response to an ajax request:

class ExpensesController < ApplicationController

  def index
    respond_to do |format|
      if request.xhr?  #Is this an ajax request?
        target_date_str = params['target_date']

        if target_date_str == "all"
          @expenses = Expense.all
        else
          @expenses = Expense.where(date: Date.strptime(target_date_str, "%Y-%m-%d"))
        end

        #Render just the table for the ajax request:
        format.html { render partial: "shared/table" }

      else  #then not an ajax request
        @expenses = Expense.all
        format.html   #By default renders index.html.erb 
      end
    end

  end

end

respond_to() looks at the Accept header in the request and based on that chooses a matching format:

format.html
format.json
format.xml
etc.  

I examined the request sent by jQuery's .get() ajax function, and it sends a request with an Accept header of text/html as the highest priority, so format.html will be chosen in the respond_to block. Edit: I originally wrote the index action thinking there would be a format.json line for the ajax request, and a format.html line for a non ajax request. But because the index action now returns html in both cases, the respond_to() block isn't needed:

class ExpensesController < ApplicationController
  def index
    if request.xhr?  #if an ajax request...
      target_date_str = params['target_date']

      if target_date_str == "all"
        @expenses = Expense.all
      else
        @expenses = Expense.where(date: Date.strptime(target_date_str, "%Y-%m-%d"))
      end

      render partial: "shared/table"  #Overrides rails default operation which renders index.html.erb

    else  #then not an ajax request
      @expenses = Expense.all  #rails defaults to rendering index.html.erb
    end

  end

Here is the javascript I used:

<script>

$( document ).ready(function() {
  $("#expense_day").on("change", function() {

    var selected_date = $(this).val();

    $.get( 
      "/expenses", 
      { target_date: selected_date }, 
      function(table) {
        $("#expenses").replaceWith(table)  
      }
    );

  });
});

</script>

    <h1>Expenses#index</h1>
    <p>Find me in app/views/expenses/index.html.erb</p>

    <div>
    <select id='expense_day'>
    <option value="all">All</option>
    <option value="2014-10-5">10/5/14</option>
    <option value="2014-10-6">10/6/14</option>
    </select>
    </div>

    <%= render partial: "shared/table", expenses: @expenses %>

I put the javascript in the index.html.erb view, which should make it clear that the javascript is part of the html page. However, you can remove the javascript from index.html.erb and put it in another file--but eventually the js has to find its way back into the index.html.erb file. Here are some links that discuss alternate locations for the javascript:

Why is rails not rendering the .js.erb file?

Best way to add page specific javascript in a Rails 3 app?

Rails has something called the asset pipeline, which is a method for cramming all the javascript and css into as few lines as possible by removing all the whitespace. Because the files are smaller, your browser can load them faster. To take advantage of that minifying, you have to put your assets, i.e. your javascript and css files, into certain directories. That is really not something you need to worry about because your javascript code is not thousands of pages long.

Community
  • 1
  • 1
7stud
  • 46,922
  • 14
  • 101
  • 127
  • Thanks for the detailed response. I've edited my question with some more code and questions. Thanks! – Naty722 Oct 04 '14 at 17:39
  • Wow, thanks again for the detail. I think I've finally figured it out. I figured out the first part after reading your comment a couple more times, but then got confused, because if I'm using json as the datatype, then I can't return html (which is what rendering the partial would do). Of course, if I have my ajax respond to html, then I would have to respond with the whole page (at least that's what I thought). However, your `if request.xhr?` solves that as now I can render just the partial if it's an ajax request, but the full page if not. I think I finally got it. Thanks! – Naty722 Oct 06 '14 at 06:01