Handling AJAX requests in Rails with Vanilla JS and UJS
Building smooth, responsive admin interfaces in Rails doesn’t require React or Vue. With the right setup, Vanilla JavaScript + Rails UJS is more than enough to handle modern AJAX workflows — including real-time updates, modals, and dynamic DOM updates — without a full page reload.
In this article, we’ll walk through implementing a role editor for users using:
- Rails
form_with
and controller Rails.ajax
for sendingPATCH
requests- Turbo-compatible modals
- No external JS frameworks
🔧 The Use Case: Admin User Role Management
Let’s say you’re building an admin interface where admins can edit a user’s role (admin
, teacher
, student
) — directly from the users index page.
💻 So what we want ?
- User clicks “Edit Role” → modal opens
- Selects a new role → submits the form
- Server responds → role updates in table without page reload
📁 Rails Setup :
Let’s start with the basic Rails layout.
users_controller.rb (Action Update)
def update
@user = User.find(params[:id])
if @user.update(user_params)
render json: { id: @user.id, role: @user.role }
else
render json: { error: "Failed to update" }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:role)
end
Here we:
- Look up the user by
params[:id]
- Update the
role
field - Return a JSON response with
id
and newrole
on success
This keeps our controller AJAX-aware and format-flexible.
🧩 HTML View: admin/users/index.html.erb
This is a Turbo-friendly layout using a modal:
<h1 class="text-3xl font-bold mb-6 text-gray-800">Manage Users</h1> <table class="min-w-full bg-white border border-gray-200 rounded-lg shadow-md"> <thead class="bg-blue-600 text-white"> <tr> <th class="py-3 px-6 text-left">Email</th> <th class="py-3 px-6 text-left">Role</th> <th class="py-3 px-6 text-left">Actions</th> </tr> </thead> <tbody class="divide-y divide-gray-200"> <% @users.each do |user| %> <tr class="hover:bg-gray-100 transition-colors"> <td class="py-4 px-6"><%= user.email %></td> <td class="py-4 px-6 capitalize"><%= user.role %></td> <td class="py-4 px-6"> <button class="text-blue-600 hover:text-blue-800 font-semibold edit-role-btn" data-user-id="<%= user.id %>" data-user-role="<%= user.role %>"> Edit Role </button> </td> </tr> <% end %> </tbody> </table>
![]()
<!-- Modal for editing role --> <div id="role-dialog" class="hidden fixed inset-0 flex items-center justify-center z-50 backdrop-blur-sm"> <div class="bg-white rounded p-6 w-96 shadow-lg border border-gray-300"> <h2 class="text-xl font-semibold mb-4">Edit User Role</h2> <form id="role-form"> <input type="hidden" id="dialog-user-id" name="user[id]" /> <label class="block mb-2 font-medium">Select Role:</label> <select id="dialog-role-select" name="user[role]" class="w-full border rounded px-3 py-2 mb-4"> <option value="admin">Admin</option> <option value="teacher">Teacher</option> <option value="student">Student</option> </select> <div class="flex justify-end space-x-4"> <button type="button" id="dialog-cancel-btn" class="px-4 py-2 border rounded">Cancel</button> <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Save</button> </div> </form> </div> </div>
🎯 JavaScript Logic (With Importmap + Rails UJS)
Rails UJS lets you send AJAX requests easily with CSRF support — no jQuery required.
In app/javascript/users_role_dialog.js
(or a similar file):
import Rails from "@rails/ujs";
document.addEventListener("turbo:load", () => {
const dialog = document.getElementById("role-dialog");
const roleSelect = document.getElementById("dialog-role-select");
const userIdInput = document.getElementById("dialog-user-id");
const roleForm = document.getElementById("role-form");
const cancelBtn = document.getElementById("dialog-cancel-btn");
// Show modal with current user info
document.querySelectorAll(".edit-role-btn").forEach(button => {
button.addEventListener("click", () => {
userIdInput.value = button.dataset.userId;
roleSelect.value = button.dataset.userRole;
dialog.classList.remove("hidden");
});
});
// Hide modal on cancel
cancelBtn.addEventListener("click", () => {
dialog.classList.add("hidden");
});
// Submit form via AJAX (PATCH)
roleForm.addEventListener("submit", (e) => {
e.preventDefault();
const userId = userIdInput.value;
const newRole = roleSelect.value;
Rails.ajax({
url: `/admin/users/${userId}`,
type: "PATCH",
data: new URLSearchParams({ "user[role]": newRole }).toString(),
success: (data) => {
// Update table directly
const row = document.querySelector(`button[data-user-id="${userId}"]`).closest("tr");
row.querySelector("td:nth-child(2)").textContent = data.role;
dialog.classList.add("hidden");
},
error: () => {
alert("Failed to update role");
}
});
});
});
🧠 Why This Works Smoothly
- No page reloads
- Uses standard Rails routes
- Full Turbo-compatibility
- Uses Rails UJS (
Rails.ajax
) with CSRF token handled automatically
You don’t always need React or Stimulus to build interactive UIs in Rails. By using vanilla JavaScript, modals, and Rails UJS, you can deliver a clean, snappy admin experience that feels modern — while keeping your stack lightweight and maintainable.
This technique is especially useful for modals, admin dashboards, role/user updates, and soft real-time UI.
🚀 Final Notes:
- Use
Rails.ajax()
for backend PATCH/POST/DELETE - Return
JSON
from controller - Update DOM dynamically with JS
- Keep the form remote (
form_with local: false
) - Use
turbo:load
to support Turbo navigation
A simple modal and a PATCH request may seem small — but when multiplied across your app, this pattern shapes a fast, intuitive experience that scales beautifully.