Posts Tagged ‘rails’

How to write a custom form builder in Rails?

28 December, 2008 06:00

Most of the current web applications contain many different types of forms. Usually they look very similar across the whole application and they are a source of repetitive code in your presentation layer.

In this blog post I would like to share with you how to minimize this repetitive code and make your form nice, concise and dry by writing custom form builders.

Let’s say you are building a new web application with your close friend. He is a ninja of css, a web standards orthodox and requires that application generate forms that look like the piece of html below.

<form action="/users" class="new_user" id="new_user" method="post">
<fieldset>
<legend>Sign up:</legend>
<dl class="required correct">
<dt><label for="user_login">Login</label></dt>
<dd><input id="user_login" name="user[login]" size="30" type="text" value="tomek" /></dd>
<dd class="notice">Lowercase letters (a-z) & numbers only - no spaces.</dd>
</dl>
<dl class="required error">
<dt><label for="user_email">E-mail</label></dt>
<dd class="error">can't be blank</dd>
<dd><input id="user_email" name="user[email]" size="30" type="text" value="" /></dd>
<dd class="notice">We won't get you spammed!</dd></dl>
...
</fieldset>
</form>

You are fully aware you cannot generate this kind of html with the standard helper methods provided by Rails.

On the one hand you don’t want to upset your friend and even start negotiations on how forms should be styled. On the other hand you would like to keep the code of your view concise and easy to maintain… let’s say more or less like the snippet below.

<% form_for @user do |f| -%>
<fieldset>
<legend>Sign up:</legend>
<%= f.text_field :login, :notice => "Lowercase letters (a-z) & numbers only - no spaces." %>
<%= f.text_field :email, :label => 'E-mail', :notice => "We wont get you spammed, we promise!" %>
...
</fieldset>
<% end -%>

Well… in tis situation… there is no other way. You have to write your own form builder!

What is a form builder?

ActionView::Helpers::FormBuilder is one of the “hidden hero” Rails classes. It is used on a frequent basis but not everybody realizes it even exists.

Whenever you use form_for helper in your view template, you pass an instance of FormBuilder class to the block associated with the method. Look at the snippet above. This is f variable.

So… if you want to create your own LabeledFormBuilder class with the enriched functionality all you need is to subclass ActionView::Helpers::FormBuilder and instruct Rails to use it. There are a few ways to do it.

One of the solutions is to create a new folder with Ruby file that will contain LabeledFormBuilder class and add it to load paths of Rails environment.

Let’s say you have just created a folder named builders with labeled_form_builder.rb file under app directory of your application. In order to tell Rails to load ruby code located in this place you have to add one line to your environment.rb file.

# app/config/envirnoment.rb
Rails::Initializer.run do |config|
...
config.load_paths += %W( #{RAILS_ROOT}/app/builders )
...
end

Next time you restart the server Ruby code placed in the labeled_form_builder.rb file will be loaded.

It is high time you rolled up your sleeves and wrote custom form builder!

Below I would like to show exemplary implementation that may help you to stay on speaking terms with your css ninja friend. I will also share a trick that I consider very useful and cool.

# app/builders/labeled_form_builder.rb
class LabeledFormBuilder < ActionView::Helpers::FormBuilder

  %w[text_field password_field text_area].each do |method_name|
    define_method(method_name) do |field_name, *args|
      options = args.extract_options!

      # Create field
      field = @template.content_tag(:dt, label(field_name, options[:label]))

      # Add validation errors
      field += field_errors(field_name)

      # Add field element
      field += @template.content_tag(:dd, super)

      # Add notice message
      field += @template.content_tag(:dd, options[:notice], :class => 'notice') if options[:notice]

      # Render field container with all elements
      @template.content_tag(:dl, field, :class => field_style(field_name))
    end
  end

  def check_box(field_name, *args)
    options = args.extract_options!
    @template.content_tag(:dl, super + label(field_name, options[:notice]), :class => "checkbox")
  end

private

  def is_required?(field_name)
    object.class.reflect_on_validations_for(field_name).map(&:macro).include?(:validates_presence_of)
  end

  def has_errors?(field_name)
    object.errors.invalid? field_name
  end

  def field_errors(field_name)
    field_errors = ''
    if has_errors?(field_name)
      object.errors[field_name].each do |msg|
        field_errors += @template.content_tag(:dd, msg, :class => 'error')
      end
    end
    field_errors
  end

  def field_style(field_name)
    field_style = ''
    if is_required?(field_name)
     field_style += 'required'
    end

    if has_errors?(field_name)
     field_style += ' error'
    elsif !object.errors.empty?
     field_style += ' correct'
    end
    field_style
  end    

  def objectify_options(options)
    super.except(:notice, :label)
  end
end

Now let me share a trick with you. Please look at is_required method. In the line…

# app/builders/labeled_form_builder.rb
...
object.class.reflect_on_validations_for(field_name).map(&:macro).include?(:validates_presence_of)
...

… I use validation reflection plugin to find out if a given field is required or not.

I also use a piece of metaprogramming to provide own implementation of text_field, password_field, text_area helper methods.

The only thing that is missing to make this little example work is intruction to Rails to use LabeledFormBuilder instead of the default one. There are two ways you can do it.

First of all you can add builder attribute to form_for helper.

<% form_for @user, :builder => LabeledFormBuilder do |f| -%>
<fieldset>
<legend>Sign up:</legend>
<%= f.text_field :login, :notice => "Lowercase letters (a-z) & numbers only - no spaces." %>
<%= f.text_field :email, :label => 'E-mail', :notice => "We wont get you spammed, we promise!" %>
...
</fieldset>
<% end -%>

Secondly you can go to your environment config file and add one line to register your custom form builder as the default one.

# app/config/envirnoment.rb
ActionView::Base.default_form_builder = LabeledFormBuilder

That’s all! Now you can send a message to your friend that his html code is safe and you can open bottle of wine :-)

