Posted over 5 years ago. Visible to the public. Linked content.
Form Object from Railscasts
Models in a Rails application can easily become very complicated as more logic gets added to them. Fortunately there are several ways to refactor them that can help to clean up them up. Most of the behaviour in the User
model of our example application is to do with forms. It has some custom virtual accessors, a number of validations, some callbacks and an association that accepts nested attributes for another model. It also has some virtual attributes and custom validations.
Copyclass User < ActiveRecord::Base has_secure_password attr_accessor :changing_password, :original_password, :new_password validates_presence_of :username validates_uniqueness_of :username validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ validates_length_of :password, minimum: 6, on: :create validate :verify_original_password, if: :changing_password validates_presence_of :original_password, :new_password, if: :changing_password validates_confirmation_of :new_password, if: :changing_password validates_length_of :new_password, minimum: 6, if: :changing_password before_create :generate_token before_update :change_password, if: :changing_password has_one :profile accepts_nested_attributes_for :profile def subscribed subscribed_at end def subscribed=(checkbox) subscribed_at = Time.zone.now if checkbox == "1" end # Other methods omitted. end
By extracting these out into form objects we can clean up our model considerably.
Copy# app/forms/password_form.rb class PasswordForm include ActiveModel::Model def persisted? false end attr_accessor :original_password, :new_password validate :verify_original_password validates_presence_of :original_password, :new_password validates_confirmation_of :new_password validates_length_of :new_password, minimum: 6 def initialize(user) @user = user end def submit(params) self.original_password = params[:original_password] self.new_password = params[:new_password] self.new_password_confirmation = params[:new_password_confirmation] if valid? @user.password = new_password @user.save! true else false end end def verify_original_password unless @user.authenticate(original_password) errors.add :original_password, "is not correct" end end end # app/forms/signup_form.rb class SignupForm include ActiveModel::Model def persisted? false end def self.model_name ActiveModel::Name.new(self, nil, "User") end validates_presence_of :username validate :verify_unique_username validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ validates_length_of :password, minimum: 6 delegate :username, :email, :password, :password_confirmation, to: :user delegate :twitter_name, :github_name, :bio, to: :profile def user @user ||= User.new end def profile @profile ||= user.build_profile end def submit(params) user.attributes = params.slice(:username, :email, :password, :password_confirmation) profile.attributes = params.slice(:twitter_name, :github_name, :bio) self.subscribed = params[:subscribed] if valid? generate_token user.save! profile.save! true else false end end def subscribed user.subscribed_at end def subscribed=(checkbox) user.subscribed_at = Time.zone.now if checkbox == "1" end def generate_token begin user.token = SecureRandom.hex end while User.exists?(token: user.token) end def verify_unique_username if User.exists? username: username errors.add :username, "has already been taken" end end end
Now methods :new and :create in our UsersController
will look much cleaner.
Copydef new @signup_form = SignupForm.new end def create @signup_form = SignupForm.new if @signup_form.submit(params[:user]) session[:user_id] = @signup_form.user.id redirect_to @signup_form.user, notice: "Thank you for signing up!" else render "new" end end
Here is an example of how we are gonna use it.
Copy# users/new.html.erb <%= form_for @signup_form do |f| %>
The same works for PasswordsController
.
Copydef new @password_form = PasswordForm.new(current_user) end def create @password_form = PasswordForm.new(current_user) if @password_form.submit(params[:password_form]) redirect_to current_user, notice: "Successfully changed password." else render "new" end end
Usage:
Copy# passwords/new.html.erb <%= form_for @password_form, url: passwords_path, method: :post do |f| %>