Today we continue our analysis of the Rails Best Practices.
In the previous post we saw Named Scope, Model Association and
Following the same direction, in today’s post we’ll examine the use of Callback Model and Virtual Attribute.
1. Virtual Attribute
Suppose we have a customers list table defined as follows
2
3
4
5
6
create_table "clients", :force => true do |t|
t.string "first_name"
t.string "last_name"
t.string "street"
t.string "city"
end
Suppose we have a customers list table defined as follows but we want to define an input mask where street and city are grouped into one field called “address”.
The form with the “address” field will be defined as follows:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= text_field_tag :address %>
</li>
</ol>
<% end %>
Now in the create method we’ll have something like:
2
3
4
5
6
7
8
9
10
11
12
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
@client.street = params[:address].split(' ', 2).first
@client.city = params[:address].split(' ', 2).last
@client.save
end
...
end
We can now improve this method by defining the address field as a virtual attribute of the Client model.
2
3
4
5
6
7
8
9
10
11
12
class Client < ActiveRecord::Base
def address
[street, city].join(' ')
end
def address=(addr)
split = addr.split(' ', 2)
self.street = split.first
self.city = split.last
end
end
In the form we can now define the address field as f.field_tag instead of as text_field_tag :
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= f.text_field :address %>
</li>
</ol>
<% end %>
Finally, the controller will become simpler:
2
3
4
5
6
7
8
9
10
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
@client.save
end
...
end
2. Model Callback
Now we see a method, called Model Callback that allows us, as with the virtual attribute just seen, to simplify the form and to move the controller’s logic inside the model.
Suppose we have to implement a feature that automatically associates a set of tags to a post.
This feature, called calculate_tags return a list of tags based on the most frequent words contained in the post.
We don’t see the code of this function and instead see how and when to invoke the automatic generation of tags.
A first implementation can be as follows. Given the following form:
2
3
4
<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= check_box_tag 'calculate_tags' %>
<% end %>
the corresponding create method will be:
2
3
4
5
6
7
8
9
10
class PostController < ApplicationController
def create
@post = Post.new(params[:post])
if params[:calculate_tags] == '1'
@post.tags = TagGenerator.generate(@post.content)
else
@post.tags = ""
end
@post.save
end
Editing the Post model, let’s see how to improve the code we’ve just wrote.
First we introduce in our model an attribute, “calculate_tags”, and a filter “generate_tags”, of type before_save
2
3
4
5
6
7
8
9
10
class Post < ActiveRecord::Base
attr_accessor :calculate_tags
before_save :generate_tags
private
def generate_tags
return unless calculate_tags == '1'
self.tags = TagGenerator.generate(self.content)
end
end
Returning to the form we can now redefine the field calculate_tags as f.check_box
2
3
4
<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= f.check_box 'calculate_tags' %>
<% end %>
Finally, our create method in the controller will return to its simplest form.
2
3
4
5
6
class PostController < ApplicationController
def create
@post = Post.new(params[:post])
@post.save
end
end