Submitting Ruby on Rails Forms via Ajax
Ever wanted to submit a form and manipulate HTML page elements without triggering a full-page reload in Rails?
This tutorial demonstrates a simple way to submit and process form data via a
I've tried to make this post accessible to beginner to intermediate level Rails developers. This workflow is a fundamental skill that belongs in any Rails developer's toolbox. In addition to form data, this same workflow is insanely useful in a variety of other contexts—including links, API calls, webhook interactions.
A repo with source code for this tutorial is available here.
Table of Contents - Ajax for Ruby on Rails Forms
A. Overview ↑ Table of Contents
What we'll build:
Just a simple form with a single text input that saves entries to a database.
There's an important detail about this form—the
.js.erb file. Rails' asset pipeline parses this
.js.erb file—first the embedded Ruby (
.js.erb file, but you could just as easily use jQuery or other JS libraries, plugins, etc.
We will validate inputs for presence (
name cannot be empty) and uniqueness (
name cannot already exist in the database). These validations will happen on the server side—at the model level—as they normally would in a Rails app.
This walk-through ignores other common validations, notably front end validations, to stay focused on the relevant material.
If the form validation fails, Rails will reject the entry and display an error message.
The result is an elegant alternative to Rails full-page reloads.
B. Create a new Rails app ↑ Table of Contents
rails new sjr_basics --database=postgresql
gem 'jbuilder'in Gemfile
jbuilder prevents Rails from generating API-related code in our scaffolding. We are disabling this functionality for simplicity.
C. Generate a scaffold ↑ Table of Contents
carwith a single attribute:
rails s, navigate to http://localhost:3000/cars, and confirm you see something like this
D. Validate the model ↑ Table of Contents
rails cand confirm the model validations work
This shows us adding two cars,
car_2 gets rejected because the name is not unique.
E. Set up form and display results ↑ Table of Contents
@carto the index method in controller
We need to add this object so we can pass it to the
form_with helper in the next step.
F. Submit form via Ajax ↑ Table of Contents
local: truefrom form. Also confirm that you're using
G. Process successful entries ↑ Table of Contents
app/controllers/cars_controller.rbto handle results using
respond_toblocks in the create method
This code instructs Rails to render different response formats depending on whether
@car saves successfully. If
carto a partial and use that partial to display the list of
app/views/cars/create.js.erband add the following code
Rails uses this code to respond to successful entries. Since we specified
format.js in our controller, Rails knows to look in the
app/views/car directory for the
create.js.erb file. For more on
respond_to, see https://kolosek.com/rails-respond_to-block/.
H. Closer look at Ajax's efficiency and granular control ↑ Table of Contents
Let's pause here and look at some server logs. The switch to Ajax forms is subtle but significant. You should understand (A) what exactly is happening on a server level, and (B) why it is valuable.
First, look at the following server log entry for a traditional Rails form submission. In development you can see these entries in the terminal window where you started a server with
Here we've submitted a sample object called
Dinosaur, also with a single
name attribute). The log shows what happened, which I'll explain below so you can follow along:
This shows the server processing our POST request as HTML. The data parameters are passed as a hash. Our model validation triggers a search to ensure the name is unique. Then it inserts and commits the object into the database. Finally, it redirects to the dinosaurs index path, triggering a full reload of the index page (that's the GET request/response in the second log entry).
Compare that with the server log entry for our
Car Ajax submission:
Notice how this approach doesn't require a separate GET request to fetch the whole page? Instead, Rails just renders
create.js.erb. This significantly reduces server response time, resulting in a faster user experience and lower server costs.
I. Render validation errors ↑ Table of Contents
At this point, our form works fine for successful form submissions. But what happens when our form fails validation?
As you can see, it looks like nothing happened. The car does not appear in the list and there is no other feedback provided to the user. Tesla remains in the input field. This is not acceptable.
What happened? Begin by looking at the server log:
The browser submitted a POST request via Ajax.
CarsController#create attempted to process the request, which failed the model validation and got rolled back. Consistent with our instructions in the controller, Rails rendered
index.html.erb doesn't pick up the errors. Why not?
Answer: Turbolinks. The Turbolinks code works by swapping HTML elements. It doesn't know exactly what to do with our asynchronous JS data. If you want to do a deeper dive on Turbolinks, which I strongly recommend, https://thoughtbot.com/upcase/videos/turbolinks is a good starting point. As you would expect, the Turbolinks repo (https://github.com/turbolinks/turbolinks) is also very well-documented and accessible.
To solve this problem, we will use the
turbolinks_render gem, which helps pass the information Rails needs to respond to the Ajax request. You should read more about this gem from its author, Jorge Manrubia. For our purposes, it is sufficient to understand that
turbolinks_render allows us to render validation errors when a data-remote form fails validation.
bundle, and restart the server
Again, it is helpful to look at the server log here to see what's happening:
After determining the name already exists in the database, Rails renders the index view. Specifically,
turbolinks_render converts the JS data into a format that Turbolinks can render.
Now our code correctly handles entries whether they pass or fail validations.
J. Bug / gut check ↑ Table of Contents
If you take a close look at this code, you will notice a bug. Our form retains its error messages when an entry fails validation and the user fixes and resubmits an entry:
The lesson: Especially as a beginner, it can be enticing to add Ajax features to Rails apps. It can feel like you're providing a more focused user experience. However, this bug shows the level of attention, detail, and testing (a.k.a. costs) that accompanies these kinds of features.
Some developers cite this apparent complexity as one of Rails' weaknesses. In some cases, that assessment is correct.
In many other cases (the vast majority, by my estimation), the Rails approach serves a useful purpose here. The majestic monolith forces you to acknowledge complexity creep. It's a gut check. A code smell. Do you really need such granular control? Is this actually making your product simpler for users? The answer is often "no." You can add it if you need to, but it's not the default for a good reason. This is a great example of convention over configuration in action.
That's all for this tutorial. Using this same approach, you can add
remote: true to elements like links, buttons, events, etc. to trigger SJR responses.
turbolink_render, which makes this process much easier than it would otherwise be. It appears the upcoming Turbolinks 6 release will render
turbolinks_renderunnecessary. However, I expect Jorge's work will remain relevant for pre-Turbolinks 6 Rails projects.