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.
<html>
<head>
<meta 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! 🚀
Top comments (0)