Simple Preferences to Any Resource with Ruby on Rails
Tags: rails, webdev, ruby
Published: 2024-05-09 08:00:00

This article was originally published on [Rails Designer](https://railsdesigner.com/simple-preferences/)

---

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](https://github.com/palkan/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:

```bash
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.

```ruby
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](https://github.com/palkan/store_attribute)** gem: `bundle add store_attribute`.

Now open the created Vault model and add these two class methods:

```ruby
# 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.

```ruby
# 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:

```ruby
# 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:

```ruby
# 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:

```bash
user = User.find(1)
=> #<User:0x00000007146fb100>
user.preferences.time_zone
=> "UTC"
user.preferences.notification_sound_enabled
=> true
```

Need to update a preference?

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