Updating User Settings Without a Save Button

Alright, so today I obsessed over something simple—user settings. You know, those toggles for notifications and profile preferences. Normally, you'd throw in a checkbox, slap on a "Save" button, and call it a day. But no, I wanted something fancier. The Problem I don't want users clicking "Save" whenever changing a setting. Feels outdated. Instead, toggles should just work—flip the switch, boom, saved. So, I went down the Stimulus + Rails 8 + JSONB rabbit hole. Step 1: Ditch the Extra Columns I refuse to clutter my database with a dozen boolean columns for settings. JSONB to the rescue class AddSettingsToUsers false }.freeze def user_settings DEFAULT_SETTINGS.merge(settings || {}) end def update_settings(new_settings) update(settings: user_settings.merge(new_settings)) end end Now, if a setting doesn't exist yet, it falls back to the default. No surprises. Step 3: The UI - Toggles instead of Checkboxes Because checkboxes are boring, and Bootstrap 5 has a built-in form-switch class to make toggles look cleaner. .setting-item.form-check.form-switch.form-check-md.mb-3 = check_box_tag "user_settings[profile_visibility]", class: "form-check-input", checked: user.user_settings["profile_visibility"], "data-setting" => "profile_visibility" = label_tag "user_settings[profile_visibility]", "Profile visibility", class: "form-check-label" Step 4: Making It Actually Work (With Stimulus) Here's the fun part. When a user flips a toggle, it immediately sends an AJAX request to update the setting. No reloads, no extra clicks. updateSetting(event) { const setting = event.target.dataset.setting const value = event.target.checked fetch("/profile/update_settings", { method: "PATCH", headers: { "Content-Type": "application/json", "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]') .content, }, body: JSON.stringify({ setting, value }), }) .then((response) => response.json()) .then((data) => { if (!data.success) { alert("Failed to update setting.") event.target.checked = !value // Revert on failure } }) } Wait, What's This "X-CSRF-Token" Thing? Rails has built-in security mechanisms to prevent Cross-Site Request Forgery (CSRF) attacks. If you try making a PATCH, POST, PUT, or DELETE request without a CSRF token, Rails will reject it. Since my settings update happens via fetch(), I need to include this token in the request headers. Instead of hardcoding it, Rails provides a helper to generate it dynamically in the layout: -# application.html.haml !!! %html %head = csrf_meta_tags csrf_meta_tags -> Inserts the CSRF token. Then, in my JavaScript, I grab the token like this: headers: { "Content-Type": "application/json", "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content } This way, every request includes the correct CSRF token without me worrying about it. Rails is happy, security stays intact, and everything just works. Final Thoughts ✅ No database clutter ✅ No "Save" button nonsense ✅ No unnecessary page reloads ✅ Everything just works Might be over-engineering things sometimes, but hey, that’s all part of the learning process—and I’m enjoying every bit of it!

Feb 8, 2025 - 23:42
 0
Updating User Settings Without a Save Button

Alright, so today I obsessed over something simple—user settings. You know, those toggles for notifications and profile preferences. Normally, you'd throw in a checkbox, slap on a "Save" button, and call it a day. But no, I wanted something fancier.

The Problem

I don't want users clicking "Save" whenever changing a setting. Feels outdated. Instead, toggles should just work—flip the switch, boom, saved.

So, I went down the Stimulus + Rails 8 + JSONB rabbit hole.

Step 1: Ditch the Extra Columns

I refuse to clutter my database with a dozen boolean columns for settings. JSONB to the rescue

class AddSettingsToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column :users, :settings, :jsonb, default: {}
  end
end

Now I can store all settings in one place. Future-proof, flexible, and no schema changes when I add new settings.

Step 2: Default Settings Without a Mess

I don't want to check for nil every time I call a setting, so I merged defaults with what's actually stored.

class User < ApplicationRecord
  DEFAULT_SETTINGS = { "profile_visibility" => true, "disable_ads" => false }.freeze

  def user_settings
    DEFAULT_SETTINGS.merge(settings || {})
  end

  def update_settings(new_settings)
    update(settings: user_settings.merge(new_settings))
  end
end

Now, if a setting doesn't exist yet, it falls back to the default. No surprises.

Step 3: The UI - Toggles instead of Checkboxes

Because checkboxes are boring, and Bootstrap 5 has a built-in form-switch class to make toggles look cleaner.

.setting-item.form-check.form-switch.form-check-md.mb-3
  = check_box_tag "user_settings[profile_visibility]",
                  class: "form-check-input",
                  checked: user.user_settings["profile_visibility"],
                  "data-setting" => "profile_visibility"
  = label_tag "user_settings[profile_visibility]",
              "Profile visibility",
              class: "form-check-label"

Step 4: Making It Actually Work (With Stimulus)

Here's the fun part. When a user flips a toggle, it immediately sends an AJAX request to update the setting. No reloads, no extra clicks.

updateSetting(event) {
  const setting = event.target.dataset.setting
  const value = event.target.checked

  fetch("/profile/update_settings", {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')
        .content,
    },
    body: JSON.stringify({ setting, value }),
  })
    .then((response) => response.json())
    .then((data) => {
      if (!data.success) {
        alert("Failed to update setting.")
        event.target.checked = !value // Revert on failure
      }
    })
}

Wait, What's This "X-CSRF-Token" Thing?

Rails has built-in security mechanisms to prevent Cross-Site Request Forgery (CSRF) attacks. If you try making a PATCH, POST, PUT, or DELETE request without a CSRF token, Rails will reject it.

Since my settings update happens via fetch(), I need to include this token in the request headers. Instead of hardcoding it, Rails provides a helper to generate it dynamically in the layout:

-# application.html.haml
!!!
%html
  %head

    = csrf_meta_tags
  • csrf_meta_tags -> Inserts the CSRF token.

  

     name="csrf-token" content="dvj7leYFe3zAzv0BkIMOlUnyOqBuPSMzwAr_rFV7GqViAsvcDDFWh_suMlKu7coEDj5WtvL5oMs6l2c_KtSeQg">

Then, in my JavaScript, I grab the token like this:

headers: { 
  "Content-Type": "application/json",
  "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
}

This way, every request includes the correct CSRF token without me worrying about it. Rails is happy, security stays intact, and everything just works.

Final Thoughts

✅ No database clutter
✅ No "Save" button nonsense
✅ No unnecessary page reloads
✅ Everything just works

Might be over-engineering things sometimes, but hey, that’s all part of the learning process—and I’m enjoying every bit of it!