Posted about 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.

Copy
class 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.

Copy
def 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.

Copy
def 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| %>

Owner of this card:

Avatar
Alexander M
Last edit:
about 5 years ago
by Alexander M
Tags:
Software-Architecture
Posted by Alexander M to Ruby and RoR knowledge base
This website uses short-lived cookies to improve usability.
Accept or learn more