Having an option for your users to set their preferences is something that is likely required rather sooner than later. These might range from aesthetic preferences to usability settings.
Some examples for users: keyboard_shortcuts_enabled
, language_preference
, timezone
, text_editor_theme
, notification_sound_enabled
.
Or for other domain models: contact_sort_order
, default_campaign_filters
, task_view_default
, favorite_projects
.
If you’ve ever built, or are part of a team that builds, SaaS products, you know these feature requests are not all known beforehand. Feature requests come in, requirements change and so on.
There are a few gems out there that provide a solution to this, but to me this is the kind of functionality I want to control and and not rely on a third-party (YMMV).
This article outlines a super simple, but extendable way to add one or many preferences to any domain model. There is some syntactic sugar added to make them quack more like Rails does too. 🦆
I want to preface that I use these preferences only for UI purposes. They are never needed to be queried/searched-for in some way.
Let’s dig right in. What is needed:
- one new data model;
- two classes, one module (🤓);
- store_attribute gem.
Just want to use it in your app right now? Run this template:
rails app:template LOCATION="https://railsdesigner.com/simple-preferences/template/"
in your Rails app’s root folder.
Let’s create the data model that holds it all:
rails g model Vault resource:belongs_to{polymorphic} scope:string payload:jsonb
I’ve chosen the general name “Vault” here as it fits quite well for the various purposes, but you can of course name it however you want.
Now let’s update the created migration file to add some sane validations, default values and some indexes.
class CreateVaults < ActiveRecord::Migration[7.1]
def change
create_table :vaults do |t|
t.belongs_to :resource, polymorphic: true, null: false
t.string :scope, null: false
t.jsonb :payload, null: false, default: {}
t.timestamps
end
add_index :vaults, :scope
add_index :vaults, :payload, using: :gin
end
end
Next add the store_attribute gem: bundle add store_attribute
.
Now open the created Vault model and add these two class methods:
# app/models/vault.rb
class Vault < ApplicationRecord
# …
def self.vault_scope(scope_name)
default_scope { where(scope: scope_name) }
end
def self.vault_attribute(key, *attributes)
options = attributes.extract_options!
store_attribute :payload, key, *attributes, **options
end
# …
end
Up next: a Vaults
concern.
# app/models/concern/vaults.rb
module Vaults
extend ActiveSupport::Concern
class_methods do
def vault(association_name, class_name: nil)
has_one association_name, as: :resource, class_name: class_names.presence || "#{self}::#{association_name.to_s.camelize}", dependent: :destroy
end
end
Now the basics are done already! Let’s create the first Vault to store some User preferences. Create the following file:
# app/models/user/preferences.rb
class User::Preferences < Vault
vault_scope :user_preferences
vault_attribute :time_zone, :string, default: "UTC"
vault_attribute :datetime_format, :string, default: "dd-mm-yyyy"
vault_attribute :notification_sound_enabled, :boolean, default: true
end
Note: this folder structure is, by convention, through the set up of the class_name
in the Vaults
concern: "#{self}::#{association_name.to_s.camelize}"
. Set a custom one by setting the class_name
below.
Then in your User model:
# app/models/user.rb
class User < ApplicationRecord
# …
include Vaults
vault :preferences
# or when using a different folder structure:
# `vault :preferences, class_name: "CustomPreferences"`
# …
end
Get User’s preferences like this:
user = User.find(1)
=> #<User:0x00000007146fb100>
user.preferences.time_zone
=> "UTC"
user.preferences.notification_sound_enabled
=> true
Need to update a preference?
user.preferences.update notification_sound_enabled: false
And that’s all! 😎
Want to add this to your app without doing any manual work? 🧠 Run this command in your Rails app’s root folder: rails app:template LOCATION="https://railsdesigner.com/simple-preferences/template/"
.