2

I have an HTML table with a list of instances of a model called Transaction. The first row, however, contains a form used to create a new transaction. Here is the _tranasctions_table.html.erb partial with the table:

<table id="transactions-table-id" class="table table-striped table-bordered table-hover">
<thead>
    <tr><th>Date</th><th>Description</th><th>Category</th><th class="transaction-value-cell">Value</th><th class="icon-cell"></th></tr>
</thead>
<tbody id="transactions-table-body">
    <tr id="new-transaction-row">
        <%= form_for @new_transaction, url: {action: "create"}, remote: true do |f| %>
          <td><%= f.text_field :date, id:"datepicker", readonly: true %></td>
          <td><%= f.text_field :description %></td>
          <td><%= f.text_field :category_id %></td>
          <td class="transaction-value-cell"><%= f.number_field :value %></td>
          <td class="icon-cell"><a href="#" class="fa fa-plus fa-lg text-muted text-center" ></a></td>
          <input type="submit" style="display:none;">
        <% end %>
    </tr>

    <% transactions.each do |trans| %>
    <tr class=<%= (trans.value < 0) ? "red-row" : "green-row" %> >
        <td><%= trans.date %></td>
        <td><%= trans.description %></td>
        <td class="transaction-category-name" style="color:gray;"><%= trans.category.name %></td>
        <td style="border-right:none;"><%= trans.value %></td>
        <td class="icon-cell"><a href="#" class="fa fa-trash fa-lg text-muted text-center"/></td>
        <td class="transaction-id" style="display:none;"><%= trans.id %></td>
    </tr>
    <% end %>
</tbody>
</table>
<%= paginate transactions %>

I can submit the form either by pressing 'Enter' or clicking on the 'plus' icon in the last table cell, and this is done with the jQuery command

$('#new_transaction').submit();

This works fine when the page is first loaded. However, there is a second table that lists categories. When the user clicks one of its rows, the transactions table is to be refreshed, showing only those that fall in the selected category. This is done via AJAX, so that only the transaction table is refreshed on the page. This was my attempt to do so:

function filter_by_category(category) {
  var category_name = category.html();
  $.ajax({
    method: "GET",
    url: "expenses/filter_category",
    data: { category: category_name }
  })
  .done(function( msg ) {
    $('tr').removeClass("selected-category");
    category.closest('tr').addClass("selected-category");
    $('#transactions_table_span').html(msg);
    add_listeners();
  });
}

The add_listener() function binds the jQuery submit command to the new form, among other things. And here is the relevant part of the controller that handles the request:

def filter_category
  @new_transaction = Transaction.new
  # other variables are set, such as @transactions, depending on the selected category
  render partial: 'transactions_table', locals: {transactions: @transactions}
end

The result is a correctly rendered table, with a seemingly correct form. However, when I try to submit from this newly rendered form, no parameters are sent.

What have I done wrong?

Update:

As asked, I'm including the view index.html.erb that holds both tables:

<%= javascript_tag do %>
    $(document).ready(function() {
      add_listeners();
      select_current_category();
      highlight_expenses_menu_tab();
    });
<% end %>

<div class="container-fluid">

    <div class="row">
        <div class="col-lg-8">
            <div class="panel panel-primary">
                <div class="panel-heading">Transactions</div>

                <div class="panel-body">

                        <%= form_tag({action: :upload}, multipart: true, class: "form-inline") do %>
                            <div class="form-group">
                                <label class="sr-only" for="transactions">File upload</label>
                                <%= file_field_tag 'transactions' %>
                            </div>

                            <%= submit_tag("Upload", class: "btn btn-default") %>
                        <% end %>
                </div>

                <span id="transactions_table_span">
                    <%= render partial:"transactions_table", locals: {transactions: @transactions} %>
                </span>
            </div>
        </div>

        <div class="col-lg-offset-1 col-lg-3">
            <div class="panel panel-primary">
              <div id="category-panel-heading" class="panel-heading">Categories</div>
                    <%= render partial:"categories_table", locals: {categories: @categories, category_name: @category_name, new_category: @new_category} %>
            </div>
        </div>
    </div>
</div>

This is how the call to filter_by_category() happens:

  • A click event listener is attached to the category rows with add_select_category_listener()
  • The listener calls another function, select_category(), whose behavior depends on whether there is a transaction currently selected. If there is, clicking on the category changes those transactions into the selected category.

Here are the two functions:

function add_select_category_listener() {
  $('tr.category-row > td').not('td.icon-cell').off('click');
  $('tr.category-row > td').not('td.icon-cell').click( function() {
    select_category($(this));
  });
}

function select_category(category) {
  var category_name = category.html();
  var number_selected_rows = $('.selected-transaction').length

  if (number_selected_rows == 0) {
    filter_by_category(category);
  } else {
    var selected_transactions = [];
    var html_selected_transactions = $('.selected-transaction > td.transaction-id');

    for (i = 0; i < html_selected_transactions.length; i++) {
      selected_transactions.push(html_selected_transactions[i].innerHTML);
    }
    change_to_category(category_name, selected_transactions);
  }
}

I checked and filter_by_category() is indeed being called. Also, I'm using Rails 4.1.7.

mper
  • 191
  • 8
  • Can you show some extra code? Wondering how filter_by_category is getting called, and wondering where #transactions_table_span is in the html. – Lanny Bose May 04 '15 at 14:35
  • Sure, I've added the extra code. – mper May 05 '15 at 00:23
  • So your server is receiving the the request, but with no data? If that's the case my first thought would be add_listeners() isn't binding to the new form properly, and is somehow submitting the old, empty form, or no form at all. Maybe try displaying that button in the table and see if it works. – Clark May 05 '15 at 16:06
  • What controllers are handling the requests? It appears as though the "filter" event is being handled by a "expenses" controller. I would infer that the initial table generation is getting handled by a "transactions" controller. If so, when the _tranasctions_table.html.erb partial gets rendered the second time url: {action: "create"} would be referring to a different method, that may not exist. You could confirm this by inspecting the form in browser and seeing if the URL changes. – Lanny Bose May 05 '15 at 16:28
  • Problem solved by moving the form outside of the table. If you notice the __transactions_table_ partial, the form is inside a row in the table. Even though the closing tags `<% end %>` encapsulates the form fields, the rendered HTML had a `
    ` with nothing in it and then the fields. I checked, and the same happens if I put in raw html. Oddly enough, the form was being correctly submitted before the AJAX call. I will try to get some references and explain this strange behavior better in the answer, but anyway thank you so much for the assistance, @Clark and @LannyBose.
    – mper May 06 '15 at 00:01

1 Answers1

0

The problem in my code is the same as this one: you cannot have a form as a child of a table, tbody or tr. It can contain a table or be put inside of a table cell. If you do what I did, the rendered HTML will have a form with nothing in it and all the inputs outside.

According to the W3C spec, inputs outside of a form are not valid for submission (except for inputs having the form attribute in HTML5) and shouldn't be sent in the request, so not sending any data is actually the correct behavior. The browser user agent, however, was at times accepting these inputs as part of the form and sending them even though it shouldn't.

Community
  • 1
  • 1
mper
  • 191
  • 8