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 sending PATCH 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 (adminteacherstudent) — 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 new role 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 modalsadmin dashboardsrole/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.

Back