thereq

online coding interviews

[why?] [how?]

tech.thereq.com
the way we do the things we do

Rails 3: Using form :remote => true with jQuery UJS

In our last post, we explored remote links in Rails 3 using jQuery UJS. In this post, we’ll explore remote submission of forms.

Similarly to remote link_to the primary difficulties that new Rails users encounter is that

  1. There are multiple ways to make this work!
  2. It’s critical to make sure that the data type the browser is requesting is matched with a suitable response by the server. (If the browser requests JSON and the server responds with HTML, things aren’t going to work very well!)

Server Responds With JS - Server Side Rendering

If you add :remote => true to your form_for method in Rails, the default behavior of the UJS library is to request a response of data type script (or “JS”) from the server. If you add :remote => true to your form and look at your log file, you’ll see something like:

Started PUT "/articles/1" for 172.16.19.1 at 2012-03-05 16:47:42 +0000
Processing by ArticlesController#update as JS

Great, now what? If you do nothing else then the user clicks the submit button and low and behold… nothing happens.

To fix this, you need to modify your controller so that responds in a way that tells the browser what to do. Typically, what you would like to happen is the following

  1. If the form submission was successful (all validation passed and the model was saved successfully) show the user the data they just created.
  2. If the form submission was unsuccessful, show some error messages on the form.

There are multiple ways (aren’t there always!) to accomplish each of these actions, especially the latter. The following controller and js.erb snippets illustrate the simplest approach:

  1. If the form submission was successful, redirect the user to the show view for the article they just created.
  2. If the form submission was unsuccessful…
    1. Re-render the views/articles/_form.html.erb partial on the server side.
    2. Send the client a JS request to replace the old form HTML with your newly rendered HTML.

The controller looks like:

#app/controllers/articles_controller.rb    
def update
  @article = Article.find(params[:id])
  respond_to do |format|
        if @article.update_attributes(params[:article])
            # Redirect to the article template
            format.html { redirect_to @article, :notice => 'Article was successfully updated.' }
            format.js { render :js => "window.location.replace('#{article_path(@article)}');"}
        else
            format.html { render :action => "edit" }
            # Renders update.js.erb which replaces the body of the form with a newly
            # rendered version that will include the form errors
            format.js {}
        end
    end
end

and the javascript necessary to re-render the form is deliciously simple. Note that the update.js.erb is first run through the ERB processor on the server side. Therefore, the render method is run on the server, using our server-side _form.html.erb template. The rendered HTML is inserted into the .html() method and then the javascript is returned to the client. The browser executes the javascript, replacing the <form> element with the newly rendered HTML that will now contain the form errors.

# app/views/articles/update.js.erb
$('form').html('<%= escape_javascript render("form") %>');

Server Responds With JSON - Client Side Rendering

If you prefer to send JSON back from the server, you can do this by adding a data-type attribute in form_for.

# app/views/articles/_form.hml.erb
<%= form_for(@article, :remote => true, :html => {"data-type" => :json}) do |f| %>

If the user clicks the submit button, then in the server logs you should see the JSON request come in

Started POST "/articles" for 172.16.19.1 at 2012-03-07 19:45:27 +0000
Processing by ArticlesController#create as JSON

… and once again from the user perppective… nothing happens! And, once again, what we’d like to happen is:

  1. If the form submission was successful (all validation passed and the model was saved successfully) show the user the data they just created.
  2. If the form submission was unsuccessful, show the errors on the form.

This time we’ll take a different approach.

  1. If the form submission was successful, pass back the created data as JSON and render it without redirecting to a new page. (Client-side rendering.)
  2. If the form submission was unsuccessful, pass back the errors as JSON and add these to the form. (Client-side rendering.)

In this case, our controller looks like this:

# app/controllers/articles_controller.rb
def create
    @article = Article.new(params[:article])

    respond_to do |format|
        if @article.save
            format.html { redirect_to @article, :notice => 'Article was successfully created.' }
            # Send back the new article, we'll render it on the client side
            format.json { render :json =>  @article, :status => :created, :location => @article }
        else
            format.html { render :action => "new" }
            # Send back the errors as JSON, we'll render them on the client side
            format.json { render :json =>  @article.errors, :status => :unprocessable_entity }
        end
    end
end

and the client side rendering is handled in AJAX callbacks that look like this:

// app/assets/javascripts/articles.js
// Parse the JSON response and replace the <form> with the successfully created article
$('form.new_article').on('ajax:success',function(event, data, status, xhr){
    $(this).replaceWith('<div>Title: ' + data.title + '</div>' +'<div>Body: ' + data.body + '</div>');
});


// Parse the JSON response and generate an unordered list of errors, then stick it inside
// <div class="errors"> which is in our view template 
$('form.new_article').on('ajax:error',function(event, xhr, status, error){

    var responseObject = $.parseJSON(xhr.responseText), errors = $('<ul />');

    $.each(responseObject, function(index, value){
      errors.append('<li>' + index + ':' + value + '</li>');
    })

    $(this).find('.errors').html(errors);
});

Full Example

If you’d like to see a full version of this in action you can download our ujsdemo application.

More Resources

Blog comments powered by Disqus