Search code examples
jqueryruby-on-railsujs

Rails: cannot submit a remote form that was loaded via Ajax


Goal

I have a page with a list of items, coming from a Rails backend. I want to be able to edit a row in that list, using Ajax calls via Rails UJS.

Approach

I've added an edit button to the end of each row. The edit button is a link_to ... :remote => true. Clicking it loads the list again, but with the selected row in edit mode. The editable row is embedded in a form ... :remote => true. The save button in that row is a submit button.

index.html.haml

#editor
  %table
    - @items.each do |item|
      %tr
        = render :partial => 'row', :locals => { :item => item }

_row.html.haml

... 
%td // a number of columns with attributes
...
%td
  = link_to t("actions.edit"), edit_item_path(item), :remote => true
  = link_to t("actions.delete"), item_path(item), :remote => true, :method => :delete, :data => { :confirm => "Are you sure?" }

edit.html.haml

#editor
  %table
    - @items.each do |item|
      %tr
        - if item == @item
          = form_for @item, :url => item_path(@item), :remote => true, do |f|
            = render :partial => "row_form", :locals => { :f => f }
        - else
          = render :partial => 'row', :locals => { :item => item }

_row_form.html.haml

... 
%td // a number of columns with editable attributes
...
%td
  %button{ :type => "submit" }=t("actions.save")
  = link_to t("actions.cancel"), items_path, :remote => true

Ajax response handling

$("#editor").on("ajax:success", function(event, data, status, xhr) {
  $("#editor").html($(data).find("#editor").html());
});

Problem

When I load a list page in edit mode /items/12/edit, the row of item 12 is editable. Clicking the save button submits the form via Ajax correctly and loads the /items index partial, replacing the editable list with jQuery. Clicking the edit button again, loads the edit page again (e.g. /items/12/edit), with the embedded form. Only this time, the form does not get submitted anymore when the save button is clicked. It seems the submit event handler is not attached to the dynamically loaded remote form.

Question

How can I submit a remote form loaded via Ajax, preferrably using the Rails UJS approach?

Duplicates

I know there are duplicates of this question, but none of them were answered. I hope someone finally comes up with a definite answer.


Solution

  • I found the problem! Thanks to the hints in the answers of both @Jake Smith and @Parandroid. Here's what I found in two steps.

    Finding 1

    While getting the ajax:success to be fired did not seem to be the problem, it did look like the form handling did not work 100% correctly just on the $("#editor") selector. At the very least that needed to be $("#editor form"), but that might not work if we start from the index page, which doesn't contain the form yet. So the approach suggested by @Jake Smith seemed to be the most robust way to go after all. This resulted in:

    edit.html.haml

    #editor
      = render :partial => "edit"
    

    edit.js.erb

    $('#editor').html('<%= escape_javascript render("edit") %>');
    

    _edit.html.haml (still not working)

    %table
      - @items.each do |item|
        %tr
          - if item == @item
            = form_for @item, :url => item_path(@item), :remote => true, do |f|
              = render :partial => "row_form", :locals => { :f => f }
          - else
            = render :partial => 'row', :locals => { :item => item }
    

    But still this did not result in a working submit button. Until I discovered what went wrong...

    Finding 2

    The solution above did give the submit button plain old POST behavior, which the server did not like, since it expected a PUT to reach the update action. Rails does this by generating a hidden _method field with the value PUT. I found out that rails generates this field (and a couple of other crucial hidden fields) on the very top of the _edit.html.haml partial and not inside the form tag! So I moved the form to the top of the partial and it worked!

    _edit.html.haml (WORKING!)

    = form_for @item, :url => item_path(@item), :remote => true, do |f|
      %table
        - @items.each do |item|
          %tr
            - if item == @item
              = render :partial => "row_form", :locals => { :f => f }
            - else
              = render :partial => 'row', :locals => { :item => item }
    

    Who would have guessed...