Ajax Forms with Rails 3 - Best Practice?
Asked Answered
B

2

22

What is considered the Rails Way for Ajax Forms

Until today I thought the way I use Rails forms + jQuery UJS was the right way to do it, but the upgrade to jQuery 1.7 'broked' the way I did it so far.

How to do this?

I want to use Ajax with my forms. The ajax response should either render the form again (e.g. when errors occur) or redirect to a success page. (It should maybe do more, like showing modals, but I want to keep this example simple).

1. Using EJS and $('#formid').html(...)

That is what I did until to date. My ajax response was an ejs template which returned javascript code to render the form again (with errors) or depending if it was an error redirected the user to success page:

<% unless @success_submit %>
$('#form_wrapper').html('<%= escape_javacsript( render :partial => 'form' ) %>'); 
<% else %>
document.location = '/success_thank_you';
<% endif %>

Now imagine the form contains an error message div like

<div class="error_message">BlaBla</div>

To add a nice effect I had a general jQuery.live event bound on ajax complete which highlighted errors.

$('form').live('ajax:complete', function() {
   // do stuff like highlighting all error messages in that form
}); 

That doesn't work with jQuery1.7 + jquery-ujs anymore (probably by good reasons). So I guess the way I did it was not right.

2. Using above way but repeat myself

Instead of binding the ajax:complete event I could do the "error highlighting stuff" in the EJS like

$('#form_wrapper').html('<%= escape_javascript( render :partial => 'form' ) %>');
$('#form_wrapper form .error_message').fadeIn();

But that would mean I would have to repeat the second line in almost each EJS which renders forms. And of course I want to keep it DRY.

3. Ajax response renders pure html, ajax:complete event handles display

A complete different solution would be that the ajax response simply would render pure html and my custom ajax:complete handler would take care of displaying the form. The handler would look like

$('form').live('ajax:success', function(ev, data, status, xhr) {
  $(this).html(data);
  // and e.g. highlight stuff
  $(this).children('.error_message').fadeIn();
});

That would work. But now what should I do if my server decides not to render the form again (e.g. after successful signup) but instead redirect to another url or showing a modal form. The server could respond with something like that

<script>
   document.location = '/success.blabla/';
</script>

But is that a good solution ?

4. Custom JSON protocol

Probably a good solution would be to use version 3 but instead of simply replacing the current form with the returned html we could create some custom json protocol. That way we could even let the server respond with stuff like

  1. first show modal ( like 'Signup Success')
  2. redirect to login page

The ajax:success handler could check if the response is pure html, in that case it would replace the current form with the html code. But if the response is a JSON array it would handle that, e.g. server responds with

{ html: '<%= render :partial => 'something' %>', 
  show_modal:  '<%= render :partial => 'modal_template' %>',
  redirect_after_modal: '/login_page'
}

The ajax:success handler would have handle it like

$('form').live('ajax:success', function(ev, data, status, xhr) {
   // try parsing json
   if (data.json['show_modal') { // show modal code.... }; 
   if (data.json['redirect']) { document.location=data.json['redirect']);...
});

How are you doing this stuff

Obviously there are many ways how you handle ajax forms. But how are you doing it, and what is considered best practice with Rails ?

Biddie answered 18/11, 2011 at 15:6 Comment(0)
C
8

So, I was able to boil your issue down to jquery only, and sure enough, it works in jquery 1.6.4, but not in jquery 1.7.

It seems that if you replace an element in the DOM, and then trigger an event on the jquery object created from the original element, 1.6.4 would still trigger the event in the elements original location and allow it to propagate up the DOM. 1.7, however, will not trigger the element (which makes more sense).

I just did a search through the jquery 1.7 changelog and sure enough, here are the two tickets which rectified this behavior: 9951 and 10489.

To answer your question about what is the best practice for accomplishing something like this, I would say, take control over what order your events fire. jQuery automatically executes JS returned in ajax responses, which means you have no control over when that happens.

The easiest way to modify your code would be to return the HTML partial itself, and then use jquery to replace the form with the returned HTML in the ajax:complete or ajax:success/ajax:error handlers. This way you can be sure that things happen in the exact order you want.

To see how exactly to accomplish this, try reading [my articles]:

Or see the jquery-ujs wiki for these links and more.

Consist answered 18/11, 2011 at 16:47 Comment(2)
hi @Consist can you look at this please : #19385220Weeden
How about this way: setTimeout(function() { $('#formid').html(...) }); in the EJS? The setTimeout will put the replacement to the next time which happens after the ajax:* events being triggered.Bundy
B
0

Not sure if you need it anymore, but here what we have done to implement forms with AJAX

Suppose we have an actions that looks like

def new
  @saved_search = SavedSearch.new
new

def create
  if @saved_search.save
    respond_to do |format|
      format.js { render :saved }
    end
  else
    respond_to do |format|
      format.js { render :new }
    end
  end
end

and we have a form that looks like

# app/views/saved_searches/new.html.erb
<%= form_for @saved_search,  url: saved_searches_path(format: :js), remote: true do |f| %>
  <div class="form">
    <div class="content">
      <div class="field">
        <%= f.label(:name, t(".name_search"), class: "label") %>
        <%= f.text_field(:name, size: "25", autofocus: "autofocus") %>
      </div>

      <div class="clear"></div>
    </div>

    <footer class="clear">
      <div class="left">
        <%= f.button t(".save"), class: "button" %>
      </div>
      <div class="clear"></div>
    </footer>
  </div>
<% end %>

And, obviously, we want to return an errors if some fields are invalid. So here is a trick

# app/views/saved_searches/new.js.erb
<%- @saved_search.errors.keys.each do |field| %>
  $('#saved_search_<%= field %>').after("<span class=\"errors\">&nbsp;<%=   @saved_search.errors[field].join(',')  %></span>")
  $('#saved_search_<%= field %>').wrap('<div class="field_with_errors" />');;
<% end %>

That does not brake JQuery bindings or callback, but still presents you with errors.

I am now thinking that it might be a good idea to write some form builder, that will be able both render an HTML form, and return bunch of JQuery directives for form fields, taking

Rails.configuration.action_view.field_error_proc

into consideartion

Breeks answered 20/5, 2013 at 17:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.