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

5

8

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.

Srini answered 17/5, 2013 at 16:33 Comment(2)
Do you get any errors in your dev tools window? If so, what are they? Also, if you haven't already, I would download RailsPanel if you are using Chrome. It provides some pretty good feedback on your rails requests that you make.Abnaki
Looking at your section entitled Ajax response handling, I think you might want to try and use event delegation like they exemplify at the bottom of this page. This, of course, would mean you would need to use a class attribute instead of an id attribute to identify your edit form, I believe.Abnaki
S
2

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...

Srini answered 17/5, 2013 at 22:41 Comment(2)
Your problem is due to html rules of not allowing the placing of a form inside of a table. That's why you had to move your form_for outside of it. See this link https://mcmap.net/q/98969/-form-inside-a-tableDecommission
I'd like to add that I had the same issue with a plain rails from (you know all that stuff with haml and simple forms is quite distructing for those who do not use it). My form was wrapped into some divs, so after removing this wrappings, it magically started working! Thanks a lot!Peddler
T
3

Problem is that DOM was modified by javascript and callbacks (on ajax success in your example) was initialized only on DOM init. So when DOM changed (in part of #editor) callbacks didnt work.

So you shoul re-initialize this callback any time you change the DOM with javascript

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

Some time ago jQuery had function called "live". It worked like "on" but tracked the DOM changings. But in current version of jQuery this function is deprecated cause it was slow.

Hope you understand my bad English =)

Tanker answered 17/5, 2013 at 17:32 Comment(0)
A
2

The way I've gotten this to work is to use the built in Rails functionality for this:

  1. In your controller's edit action, add to the respond_to block the line format.js. This allows you to create an edit.js.erb file that you can use to load the edit form into your DOM.
  2. Then in your update action of the same controller, again add format.js to the respond_to block, and have an update.js.erb file that does the necessary DOM manipulation via jQuery.

I'm sure this is explained further in tutorials online, but where I finally understood how it worked without having to call jquery ajax methods was at CodeSchool.com.

Abnaki answered 17/5, 2013 at 17:29 Comment(4)
What exactly is the error you are getting? Do you know how far this approach got until it stopped working?Abnaki
So just go ahead and link to the codeschool root domain as that is totally useful ... eh no.Anelace
Probably not for you. But the intent was for that link to provide the starting place for them to get signed up, and find the course they would want to take. If I linked to a particular course, they'd have to sign up anyway. But thanks for your useful comment there.Abnaki
No problemo, any time. :)Anelace
S
2

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...

Srini answered 17/5, 2013 at 22:41 Comment(2)
Your problem is due to html rules of not allowing the placing of a form inside of a table. That's why you had to move your form_for outside of it. See this link https://mcmap.net/q/98969/-form-inside-a-tableDecommission
I'd like to add that I had the same issue with a plain rails from (you know all that stuff with haml and simple forms is quite distructing for those who do not use it). My form was wrapped into some divs, so after removing this wrappings, it magically started working! Thanks a lot!Peddler
A
0

I had an issue similar to this one whereby a form that was loaded in via ajax was not being submitted, this was due to Turbolinks and I found the solution was to bind my event to the body and not the element, relevant answers are found in this thread:

How to use $(document).on("click.. on <a tag?

For me the problem was on clicking a label, so I had to change

$('label').click(function...

to

$('body').on('click', 'label', function

Anelace answered 7/5, 2015 at 14:8 Comment(0)
I
-1

I had the same issue and finally understood the reason. Your html source must be formed of as follows.

<tr>
  <form>
    <td><input type="text"></td>
    <td><input type="submit"></td>
  </form>
</tr>

It seems some browsers including Chrome and Firefox cannot handle such nested tags correctly.

I could successfully submit rerendered forms after replacing table/tr/td tags with other ones like ul/li.

Ineradicable answered 30/9, 2015 at 16:9 Comment(1)
Note that your HTML here is entirely invalid. The form element cannot be a direct descendant of a tr.Cislunar

© 2022 - 2024 — McMap. All rights reserved.