Search code examples
ruby-on-railsrubyformsdefaulterb

Rails: Form that sets up default values for an object?


Say I have an object that has some attributes with default values. I set up the default values for these attributes at a model level like so...

  after_initialize :set_defaults

  def set_defaults
    self.name ||= "Player"
    self.level ||= 5
  end

I want to have a way for the user to set what the default values are though. The workflow would be the user would have an action to set their own defaults (in the view, this would be done via a form) and then these defaults would be used in the model method set_defaults, like so...

  def set_defaults
    self.name ||= user_default_for_name
    self.level ||= user_default_for_level
  end

I have thought about approaching this by making a "default" object in the code that the user "edits". Then, in the model method, the defaults will be set from the values in that object. But is there any better way of doing this?


Solution

  • In general I'm not a fan of callbacks to set default values for attributes. This is a very dated approach and can have wierd and unexpected side effects due to the timing issues.

    If the default values are not part of the database schema (like booleans that default to false for example) but can be defined on the class level you can use the ActiveModel or ActiveRecord attributes API to set defaults.

    attribute :level, :integer, default: 5
    

    This is also an area where you could consider using the Form Object Pattern to avoid making your model into even more of god class.

    Form Objects are classes that wrap a model for the purpose of handling user input.

    class PlayerForm
      include ActiveModel::Model
      include ActiveModel::Attributes
      
      attr_accessor :player
      attribute :level, :integer,  default: 5
      attribute :name,  :string,   default: "Player"
      
      # this lets the form object interact with Rails API's
      # for routing 
      delegate :to_model, :model_name, to: :player
    
      def initialize(**attributes)
        super  
        @player = Player.new
      end
    
      def save
        return false unless self.valid?
        @player.assign_attributes(level: level, name: name)
        @player.save   
      end
    end
    

    On the controller level we just swap out the Player class for the PlayerForm class:

    class PlayerController < ApplicationController
      def new 
        @player = PlayerForm.new
      end
    
      def create
        @player = PlayerForm.new(player_attributes)
        if @player.save 
          redirect_to @player
        else 
          render :new
        end
      end
    end
    

    While the immediate gains here can seem small now setting the defaults only happens when you actually want it - when populating a form and not every time the model is instanciated.

    I have thought about approaching this by making a "default" object in the code that the user "edits". Then, in the model method, the defaults will be set from the values in that object. But is there any better way of doing this?

    If you want the default values to be personizable by the users via the GUI you would create a model (and the related routes and controller) for that:

    # rails g model UserDefault name:string level:integer
    class UserDefault < ApplicationRecord
      belongs_to :user
    end
    

    You could then plug the user defaults model into the Form Object we created earlier:

    class PlayerForm
      include ActiveModel::Attributes
      include ActiveModel::Model
    
      attr_accessor :player
      attribute :level, :integer
      attribute :name,  :string
      
      # this lets the form object interact with Rails API's
      # for routing 
      delegate :to_model, :model_name, to: :player
    
      def initalize(**attributes)
        super
        @player = Player.new
      end
    
      def save
        return false unless self.valid?
        @player.assign_attributes(level: level, name: name)
        @player.save   
      end
    
      def assign_user_defaults(defaults)
        assign_attributes(defaults.attributes.slice(:name, :level))
      end
    end
    
    class PlayerController < ApplicationController
      before_action :authenticate_user
    
      def new 
        @player = PlayerForm.new
        @player.assign_user_defaults(current_user.user_default) if current_user.user_default
      end
    
      def create
        @player = PlayerForm.new(player_attributes)
        if @player.save 
          redirect_to @player
        else 
          render :new
        end
      end
    end
    

    Here I'm making the assumption that there is an authentication system and some sort of method on the user that returns their preferred set of defaults.