DEV Community

Cover image for Simple Preferences to Any Resource with Ruby on Rails
Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Simple Preferences to Any Resource with Ruby on Rails

This article was originally published on Rails Designer


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:

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Get User's preferences like this:

user = User.find(1)
=> #<User:0x00000007146fb100>
user.preferences.time_zone
=> "UTC"
user.preferences.notification_sound_enabled
=> true
Enter fullscreen mode Exit fullscreen mode

Need to update a preference?

user.preferences.update notification_sound_enabled: false
Enter fullscreen mode Exit fullscreen mode

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/".

Top comments (0)