Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to extend a core Rails FormBuilder field

I am using Bootstrap 3 with Rails 4, and I wanted to create a custom FormBuilder to handle some of Bootstrap's unique HTML syntax. Specifically, I needed a custom helper that would create the form-group div wrapper around a form field, since Bootstrap applies error state to this wrapper, and not the field itself...

<div class="form-group has-error">
  <label class="col-md-3 control-label" for="user_email">Email</label>
  <div class='col-md-9'>
    <input class="form-control required-input" id="user_email" name="user[email]" placeholder="[email protected]" type="email" value="[email protected]" />
  </div>
</div>

Note the extra class has-error in the outer div...

Anyway, I wrote that helper, and it works great!

def form_group(method, options={})
  class_def = 'form-group'
  class_def << ' has-error' unless @object.errors[method].blank?
  class_def << " #{options[:class]}" if options[:class].present?
  options[:class] = class_def
  @template.content_tag(:div, options) { yield }
end

# Here's a HAML sample...

= f.form_group :email do
  = f.label :email, nil, class: 'col-md-3 control-label'
  .col-md-9
    = f.email_field :email, class: 'form-control required-input', placeholder: t('sample.email')

Now I want to utilize Bootstrap's form help text in order to display error messages. This requires me to extend Rails native helpers (such as text_field in the example above) and then call them within the the block of f.form_group.

The solution seemed simple enough: call the parent, and append my span block onto the end...

def text_field(method, options={})
  @template.text_field(method, options)
  if [email protected][method].blank?
    @template.content_tag(:span, @object.errors.full_messages_for(method), class: 'help-block')
  end
end

Only it wouldn't output any HTML, the div would simply show up empty. I've tried a bunch of diff syntax approaches:

  • super vs text_field vs text_field_tag
  • concat-ing the results -- @template.concat(@template.content_tag( [...] ))
  • dynamic vars, e.g. def text_field(method, *args) and then options = args.extract_options!.symbolize_keys!

I only ever get weird syntax errors, or an empty div. In some instances, the input field would appear, but the help text span wouldn't, or vice verse.

I'm sure I'm screwing up something simple, I just don't see it.

like image 433
Frank Koehl Avatar asked Mar 04 '14 21:03

Frank Koehl


1 Answers

Took a few days, but I ultimately stumbled onto the proper syntax. Hopefully it saves someone else's sanity!

Ruby's return automagic, combined with Rails at-times complex scoping, had me off kilter. Specifically, @template.text_field draws the content, but it must be returned by the helper method in order to appear inside the calling block. However we have to return the results of two calls...

def text_field(method, options={})
  field_errors = object.errors[method].join(', ') if [email protected][method].blank?
  content = super
  content << (@template.content_tag(:span, @object.errors.full_messages_for(method), class: 'help-block') if field_errors)
  return content
end

We must return the results of both the parent method (via super) plus our custom @template.content_tag(:span, injection. We can shorten this up a bit using Ruby's plus + operator, which concatenates return results.

def text_field(method, options={})
  field_errors = object.errors[method].join(', ') if [email protected][method].blank?
  super + (@template.content_tag(:span, @object.errors.full_messages_for(method), class: 'help-block') if field_errors)
end

Note: the form was initiated with an ActiveModel object, which is why we have access to @object. Implementing form_for without associating it with a model would require you to extend text_field_tag instead.

Here's my completed custom FormBuilder

class BootstrapFormBuilder < ActionView::Helpers::FormBuilder

  def form_group(method, options={})
    class_def = 'form-group'
    class_def << ' has-error' unless @object.errors[method].blank?
    class_def << " #{options[:class]}" if options[:class].present?
    options[:class] = class_def
    @template.content_tag(:div, options) { yield }
  end

  def text_field(method, options={})
    field_errors = object.errors[method].join(', ') if [email protected][method].blank?
    super + (@template.content_tag(:span, @object.errors.full_messages_for(method), class: 'help-block') if field_errors)
  end

end

Don't forget to tell form_for!

form_for(:user, :builder => BootstrapFormBuilder [, ...])

Edit: Here's a number of useful links that helped me along the road to enlightenment. Link-juice kudos to the authors!

  • Writing a custom FormBuilder in Rails 4.0.x
  • Formatting Rails Errors for Twitter Bootstrap
  • Very Custom Form Builders in Rails
  • SO: Nesting content tags in rails
  • SO: Rails nested content_tag
  • SO: Rails 3 Custom FormBuilder Parameters
  • SO: Trying to extend ActionView::Helpers::FormBuilder
  • RailsGuides: Form Helpers
  • Real world sample from treebook tutorial code
like image 64
Frank Koehl Avatar answered Oct 16 '22 23:10

Frank Koehl