Fernando Luizão

Desenvolvimento de software e nerdices em geral

FormBuilders: Padronizando seus formulários no Rails

with 3 comments

A um ano atrás, o Urubatan escreveu um artigo sobre como diminuir a repetição nas views e padronizar formulários. A abordagem dele foi escrever helpers para encapsular o formulário dentro de uma tabela (não vou entrar no mérito do uso de tabelas para formatação, não sou xiita :)). Se você não leu o artigo dele, recomendo que leia. Vou revisitar o artigo dele e apresentar um recurso do Rails que muita gente desconhece (ou ignora): FormBuilders.

O que é um FormBuilder?

Como o nome sugere, um FormBuilder é uma classe para auxiliar a construção de formulários. É essa classe que nos oferece vários métodos que usamos diariamente em nossas views, como text_field, check_box, e vários outros. A maioria das pessoas usa esses métodos sem saber de onde eles vem. Para entender melhor a estrutura do form builder padrão do Rails, recomendo a leitura desse artigo.

Criando nosso FormBuilder

Para criarmos um builder personalizado, temos duas alternativas: implementar os métodos da classe ActionView::Helpers::FormBuilder OU estender essa classe. A segunda é obviamente mais fácil e rápida, e para atingirmos nosso objetivo vamos usá-la. Queremos que nosso FormBuilder nos permita trocar isso (retirado do exemplo do Urubatan):

<% form_for(@example) do |f| %>
  <table>
    <tr>
      <td>Name</td>
      <td><%= f.text_field :name %></td>
    </tr>
    <tr>
      <td>Url</td>
      <td><%= f.text_field :url %></td>
    </tr>
    <tr>
      <td colspan="2"><%= f.submit "Update" %></td>
    </tr>
</table>
<% end %>

Por isso:

<% form_for(@example) do |f| %>
  <%= f.text_field :name %>
  <%= f.text_field :url %>
  <%= f.submit "Update" %>
<% end %>

Vamos ao trabalho!

class MyFormBuilder < ActionView::Helpers::FormBuilder

  # vamos embulhar esses helpers em uma tr
  %w&#91;text_field password_field text_area&#93;.each do |method_name|
    define_method(method_name) do |field_name, *args|
      options = args.extract_options!

      # cria o label dentro de uma td
      label = @template.content_tag(:td, label(field_name, options&#91;:label&#93; || field_name.to_s.humanize))

      # cria o elemento dentro de uma td
      input = @template.content_tag(:td, super)

      # cria uma tr com o label e o input criados
      @template.content_tag(:tr, label + input)
    end
  end

  def submit(value = "Save changes", options = {})
    button = @template.content_tag(:td, super, :colspan => 2)
    @template.content_tag(:tr, button)
  end

end

Essa classe pode ser colocada em um arquivo dentro do diretório lib e requerida no environment. Implementei apenas alguns helpers apenas para demonstração, mas outros helpers (check_box, select, etc) também podem ser personalizados a gosto. Agora podemos testar a renderização do formulário usando nosso builder:

<% form_for(@example, :builder => MyFormBuilder) do |f| %>
  <%= f.text_field :name %>
  <%= f.text_field :url %>
  <%= f.submit "Update" %>
<% end %>

Humm, nada de diferente… olhando o html gerado podemos ver que esta correto, os campos estão dentro de tr’s, porém não estão dentro de uma tabela. Vamos sobrescrever o helper form_for para conseguir isso:

module ActionView
  module Helpers
    module FormHelper

      def form_for(record_or_name_or_array, *args, &proc)
        raise ArgumentError, "Missing block" unless block_given?

        options = args.extract_options!

        case record_or_name_or_array
        when String, Symbol
          object_name = record_or_name_or_array
        when Array
          object = record_or_name_or_array.last
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
          apply_form_for_options!(record_or_name_or_array, options)
          args.unshift object
        else
          object = record_or_name_or_array
          object_name = ActionController::RecordIdentifier.singular_class_name(object)
          apply_form_for_options!([object], options)
          args.unshift object
        end

        concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
        concat('<table>')
        fields_for(object_name, *(args << options), &proc)
        concat('</table>')
        concat('</form>')
      end

    end
  end
end

Monkey patching não é a melhor coisa do mundo, mas nesse caso não vi outra solução melhor e achei válido usar. No artigo do Urubatan ele adicionou as tags “table” dentro do próprio formulário. Se tivesse que alterar a marcação no futuro, teria que ser alterado em vários formulários para tirar as tags table. O monkey patch introduz risco de quebrar o sistema em caso de mudanças no Rails, mas acho que vale mais a pena alterar apenas um arquivo do que um monte de formulários :).

Testando novamente… funcionou :). Muito bom, agora vamos criar um plugin para reusar esse form builder em outros projetos:

script/generate plugin my_builder
      create  vendor/plugins/my_builder/lib
      create  vendor/plugins/my_builder/tasks
      create  vendor/plugins/my_builder/test
      create  vendor/plugins/my_builder/README
      create  vendor/plugins/my_builder/MIT-LICENSE
      create  vendor/plugins/my_builder/Rakefile
      create  vendor/plugins/my_builder/init.rb
      create  vendor/plugins/my_builder/install.rb
      create  vendor/plugins/my_builder/uninstall.rb
      create  vendor/plugins/my_builder/lib/my_builder.rb
      create  vendor/plugins/my_builder/tasks/my_builder_tasks.rake
      create  vendor/plugins/my_builder/test/my_builder_test.rb
      create  vendor/plugins/my_builder/test/test_helper.rb

No arquivo vendor/plugins/my_builder/lib/my_builder.rb coloque o a nossa classe MyFormBuilder e também a reimplementação do form_for. No arquivo vendor/plugins/my_builder/init.rb, coloque o seguinte conteúdo:

require 'my_builder'

# define como o FormBuilder padrao
ActionView::Base.default_form_builder = MyFormBuilder

A última linha serve justamente para não termos que especificar o builder em cada form que fizermos, definindo nosso builder como o padrão.

Conclusões

FormBuilders são relativamente simples para personalizar e permitem remover muito código repetido de forma elegante, além de facilitar a manutenção. Pena que muita gente nem sabe que eles existem… espero que esse artigo tenha mostrado as possibilidades de uso dos builders e incentive o pessoal a usá-los mais. Quem quiser exemplos mais concretos, basta pesquisar no github, tem alguns plugins por lá :).

Bons estudos!

Advertisements

Written by fernandoluizao

January 19, 2009 at 7:43 pm

Posted in Rails

3 Responses

Subscribe to comments with RSS.

  1. Muito bom o post 😀
    Mas só pra constar, o meu post não era sobre padronizar formulários, este foi só o exemplo, o post era sobre view helpers em plugins, coisa bastante utilizada por exemplo nos plugins que proveem um calendário javascript 😀
    PS.: também não curto tabelas para layout, mas para escrever o código do post foi mais rápido assim 😀

    Rodrigo Urubatan

    January 19, 2009 at 8:11 pm

  2. inclusive da para utilizar as duas coisas junto, encapsular o FormBuilder e o método “form_for” que tu alterou para usar o teu FormBuilder em um plugin 😀

    Rodrigo Urubatan

    January 19, 2009 at 8:13 pm

  3. Sei que seu post não era sobre padronizar, mas acabei usando seu post como gancho :). O seu foi um dos primeiros posts que eu li sobre criação de plugins, e achei que seria interessante dar uma nova roupagem usando builders, Inclusive no fim do artigo eu explico como criar o plugin com o FormBuilder e o form_for :).

    Abraços!

    fernandoluizao

    January 19, 2009 at 9:22 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: