These are my rails tips for the railscast contest:)
Contest link:
Railscasts.com/contest
So here we go:
Tip 1: Automatic assets ninimization and packaging with rails 2.0.x
This one is actually just a link:
link
In rails 2.0, it is possible to cache multiple javascript and css files into 1 file (one for js, and one for css).
Thats a really nice feature as it speeds up the page loading a lot when you have a log css and javascript files. But they
fotgot one interesting feature: minimization (removing unnecessary whitespaces and comments...). In Dave Troy's article
you can find out how to automate that with rails 2.0.x
Tip 2: Counter in looped partials
This is something I came across when reading the rails api. Its a small one, but I think there are still some
people who don't know about it.
Did you ever did something like this:
<% counter = 0 %>
<% for post in @posts do%>
<%= render :partial => 'post', :locals => {:counter => counter} %>
<% counter++ %>
<% end %>
and in the post-partial:
Post number : <%= counter %>
<%= h post.text %>
...
All the counter stuff is quite ugly, so there must be a better way, and there is :)
Apparantly there is a special counter variable when you use partials: just append _counter to the name of the partial, in this case post_counter
so in this case we would write:
<% for post in @posts do%>
<%= render :partial => 'post' %>
<% end %>
and in the post-partial:
Post number : <%= post_counter %>
<%= h post.text %>
...
Much nicer
Tip 3: Trim whitespace
Ryan Bates from
railscasts.com already pointed out how to trim whitespace
generated by erb code by using <%- -%>. There is actually a config setting who does the same thing without having
to put the minus sign all over the place. Put this in you environment.rb:
config.action_view.erb_trim_mode = ">"
Now you can use your erb blocks without the minus sign.
This actually only takes care of the whitespace after an erb block, so newlines will disappear,
but tabs and other whitespace before the block will stay. I didn't find a configuration setting to handle the whitespace
before the block.
Tip 4: Custom helpers accepting a block
If you have some stuff in your views you have to wrap in always the same divs, this is a good way to do it.
It uses a helper who accepts a block, and renders a partial, so you can easily modify your container layout.
Say you have a site with a sidebar and inside of the sidebar multiple blocks with the same layout,
someting like this:
<div id="sidebar">
<div class="block">
<div class="block_header">First Title</div>
<div class="block_body">
<b>this is the content of block 1</b>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
</div>
<div class="block">
<div class="block_header">Second Title</div>
<div class="block_body">
<b>this is the content of block 2</b>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
</div>
</div>
It would be nice if you could do something like this:
<% container "First Title" do %>
<b>this is the content of block 1</b>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
<% end %>
<% container "Second Title" do %>
<b>this is the content of block 2</b>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
<% end %>
This is how you do it:
views/other/_container.html.erb: make a partial with the container:
<div class="block">
<div class="block_header"><%= title %></div>
<div class="block_body">
<%= block %>
</div>
</div>
You can put the partial in whatever view-folder you like, but make sure you target
it the right way in your helper function!
helpers/application_helper.rb: make a helper function:
def container(title, &block)
@t = capture do
render :partial => 'other/container', :locals => { :title => title, :block => capture(&block) }
end
concat @t, block.binding
end
Tip 5: Custom form-fields
Wouldn't it be nice if you could write your own form-fields?
for example an address field would build a textfield for the street, number, zip-code, city...
so you dont have to write that same code everytime you need the address fields.
You can do it by using a custom form-builder. The actual form builder is taken from
the "Advanced Rails Recipes"-eBook (Keep Forms DRY and Flexible by Mike Mangino), but we will
extend it with an address field.
In your view you will be able to do this:
<% form_for @address, :builder => CustomFormBuilder do |f| %>
<%= f.address_field :address %>
<% end %>
so lets get started...
First, lets create our field partials.
Make a directory 'forms' in app/views with 3 partials in it:
_field.html.erb: the container for our normal fields
<div class="form_row">
<div class="form_label"><%= label %>:</div>
<div class="form_content"><%= element %></div>
</div>
_field_with_errors.html.erb: the container for fields with errors
<div class="form_row field_with_errors">
<span class="form_error"><%= error %></span>
<div class="form_label"><%= label %>:</div>
<div class="form_content"><%= element %></div>
</div>
_address_field.html.erb: the partial for the address field
<div class="form_row">
<div class="form_label">Street:</div>
<div class="form_content">
<%= text_field object, :street %>
Nr.:<%= text_field object, :number, :size => 4 %>
Bus:<%= text_field object, :bus, :size => 4 %>
</div>
<div class="form_label">Zip:</div>
<div class="form_content">
<%= text_field object, :zip, :size => 10 %>
City: <%= text_field object, :city %>
</div>
<div class="form_label">Country:</div>
<div class="form_content">
<%= country_select object, :country_id %>
</div>
</div>
Now add a file in your lib-directory that will hold the formbuilder
custom_form_builder.rb
module ActionView
module Helpers
module FormHelper
def address_field(object_name, method, options = {})
#stub function
end
end
end
end
class CustomFormBuilder < ActionView::Helpers::FormBuilder
# override default error div for form elemens with errors
ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag }
helpers = field_helpers +
%w(date_select datetime_select time_select collection_select) +
%w(collection_select select country_select time_zone_select) -
%w(label fields_for)
helpers.each do |name|
define_method name do |field, *args|
options = args.detect {|argument| argument.is_a?(Hash)} || {}
build_shell(field, options) do
super
end
end
end
def address_field(method, options = {})
build_shell('address', options){ @template.address_field(@object_name, method, options.merge(:object => @object)) }
end
def build_shell(field, options)
@template.capture do
locals = {
:element => yield,
:label => label(field, options[:label]),
:help => options[:help]
}
if has_errors_on?(field)
locals.merge!(:error => error_message(field, options))
@template.render :partial => 'forms/field_with_errors',
:locals => locals
elsif (field == 'address')
@template.render :partial => 'forms/address_field',
:locals => {:object => @object_name}
else
@template.render :partial => 'forms/field',
:locals => locals
end
end
end
def error_message(field, options)
if has_errors_on?(field)
errors = object.errors.on(field)
errors.is_a?(Array) ? errors.to_sentence : errors
else
''
end
end
def has_errors_on?(field)
!(object.nil? || object.errors.on(field).blank?)
end
end
Extra Tip: own base controller for profile page
If you have a website where all users can have a profile page with a guestbook section,
photo-album section... and you want stuff to happen on al profile-controllers, but not on the
main-website pages. You cant put them in application.rb, so, lets make a custom base-controller
for our profile controllers, so we can put before_filters in that base-controller (like we would put
before_filters in application.rb for stuff that has to happen on every page).
in app/controllers, make a profile_controller:
profile_controller.rb
class ProfileController < ApplicationController
before_filter :get_profile_user
def get_profile_user
@profile_user = User.find_by_name(params[:profile_user]) #you will have to set up your routes to have such a parameter
end
end
now you can inherit for example you GuestbookController from ProfileController
app/guestbook_controller.rb:
class GuestbookController < ProfileController #ProfileController instead of ApplicationController
def index
@profile_user #here we can access @profile_user, as we fetched it in the profile_controller with a before_filter
end
end