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!
![Updating User Settings Without a Save Button](https://media2.dev.to/dynamic/image/width%3D1000,height%3D500,fit%3Dcover,gravity%3Dauto,format%3Dauto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoudevxbihj8z89or82n.png)
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!