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 data-remote
form. By the end of this post, you will understand how to: (1) submit Rails forms asynchronously, (2) validate and handle asynchronous data, and (3) use Server-generated JavaScript Responses (SJR) to respond to form submissions.
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 data-remote
attribute:

This attribute means the form will submit results via Ajax. In turn, Rails responds to successful form entries using JavaScript contained in a .js.erb
file. Rails' asset pipeline parses this .js.erb
file—first the embedded Ruby (.erb
) part, then the JavaScript (.js
) part.
Practically speaking, this means you can include Ruby code inside JavaScript code. We'll use standard JavaScript in our .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.

If the form passes validation, we'll use JavaScript and Ruby via SJR to add the new entry to the list without re-serving the entire page.

The result is an elegant alternative to Rails full-page reloads.
There are other options to build similar features, including Stimulus, JavaScript interception, jQuery Ajax, and other more robust front end solutions (React, Vue, Ember, etc.). But, in many use cases, this is a perfect compromise between simplicity and functionality.
B. Create a new Rails app ↑ Table of Contents
rails -v
This walk-through assumes you are using Rails 6. It should work on all versions at or above 5.1.0 (release date 27 Apr 2017), when Rails replaced jQuery with its own unobtrusive javascript adapter called rails-ujs. If you're using an earlier version of Rails, visit https://www.learnetto.com/blog/rails-ajax to read more about the switch from jQuery to rails-ujs.
rails new sjr_basics --database=postgresql
gem 'jbuilder'
in Gemfile
Disabling jbuilder
prevents Rails from generating API-related code in our scaffolding. We are disabling this functionality for simplicity.
bundle
C. Generate a scaffold ↑ Table of Contents
car
with a single attribute: name
rails db:migrate
rails s
, navigate to http://localhost:3000/cars, and confirm you see something like this

D. Validate the model ↑ Table of Contents
app/models/car.rb
rails c
and confirm the model validations work
This shows us adding two cars, car_1
and car_2
. The car_2
gets rejected because the name is not unique.
E. Set up form and display results ↑ Table of Contents
@car
to 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.
app/views/cars/index.html.erb

F. Submit form via Ajax ↑ Table of Contents
local: true
from form. Also confirm that you're using form_with
This is a small but important change. We're telling Rails to submit the form using Ajax instead of a normal POST request. Rails will detect this in the controller and we'll use it to trigger a JavaScript-specific response.
data-remote: true
attribute

G. Process successful entries ↑ Table of Contents
app/controllers/cars_controller.rb
to handle results using respond_to
blocks in the create method
This code instructs Rails to render different response formats depending on whether @car
saves successfully. If @car
saves, we render a JavaScript response. If it fails, we render an HTML response.
car
to a partial and use that partial to display the list of cars
app/views/cars/create.js.erb
and 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/.
This JavaScript adds a row to the list of cars, clears the input field, and, finally, focuses the cursor in the input field so the user can create another car without having to click on the box again.

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 rails s
.
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
.
But 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.
turbolinks_render
to Gemfile
, run 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:

We can easily fix this with some additional JavaScript. But that's not why I'm highlighting this bug and we will not fix it here.
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.
The End
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_render
unnecessary. However, I expect Jorge's work will remain relevant for pre-Turbolinks 6 Rails projects.