Unobtrusive Form Submission via AJAX in Rails 3.1

Ever find yourself playing the “Did it save?” game with setting or a form you update? Make your change, and…Did it apply? Is there a save button you need to find? If clicking it doesn’t reload the page, do you feel certain that the change was applied?

Do I need to save? Where's the save?

The web app world is currently between two different UX styles. I’m seeing fewer and fewer save buttons. The Macintosh style of click-to-set and no-need-for-a-save-button is slowly making it’s way to many web apps I use regularly. In most cases, it’s the right idea: save the entered text or apply the setting right away. But the UX for doing so is still largely up to interpretation. Let’s take a quick look at how to do this in Rails 3.1.

Some of the tools we have to work with:

  • Updated unobtrusive javascript helpers from Rails, like the :remote option on forms
  • Customizable animated gif “spinner” or waiting icons
  • Easy event binding courtesy of jQuery and CoffeeScript
  • Custom formats in Rails controllers

All that can work together to make a form save as data is modified, while providing good feedback to the user about what is happening. An example? Oh, sure!

Say we’d like to let someone update a journal entry text area with their latest happy thoughts whenever they are on a page, and save the data as soon as they click out of the textarea or tab to another field.

Build your form a-like so:

# app/views/entries/edit.html.haml

      = form_for @entry, remote: true do |f|
        = f.text_area :summary, size: "40x8"
        = f.submit "Save"

Setting the remote option on the form is all we need to do to make it save over AJAX. That was easy! Are we done? Not quite.

We need to tell it when to save. Even though we don’t plan on the user clicking the button, the save button is still needed so we can trigger the submit at the time we desire. Which is when the textarea loses focus, either due to our jovial journaler tabbing to another part of the page or clicking elsewhere.

Add a little unobtrusive JS:

# app/assets/javascripts/entries.js.coffee

$("textarea").live "blur", -> $(this).parents("form").submit()

That happy little one-liner will make all textareas on the page (in case we want to let them update past journalings on the same page) submit the form when they are blurred.

If you are using a scaffolded controller, you can now see this working. Load the page, enter some text, and reload. It’s still there! Joy! We have added unobtrusive instant background saving to our form with one option on the form, and one line of JavaScript.

Hm, if you watch the webkit inspector as you click out of the changed textarea to see the AJAX events firing, you should notice that 2 events are firing. First the form data is POSTed, as expected, but then a GET call is made to the updated resource. This is standard Rails flow…for an http submission. But we don’t need to show the updated resource on it’s own page, because it is already in front of the user. Luckily, since Rails 3.1 scaffolding adds a json format handler as well, we can simply tell the form to use that. Our form builder now becomes:

# app/views/entries/edit.html.haml

      = form_for @entry, remote: true, format: :json do |f|
      ...

And presto, the form is only POSTed, and the controller responds with the http status code of 200, meaning “All is well in the kingdom.”

So, are we done? No way: no functional behavior is complete until it interacts sanely with the user. We need to tell our journalista what we did…translating the POST and 200 into good feedback.

With a little more unobtrusive JS, we can trigger some custom actions as different parts of the AJAX event fire. But first go pick a waiting spinner and drop it in app/assets/images. Then, on your page, in an obvious place, drop in the spinner near a place to display the result of the save:

=# app/views/entries/edit.html.haml or globally in app/views/layouts/application.html.haml

#spinner
  = image_tag "spinner.gif"
#response

Now build out the behavior by adding something like this:

# app/assets/javascripts/entries.js.coffee
…
# Show spinner while saving:
toggleSpinner = -> $("#spinner").toggle()

# When the page is ready:
$(->
  $("form[data-remote]")
    .bind('ajax:before', toggleSpinner)
    .bind('ajax:complete', toggleSpinner)
    .bind('ajax:success', (event, data, status, xhr) ->
      $("#response").html("Saved!").show().fadeOut("slow")
    )
    .bind('ajax:error', (xhr, status, error) -> )
)

Now when we reload the page, make an edit, and click out…Behold the spinner! But be sharp, it doesn’t stick around long. That’s why having it in an obvious location is important.

I had a little trouble getting the initial showing spinner action to fire. Other people had suggested binding to “ajax:loading” or even “ajax:beforeSend”, but those didn’t work for me.

That pretty much does it! As a bonus here’s two quick snippets that clean it up even more…

When Safari detects an "unsaved" change has been made to the page, reloading or navigating away will display this message:
Are you sure you want to reload this page?
You have entered text on “My Glorious Daily Journal”. 
If you reload the page, your changes will be lost.
Do you want to reload the page anyway?
[Cancel]   [Reload]

This is simply wrong in many ways, so disable it by overriding the onBeforeUnload method with a blank function:

# app/assets/javascripts/entries.js.coffee

# Kill the "entered text" warning in Safari:
window.onbeforeunload = (e) ->

Better. But what about that pesky save button? We don’t want our users thinking it’s needed. We want them to get used to not even bothering to look for it. We need it on the page, but we want our user focus on writing. Hide it! It’s easy as:

// app/assets/stylesheets/flow.css.sass
form.edit_review
  input[type=submit]
    display: none

Bam! We’re done! Go forth and allow unobtrusive instant background saves on all your update forms!

No comments yet.

Leave a Reply