Validate

Forms of Every Shape and Color

As the Web continues to develop as an interactive, read/write medium, web forms are more useful and necessary than ever. Historically speaking, forms tend to be difficult for developers — validation, error handling, field population, redirection, data modeling and storage, these all play a part in even the simplest forms.

In the Rails world, our lives were made significantly easier thanks to the form_for helper. With this helper method, the Rails developer gets a lot for free thanks to its interaction with the ActiveRecord object that is being manipulated. Between ActiveRecord validation and the intelligent field population of form_for, creating forms becomes nearly trivial.

But what about the other forms, those that are not directly manipulating an ActiveRecord object? Forms like 'Log In' and 'Search' forms? If the developer is not using the form to CRUD an ActiveRecord model instance, he suddenly finds himself back to using low level helpers like text_field_tag and enforcing validation and other business logic within the controller.

But There's a Better Way!

All data that we receive from forms should be modeled, but we don't always have to use an ActiveRecord model class to do it. For data that needs to be modeled but not stored in the database, we can simply use a plain ol' Ruby class. Let's say that we want to create a login form - it will have fields for email and password, and a checkbox for "Remember Me". We can model this data like so:

# app/models/login.rb
class Login
  attr_reader :email, :password, :remember_me
  def initialize(params = {})
    @email = params[:email]
    @password = params[:password]
    @remember_me = params[:remember_me]
  end
  
  def authenticate
    # tie into User model for authentication
    user = User.authenticate(self.email, self.password)
    return user
  end
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    @login = Login.new
  end
  
  def create
    @login = Login.new(params[:login])
    if current_user = @login.authenticate
      redirect_to successful_login_path
    else
      render :action => 'new' # unsuccessful login
    end
  end
end
<%# app/views/sessions/new %>
<% form_for :login, :url => sessions_path do |f| %>
  <p class="form_item">
    <label for="login[email]">Email</label>
    <%= f.text_field :email %>
  </p>
  <p class="form_item">
    <label for="login[password]">Password</label>
    <%= f.password_field :password %>
  </p>
  <p class="form_item">
    <label for="login[remember_me]">Remember Me</label>
    <%= f.check_box :remember_me %>
  </p>
  <p class="form_item submit_button">
    <%= f.submit "Login" %>
  </p>
<% end %>

A Dash of Validation

The good news is that we have modeled the login data and now have a place other than the controller for login-specific logic. We are still missing validation, however. If our Login class doesn't inherit from ActiveRecord, does that mean we need to roll our own validation methods?

Thankfully Jay Fields already did the work for us by creating the Validatable Ruby gem (sudo gem install validatable). The Validatable module can be mixed into any Ruby class and provides the same validation interface that we are accustomed to using with ActiveRecord. Now we can require both the email and password to be filled out before we even try attempt a login. Here's our updated code:

# app/models/login.rb
class Login
  include Validatable
  
  validates_presence_of :email, :password
  
  attr_reader :email, :password, :remember_me
  def initialize(params = {})
    @email = params[:email]
    @password = params[:password]
    @remember_me = params[:remember_me]
  end
  
  def authenticate
    # tie into User model for authentication
    unless user = User.authenticate(self.email, self.password)
      self.errors.add(:base, "No user was found with that email and password.")
    end
    return user
  end
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    @login = Login.new
  end
  
  def create
    @login = Login.new(params[:login])
    if @login.valid? && current_user = @login.authenticate
      redirect_to successful_login_path
    else
      render :action => 'new' # unsuccessful login
    end
  end
end
<%# app/views/sessions/new %>
<%= error_messages_for :login %>
<% form_for :login, :url => sessions_path do |f| %>
  <p class="form_item">
    <label for="login[email]">Email</label>
    <%= f.text_field :email %>
  </p>
  <p class="form_item">
    <label for="login[password]">Password</label>
    <%= f.password_field :password %>
  </p>
  <p class="form_item">
    <label for="login[remember_me]">Remember Me</label>
    <%= f.check_box :remember_me %>
  </p>
  <p class="form_item submit_button">
    <%= f.submit "Login" %>
  </p>
<% end %>

Our Login class now can use the familiar validates_* class methods, the valid? instance method and all the facilities that come with having the errors object (like using error_messages_for).

It also becomes very easy to set default values for the login form. For example, if we wanted to 'check' the Remember Me box by default, we could simply change our SessionsController#new action to this:

def new
  @login = Login.new(:remember_me => true)
end

Modeling our form data in this way is a win-win scenario — we're using good coding practices by keeping business logic out of our controller, and we're also inheriting all the facilities that Rails provides by leveraging form_for, error_messages_for and validations.




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites