homeASCIIcasts

326: ActiveAttr 

(view original Railscast)

Back in episode 219 we used ActiveModel to create a model that isn’t backed by a database table but which still has some ActiveRecord features, such as validations. ActiveModel is great but isn’t very convenient to use directly like this. For example it takes quite a bit of code just to make a simple model that has some validation support.

/app/models/message.rb

class Message
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  attr_accessor :name, :email, :content
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end
  def persisted?
    false
  end
end

There is a gem called ActiveAttr that can help with this. It’s described by its author Chris Greigo as “what ActiveModel left out” which is a fair description of what it does. Using it makes it much easier to create a table-less model that behaves similarly to ActiveRecord and we’ll show you how it works in this episode.

Using ActiveAttr With a Contact Form

We’ll be working with an application which has a “Contact Us” form. When the form is filled in and submitted we want to send an email but not save the message to the database. We don’t want to use ActiveRecord at all here but we do want to use some of its features, such as validations, so that if the user fails to fill the form in correctly they see some error messages explaining what they’ve done wrong.

The contact form.

We’ve already created the controller and view for this and they work very similarly to what we’d have if we used Rails’ scaffolding. We’ll walk through it quickly now. The MessagesController has new and create actions. When the new action is triggered it will create a new instance of Message and render out a template.

/app/views/messages/new.html.erb

<h1>Contact Us</h1>
<%= form_for @message do |f| %>
  <% if @message.errors.any? %>
    <div class="error_messages">
      <h2><%= pluralize(@message.errors.count, "error") %> prohibited this message from being saved:</h2>
      <ul>
      <% @message.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :content, "Message" %><br />
    <%= f.text_area :content, :rows => 5 %>
  </div>
  <div class="actions"><%= f.submit "Send Message" %></div>
<% end %>

This view holds the code for the form. Note that we’re using form_for to define the form and passing it the message model instance from the controller. We display error messages just as we would with scaffold-generated code so from the view template this looks just like code to handle an ActiveRecord model. When the form is submitted it triggers the controller’s create action.

/app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def new
    @message = Message.new
  end
  def create
    @message = Message.new(params[:message])
    if @message.valid?
      # TODO send message here
      redirect_to root_url, notice: "Message sent! Thank you for contacting us."
    else
      render "new"
    end
  end
end

This action makes a new Message instance base based on the parameters from the form then checks that the new message is valid. If so it will email the message and redirect back to the home page. If it’s invalid it will render the form again. We need this message model to behave just like ActiveRecord, except that we’re just validating it not saving it to a database table.

The Message model currently uses ActiveModel to handle this behaviour and you can see its code at the top of the this episode. We don’t want to use this approach here, though. Instead we’re going to use ActiveAttr. To do this we’ll need to add the gem to the gemfile and run bundle to install it.

/Gemfile

gem 'active_attr'

We can now use ActiveAttr in our model.

/app/models/message.rb

class Message
  include ActiveAttr::Model
end

Note that Message doesn’t inherit from another class, it’s just a simple Ruby class. By including ActiveAttr::Model we’ll add some functionality that builds on ActiveModel to make this class behave more like an ActiveRecord model. We can define attributes for the model by using attribute and we can add validations in the same way we would for an ActiveRecord-derived class.

/app/models/message.rb

class Message
  include ActiveAttr::Model
  attribute :name
  attribute :email
  attribute :content
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
end

We now have a fully-functional model that behaves like an ActiveRecord model. If we try visiting the form again and submit it without filling in any of the fields we’ll see validation errors just like we expect but the model code is quite a bit simpler.

If the form is filled in incorrectly the validation errors will be displayed.

Mass Assignment Protection

ActiveAttr also provides mass assignment protection. Let’s say that we have a priority attribute on the Message model and that we don’t want it to be settable through form values. We can use attr_accessible to define the attributes that should be accessible just like we would with an ActiveRecord model.

/app/models/message.rb

class Message
  include ActiveAttr::Model
  attribute :name
  attribute :email
  attribute :content
  attribute :priority
  attr_accessible :name, :email, :content
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
end

We can test this behaviour in the console. If we create a new Message and try to set its priority this will fail, although we can set priority directly.

console

1.9.3p0 :001 > m = Message.new(priority: 1)
 => #<Message content: nil, email: nil, name: nil, priority: nil> 
1.9.3p0 :002 > m.priority
 => nil 
1.9.3p0 :003 > m.priority = 1
 => 1

ActiveAttr also allows us to call an attribute with a question mark to force its value to be boolean just like ActiveRecord does. Future versions of ActiveAttr will also allow us to pass additional options to attribute so that we can specify the attribute’s type and also a default value.

/app/models/message.rb

 attribute :priority, type: Integer, default: 0

These options aren’t yet available but they should be coming soon so it’s worth checking ActiveAttr’s Github page so see if these features have been released.

That’s it for this episode on ActiveAttr. It’s a great way to make table-less models. The documentation has further details of what you can do with it. We’ve used the ActiveAttr::Model module which includes everything but there are separate modules for its different features which can be used if you only need part of ActiveAttr’s functionality.