#326 ActiveAttr
- Download:
- source codeProject Files in Zip (78 KB)
- mp4Full Size H.264 Video (15.8 MB)
- m4vSmaller H.264 Video (7.53 MB)
- webmFull Size VP8 Video (7.62 MB)
- ogvFull Size Theora Video (16.7 MB)
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.
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.
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.
<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.
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.
gem 'active_attr'
We can now use ActiveAttr in our model.
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.
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.
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.
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.
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.
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.