Posted in: Software | 6 Comments » tags:

Internationalization in Rails 2.2

18 October, 2008 01:00

In the darkness of a night, exhausted after long hours of your day-work, you build a new kicking ass web application.

You want to change the world and make your application accessible to all good people out there… that’s why your a new-born baby has to speak several languages!

Now with Rails 2.2 it is easy! All you need to know is two API methods: I18n.transate aka I18n.t and I18n.localize aka I18n.l …and somebody who can help you with translation to exotic languages like Chinese, Thai or Polish.

In this post you will find a quick, step-by-step tutorial how to add internationalization to your Ruby on Rails application. Please be aware it is a very simple example and you have to “live on the edge” to fully benefit from this post… ok… no more fluff and let’s dive into code together!

First of all let’s create Rails app, freeze edge and create resource to test things out.

rails speak2me
cd speak2me
rake rails:freeze:edge
./script/generate resource friend
./script/generate migration AddNameToFriend name:string

Now it is time to create a directory named locales which will contain our translations….

mkdir config/locales

…and create there two translations for English and Polish language

# config/locales/en-US.yml
"en-US":
  main:
    hello: "Hello Darling!"
# config/locales/pl-PL.yml
"pl-PL":
  main:
    hello: 'Witaj Kochanie!'

In the release 2.2 of Ruby on Rails framework there was introduced the I18n module.

In the configuration file we will encapsulate details of what locales are available, where they are kept, and what is to be used as the default. Let’s create i18n.rb file under initializers directory.

# config/initializers/i18n.rb
I18n.load_path += Dir[ File.join(RAILS_ROOT, 'config', 'locales', '*.{rb,yml}') ]
I18n.default_locale = "en-US"

We have just configured I18n module and now it is right time to get use of it in our controller and view files.

First of all let’s add method to set locale. We will create a before_filter in the common base class for all of our controllers.

class ApplicationController < ActionController::Base
  before_filter :set_locale
protected
  def set_locale
    session[:locale] = params[:locale] if params[:locale]
    I18n.locale = session[:locale] || I18n.default_locale
  end
end

Next we will add index.html.erb file with localized version of greeting.

# views/friends/index.html.erb
<%= I18n.t "main.hello" %>

The only thing left is to update route configuration and test things out.

# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :friends
  map.root :controller => 'friends'
end

Open your browser and have fun… default locale is en-US but as soon as you change locale param you will get polish version of text.

I have mentioned that in order to localize you Rails 2.2 application you should be aware of two API methods: I18n.transate and I18n.localize.

So far we used only alias to one of the methods - I18n.t. So… what is a function of I18n.localize method?. Well.. this one basically allows you to format Date and Time objects for a certain locale. In order to see it in action we should update our file with polish locale and add one line in view file.

# views/friends/index.html.erb
<%= I18n.t "main.hello" %>
<%= I18n.l Time.now %>
# config/locales/pl-PL.yml
"pl-PL":
    main:
        hello: 'Witaj Kochanie!'

    date:
        formats:
            default: "%d.%m.%Y"
            short: "%e. %b"
            long: "%e. %B %Y"
            only_day: "%e"

        day_names: [Niedziela, Poniedziełek, Wtorek, Środa, Czwartek, Piątek, Sobota]
        abbr_day_names: [N, Pn, Wt, Śr, Cz, Pt, So]
        month_names: [~, Styczeń, Luty, Marzec, Kwiecień, Maj, Czerwiec, Lipiec, Sierpień, Wrzesień, Październik, Listopad, Grudzień]
        abbr_month_names: [~, Sty, Lut, Mar, Kwi, Maj, Cze, Lip, Sie, Wrz, Paz, Lis, Gru]
        order: [ :day, :month, :year ]

    time:
        formats:
            default: "%A, %e. %B %Y, %H:%M"
            time: "%H:%M"
            short: "%e. %B, %H:%M"
            long: "%A, %e. %B %Y, %H:%M"
            only_second: "%S"

        am: "przed południem"
        pm: "po południu"

That’s all folks!

For more details about internationalization in the latest version of Ruby on Rails I strongly recommend to visit Sven Fuchs’s blog. You will get knowledge directly from a source.

Posted in: Software | 3 Comments » tags: