Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Form builder for Twitter Bootstrap v2 form elements
# This file goes in config/initializers
require 'bootstrap_form_builder'
# Make this the default Form Builder. You can delete this if you don't want form_for to use
# the bootstrap form builder by default
ActionView::Base.default_form_builder = BootstrapFormBuilder::FormBuilder
# Add in our FormHelper methods, so you can use bootstrap_form_for.
ActionView::Base.send :include, BootstrapFormBuilder::FormHelper
### Only use one of these error handling methods ###
# Get rid of the rails error handling completely.
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance_tag|
"#{html_tag}".html_safe
end
# Only remove the default rails error handling on input and label
# Relies on the Nokogiri gem.
# Credit to https://github.com/ripuk
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
html = %(<div class="field_with_errors">#{html_tag}</div>).html_safe
elements = Nokogiri::HTML::DocumentFragment.parse(html_tag).css "label, input"
elements.each do |e|
if e.node_name.eql? 'label'
html = %(#{e}).html_safe
elsif e.node_name.eql? 'input'
html = %(#{e}).html_safe
end
end
html
end
# This file goes in lib/
# Usage:
#= bootstrap_form_for @calendar_entry do |f|
# %fieldset
# %legend= locals[:title] || 'Edit Calendar Entry'
# = f.text_field :name, :class => 'span3'
# = f.text_area :description, :class => 'span3'
# = f.jquery_datetime_select :start_time, :class => 'span3'
# = f.jquery_datetime_select :end_time, :class => 'span3'
# = f.check_box :all_day
# = f.text_field :tag_string, :label => {:text => 'Tags'}, :class => 'span3'
# .form-actions
# = f.submit 'Save', :class => 'btn btn-primary'
# = link_to 'Cancel', calendar_entries_path, :class => 'btn'
#
# If you don't use HAML, here is the same thing in ERB
# <%= bootstrap_form_for @calendar_entry do |f| %>
# <%= content_tag :legend, (locals[:title] || 'Edit Calendar Entry') %>
# <%= f.text_field :name, :class => 'span3' %>
# <%= f.text_area :description, :class => 'span3' %>
# <%= f.jquery_datetime_select :start_time, :class => 'span3' %>
# <%= f.jquery_datetime_select :end_time, :class => 'span3' %>
# <%= f.check_box :all_day %>
# <%= f.text_field :tag_string, :label => {:text => 'Tags'}, :class => 'span3' do %>
# <p class="help-block">Use commas to separate tags.</p>
# <% end %>
# <div class="form-actions">
# <%= f.submit 'Save', :class => 'btn btn-primary' %>
# <%= link_to 'Cancel', calendar_entries_path, :class => 'btn' %>
# </div>
module BootstrapFormBuilder
module FormHelper
[:form_for, :fields_for].each do |method|
module_eval do
define_method "bootstrap_#{method}" do |record, *args, &block|
# add the TwitterBootstrap builder to the options
options = args.extract_options!
options[:builder] = BootstrapFormBuilder::FormBuilder
if method == :form_for
options[:html] ||= {}
options[:html][:class] ||= 'form-horizontal'
end
# call the original method with our overridden options
send method, record, *(args << options), &block
end
end
end
end
class FormBuilder < ActionView::Helpers::FormBuilder
include FormHelper
def get_error_text(object, field, options)
if object.nil? || options[:hide_errors]
""
else
errors = object.errors[field.to_sym]
if errors.empty? then "" else errors.first end
end
end
def get_object_id(field, options)
object = @template.instance_variable_get("@#{@object_name}")
return options[:id] || object.class.name.underscore + '_' + field.to_s
end
def get_label(field, options)
labelOptions = {:class => 'control-label'}.merge(options[:label] || {})
text = labelOptions[:text] || nil
labelTag = label(field, text, labelOptions)
end
def submit(value, options = {}, *args)
super(value, {:class => "btn btn-primary"}.merge(options), *args)
end
def jquery_date_select(field, options = {})
id = get_object_id(field, options)
date =
if options['start_date']
options['start_date']
elsif object.nil?
Date.now
else
object.send(field.to_sym)
end
date_picker_script = "<script type='text/javascript'>" +
"$( function() { " +
"$('##{id}')" +
".datepicker( $.datepicker.regional[ 'en-NZ' ] )" +
".datepicker( 'setDate', new Date('#{date}') ); } );" +
"</script>"
return basic_date_select(field, options.merge(javascript: date_picker_script))
end
def basic_date_select(field, options = {})
placeholder_text = options[:placeholder_text] || ''
id = get_object_id(field, options)
errorText = get_error_text(object, field, options)
wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end
labelTag = get_label(field, options)
date =
if options[:start_date]
options[:start_date]
elsif object.nil?
Date.now.utc
else
object.send(field.to_sym)
end
javascript = options[:javascript] ||
"
<script>
$(function() {
var el = $('##{id}');
var currentValue = el.val();
if(currentValue.trim() == '') return;
el.val(new Date(currentValue).toString('dd MMM, yyyy'));
});
</script>"
("<div class='#{wrapperClass}'>" +
labelTag +
"<div class='controls'>" +
super_text_field(field, {
:id => id, :placeholder => placeholder_text, :value => date.to_s,
:class => options[:class]
}.merge(options[:text_field] || {})) +
errorSpan +
javascript +
"</div>" +
"</div>").html_safe
end
def jquery_datetime_select(field, options = {})
id = get_object_id(field, options)
date_time =
if options['start_time']
options['start_time']
elsif object.nil?
DateTime.now.utc
else
object.send(field.to_sym)
end
datetime_picker_script = "<script type='text/javascript'>" +
"$( function() { " +
"$('##{id}')" +
".datetimepicker( $.datepicker.regional[ 'en-NZ' ] )" +
".datetimepicker( 'setDate', new Date('#{date_time}') ); } );" +
"</script>"
return basic_datetime_select(field, options.merge(javascript: datetime_picker_script))
end
def basic_datetime_select(field, options = {})
placeholder_text = options[:placeholder_text] || ''
id = get_object_id(field, options)
errorText = get_error_text(object, field, options)
wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end
labelTag = get_label(field, options)
date_time =
if options[:start_time]
options[:start_time]
elsif object.nil?
DateTime.now.utc
else
object.send(field.to_sym)
end
javascript = options[:javascript] ||
"
<script>
$(function() {
var el = $('##{id}');
var currentValue = el.val();
if(currentValue.trim() == '') return;
el.val(new Date(currentValue).toString('dd MMM, yyyy HH:mm'));
});
</script>"
("<div class='#{wrapperClass}'>" +
labelTag +
"<div class='controls'>" +
super_text_field(field, {
:id => id, :placeholder => placeholder_text, :value => date_time.to_s,
:class => options[:class]
}.merge(options[:text_field] || {})) +
errorSpan +
javascript +
"</div>" +
"</div>").html_safe
end
basic_helpers = %w{text_field text_area select email_field password_field check_box number_field}
multipart_helpers = %w{date_select datetime_select}
basic_helpers.each do |name|
# First alias old method
class_eval("alias super_#{name.to_s} #{name}")
define_method(name) do |field, *args, &help_block|
options = args.last.is_a?(Hash) ? args.last : {}
object = @template.instance_variable_get("@#{@object_name}")
labelTag = get_label(field, options)
errorText = get_error_text(object, field, options)
wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end
("<div class='#{wrapperClass}'>" +
labelTag +
"<div class='controls'>" +
super(field, *args) +
errorSpan +
(help_block ? @template.capture(&help_block) : "") +
"</div>" +
"</div>"
).html_safe
end
end
multipart_helpers.each do |name|
define_method(name) do |field, *args, &help_block|
options = args.last.is_a?(Hash) ? args.last : {}
object = @template.instance_variable_get("@#{@object_name}")
labelTag = get_label(field, options)
options[:class] = 'inline ' + options[:class] if options[:class]
errorText = get_error_text(object, field, options)
wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end
("<div class='#{wrapperClass}'>" +
labelTag +
"<div class='controls'>" +
super(field, *args) +
errorSpan +
(help_block ? @template.capture(&help_block) : "") +
"</div>" +
"</div>"
).html_safe
end
end
end
end

Whilst this looks like what I need, I'm having trouble loading the module. Where should I put it in my app structure, and how do I ensure it's loaded on app start? Thanks..

Owner

I just edited it to add my initializer file, and a comment at the top of both files to indicate where they go.

Cool, thanks, it's looking great now! Only issue now is with form validation - upon validation error you still get the hideous "field_with_errors" default Rails crap around the label and input, which breaks Bootstrap's pretty form error styling.

I hacked it out using the following piece of code in my application.rb, and the Nokogiri gem:

# remove hideous styling on standard rails form validation errors
    ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
      html = %(<div class="field_with_errors">#{html_tag}</div>).html_safe

      elements = Nokogiri::HTML::DocumentFragment.parse(html_tag).css "label, input"
      elements.each do |e|
        if e.node_name.eql? 'label'
          html = %(#{e}).html_safe
        elsif e.node_name.eql? 'input'
          html = %(#{e}).html_safe
        end
      end
      html
    end

I also removed the default_form_builder section from the initializer as I prefer to enable your builder myself rather than have it on by default :-)

Thanks for the great work!

Owner

Thanks a lot :-)

Turns out I had another initializer called view_error_handler.rb (see below), which just got rid of all error info. Yours looks like a better solution though.

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance_tag|
  "#{html_tag}".html_safe
end

Thanks for this, great work :) I did however have to change

label = label(field, options[:label])

To

label = label(field, options[:label], :class => "control-label")

In bootstrap_form_builder.rb as the label requires this class to be positioned correctly. Im fairly new to rails so maybe im missing something?

Thanks

Owner

Hi Ben,

Thanks for that. I've updated the label generation to add class="control-label" by default, but you can override it by adding :label => {:class => 'my-custom-class'} to your form field.

glongman commented Mar 3, 2012

Jamie, I tweaked this so that the helpers can take blocks so one can inject more content into the

<div class="controls'> 

tags.

I forked since I never forked a gist before and wanted to try it out

https://gist.github.com/1967910

Will this work with form_tag as well? (prolly showing my complete newness with this question)

Cheers

Owner

@glongman I just merged your changes just to see how it works with Gists - works just like a normal repo. Thanks for the additions :-)

@drew1two Don't worry, there are no dumb questions. Rails creates a FormBuilder object when you use form_for @model do |f| - f is an instance of whatever FormBuilder class you have defined as default. I don't think it is possible to construct a FormBuilder without passing it a model object - I certainly rely on it to get a bunch of information for the labels and inputs. You can just pass in a ModelClass.new if you don't have one constructed at that point, but it needs to be the same type of object as what you are building the form for.

I hope that was more helpful than confusing.

Thanx for the reply,
Main reason for asking is that I'm integrating omniauth-identity into my app (though the user can also have Multiple login options through omniauth) and even though I've associated the Identity with my User class, omniauth-identity doesn't play nice with form_for due to the custom action url it uses.
I will be using your form_builder throughout the rest of the app, and was hoping to keep the user registration consistent in its display of error messages.
Maybe I could mimic your engines display just for the registration and login somehow. I'll look into it.

Thanx again for the reply, and this great builder as well :)

Owner
vfilby commented Apr 9, 2012

Stumbled across this while looking up how to override the form error for bootstrap style form, and it works perfectly. Thank you!

davidw commented Apr 23, 2012

Any reason not to make a gem of this, so that people can easily track updates?

Also, I already have labels, how does that work? Do I need to go through and eliminate all of them?

Owner

Really nice code :)
Just changed this:

def get_label(field, options) labelOptions = {:class => 'control-label'}.merge(options[:label] || {}) labelTag = label(field, options[:labeltext], labelOptions) end

So i could change the label text. It's currently not accepting the :label=>"text" code

Wouldn't it be nice to create a form handler for f.submit so that it would always apply :class => 'btn btn-primary' so that we don't have to specify this on every layout?

<%= f.submit 'Save', :class => 'btn btn-primary' %>

would go to:

<%= f.submit 'Save'>
Owner

I agree with @bruno-campos - I had the same problem trying to pass in label text per the example.

:label => 'Blah"

I'm now using his code and passing:

:label_text => 'Blah'

And this:

def get_label(field, options)
  labelOptions = {:class => 'control-label'}.merge(options[:label] || {})
  labelTag = label(field, options[:label_text], labelOptions)
end

Oh, wait... I see that you are supposed to pass a hash for the label options, not just a string value.

Even so, I like the solution by @bruno-campos, as it requires a little less code to specify the label text, which is a pretty common thing to do.

Owner

Can you explain exactly WHY you are "removing the default rails error handling on input and label" in the bootstrap.rb file?

Is this what's needed for the f.submit? (Taken from another gist)

def submit(*args)
  super(*args, class: "btn primary")
end
vfilby commented Apr 30, 2012

@mattslay I think the reason for removing the default error handling classes/markup is so that we can use the bootstrap style classes/markup for errors. This means that you get the bootstrap formatting for form handling.

Owner

@mattslay @vfilby has it right - the default rails error handling wraps broken field's input and label in a div (and might add some text, I can't remember) which doesn't work well with the Bootstrap way of showing errors. It's just to make it look nice, it doesn't actually remove the error handling outside the view.

Owner

Also, that snippet looks pretty similar to what I was thinking - I'd probably do something like this:

def submit(value, options, *args)
  super(value, {:class => "btn btn-primary"}.merge(options), *args)
end

That will make the class "btn btn-primary" by default, but allow overriding of it.

mattslay commented May 1, 2012

Your code works, but I added ={} on the options parameter so it would default to an empty hash if nothing was passed in:

def submit(value, options = {}, *args)
  super(value, {:class => "btn btn-primary"}.merge(options), *args)
end
Owner

Thanks Matt, I've added it to the Gist now.

mattslay commented May 7, 2012

Hmmm... I have an Integer field in one of my tables, and the Rails view generator spit out a "number_field":

<%= f.number_field :port %>

Yet this form_helper doesn't have any support for that field type. Should it be added? Or should I use a regular text_field?

If I use a text_field, will Rails know to cast it back to an integer type when the record is saved by the user?

Owner
mattslay commented May 7, 2012
Owner
mattslay commented May 7, 2012
Owner
mattslay commented May 7, 2012
Owner
mattslay commented May 9, 2012

Ok.... It works fine to add number_field to:

basic_helpers = %w{text_field text_area select email_field password_field check_box number_field}

Here is a screenshot of what the number_field looks like in Safari (notice that it has a spinner):

http://www.screencast.com/t/YwmmcYYhV6z

And, yes, even when the database field is an integer field, you can render it as a text_field, and the record will be saved just fine (at least it did on SqlLite). But if you wan't to flag it as a number_field, you can render it that way, in case you want/need the slightly different output, or to take advantage of any special browser, jquery, or javasript handling for type=number that may be out there.

Owner

Thanks a lot Matt, I've added it to the code now.

allard commented May 10, 2012

Here's my updated version with the following changes:

  • Added the option of using either :label_text => 'something' or :label => { :text => 'something' }
  • fixed check_box parsing. The check_box option is the first argument, not the last is with the other helpers
  • Updated the check_box label in according with bootstrap documentation to do <label><input type="checkbox">label text</label>, this will write the check box label after the check box, and with the label enclosing the input, you can click anywhere on the label or form element to change the checkbox.
  • Added a :description => 'left hand label' if you want to include a left hand label, same as the other forms.
  • Removed the :label, :label_text and :description fields from the options so they won't be included in the fields like <input label_text="something">

I haven't tested inline check boxes, or adding options to check boxes because I almost never do.

Enjoy!

# This file goes in lib/
# Usage:
#= bootstrap_form_for @calendar_entry do |f|
#  %fieldset
#    %legend= locals[:title] || 'Edit Calendar Entry'
#    = f.text_field :name, :class => 'span3'
#    = f.text_area :description, :class => 'span3'
#    = f.jquery_datetime_select :start_time, :class => 'span3'
#    = f.jquery_datetime_select :end_time, :class => 'span3'
#    = f.check_box :all_day, { :label_text => 'Agree?', :description => 'left hand label' }
#    = f.text_field :tag_string, :label => {:text => 'Tags'}, :class => 'span3'
#    .form-actions
#      = f.submit 'Save', :class => 'btn btn-primary'
#      = link_to 'Cancel', calendar_entries_path, :class => 'btn'
#
# If you don't use HAML, here is the same thing in ERB
# <%= bootstrap_form_for @calendar_entry do |f| %>
#   <%= content_tag :legend, (locals[:title] || 'Edit Calendar Entry') %>
#   <%= f.text_field :name, :class => 'span3' %>
#   <%= f.text_area :description, :class => 'span3' %>
#   <%= f.jquery_datetime_select :start_time, :class => 'span3' %>
#   <%= f.jquery_datetime_select :end_time, :class => 'span3' %>
#   <%= f.check_box :all_day, { :label_text => 'Agree?', :description => 'left hand label' } %>
#   <%= f.text_field :tag_string, :label => {:text => 'Tags'}, :class => 'span3' do %>
#     <p class="help-block">Use commas to separate tags.</p>
#   <% end %>
#   <div class="form-actions">
#     <%= f.submit 'Save', :class => 'btn btn-primary' %>
#     <%= link_to 'Cancel', calendar_entries_path, :class => 'btn' %>
#   </div>

module BootstrapFormBuilder
  module FormHelper
    [:form_for, :fields_for].each do |method|
      module_eval do
        define_method "bootstrap_#{method}" do |record, *args, &block|
          # add the TwitterBootstrap builder to the options
          options           = args.extract_options!
          options[:builder] = BootstrapFormBuilder::FormBuilder

          if method == :form_for
            options[:html] ||= {}
            options[:html][:class] ||= 'form-horizontal'
          end

          # call the original method with our overridden options
          send method, record, *(args << options), &block
        end
      end
    end
  end

  class FormBuilder < ActionView::Helpers::FormBuilder
    include FormHelper

    def get_error_text(object, field, options)
      if object.nil? || options[:hide_errors]
        ""
      else
        errors = object.errors[field.to_sym]
        if errors.empty? then "" else errors.first end
      end
    end

    def get_object_id(field, options)
      object = @template.instance_variable_get("@#{@object_name}")
      return options[:id] || object.class.name.underscore + '_' + field.to_s
    end

    def get_label(field, options)
      labelOptions = {:class => 'control-label'}.merge(options[:label] || {})
      text = options[:label_text] || labelOptions[:text] || nil
      options.delete(:label)
      options.delete(:label_text)
      labelTag = label(field, text, labelOptions)
    end

    def submit(value, options = {}, *args)
      super(value, {:class => "btn btn-primary"}.merge(options), *args)
    end

    def jquery_date_select(field, options = {})
      id = get_object_id(field, options)

      date = 
        if options['start_date']
          options['start_date']
        elsif object.nil?
          Date.now
        else
          object.send(field.to_sym)
        end

      date_picker_script = "<script type='text/javascript'>" +
            "$( function() { " +
              "$('##{id}')" +
              ".datepicker( $.datepicker.regional[ 'en-NZ' ] )" +
              ".datepicker( 'setDate', new Date('#{date}') ); } );" +
          "</script>"
      return basic_date_select(field, options.merge(javascript: date_picker_script))
    end

    def basic_date_select(field, options = {})
      placeholder_text = options[:placeholder_text] || ''
      id = get_object_id(field, options)

      errorText = get_error_text(object, field, options)
      wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
      errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end

      labelTag = get_label(field, options)

      date = 
        if options[:start_date]
          options[:start_date]
        elsif object.nil?
          Date.now.utc
        else
          object.send(field.to_sym)
        end

      javascript = options[:javascript] || 
        "
  <script>
    $(function() { 
      var el = $('##{id}');
      var currentValue = el.val();
      if(currentValue.trim() == '') return;
      el.val(new Date(currentValue).toString('dd MMM, yyyy'));
    });
  </script>"

      ("<div class='#{wrapperClass}'>" + 
        labelTag +
        "<div class='controls'>" +
          super_text_field(field, {
            :id => id, :placeholder => placeholder_text, :value => date.to_s,
            :class => options[:class]
          }.merge(options[:text_field] || {})) + 
          errorSpan +
          javascript +
        "</div>" +
      "</div>").html_safe
    end

    def jquery_datetime_select(field, options = {})
      id = get_object_id(field, options)

      date_time = 
        if options['start_time']
          options['start_time']
        elsif object.nil?
          DateTime.now.utc
        else
          object.send(field.to_sym)
        end

      datetime_picker_script = "<script type='text/javascript'>" +
            "$( function() { " +
              "$('##{id}')" +
              ".datetimepicker( $.datepicker.regional[ 'en-NZ' ] )" +
              ".datetimepicker( 'setDate', new Date('#{date_time}') ); } );" +
          "</script>"
      return basic_datetime_select(field, options.merge(javascript: datetime_picker_script))
    end

    def basic_datetime_select(field, options = {})
      placeholder_text = options[:placeholder_text] || ''
      id = get_object_id(field, options)

      errorText = get_error_text(object, field, options)
      wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
      errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end

      labelTag = get_label(field, options)

      date_time = 
        if options[:start_time]
          options[:start_time]
        elsif object.nil?
          DateTime.now.utc
        else
          object.send(field.to_sym)
        end

      javascript = options[:javascript] || 
        "
  <script>
    $(function() { 
      var el = $('##{id}');
      var currentValue = el.val();
      if(currentValue.trim() == '') return;
      el.val(new Date(currentValue).toString('dd MMM, yyyy HH:mm'));
    });
  </script>"

      ("<div class='#{wrapperClass}'>" + 
        labelTag +
        "<div class='controls'>" +
          super_text_field(field, {
            :id => id, :placeholder => placeholder_text, :value => date_time.to_s,
            :class => options[:class]
          }.merge(options[:text_field] || {})) + 
          errorSpan +
          javascript +
        "</div>" +
      "</div>").html_safe
    end

    basic_helpers = %w{text_field text_area select email_field password_field number_field}

    multipart_helpers = %w{date_select datetime_select}

    trailing_label_helpers = %w{check_box}

    basic_helpers.each do |name|
      # First alias old method
      class_eval("alias super_#{name.to_s} #{name}")

      define_method(name) do |field, *args, &help_block|
        options = args.last.is_a?(Hash) ? args.last : {}
        object = @template.instance_variable_get("@#{@object_name}")

        labelTag = get_label(field, options)

        errorText = get_error_text(object, field, options)

        wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
        errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end 
        ("<div class='#{wrapperClass}'>" +
            labelTag + 
            "<div class='controls'>" + 
              super(field, *args) +
              errorSpan +
              (help_block ? @template.capture(&help_block) : "") +
            "</div>" +
          "</div>"
        ).html_safe
      end
    end  

    multipart_helpers.each do |name|
      define_method(name) do |field, *args, &help_block|
        options = args.last.is_a?(Hash) ? args.last : {}
        object = @template.instance_variable_get("@#{@object_name}")

        labelTag = get_label(field, options)

        options[:class] = 'inline ' + options[:class] if options[:class]

        errorText = get_error_text(object, field, options)

        wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
        errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end 
        ("<div class='#{wrapperClass}'>" +
            labelTag + 
            "<div class='controls'>" + 
                super(field, *args) +
                errorSpan +
                (help_block ? @template.capture(&help_block) : "") +
            "</div>" +
          "</div>"
        ).html_safe
      end
    end

    trailing_label_helpers.each do |name|
      # First alias old method
      class_eval("alias super_#{name.to_s} #{name}")

      define_method(name) do |field, *args, &help_block|
        options = args.first.is_a?(Hash) ? args.first : {}
        object = @template.instance_variable_get("@#{@object_name}")

        label_text = options[:label_text] || labelOptions[:text] || ""
        options.delete(:label)
        options.delete(:label_text)

        errorText = get_error_text(object, field, options)

        wrapperClass = 'control-group' + (errorText.empty? ? '' : ' error')
        errorSpan = if errorText.empty? then "" else "<span class='help-inline'>#{errorText}</span>" end 
        description = if options[:description] then %{<label class="control-label">#{options[:description]}</label>} else "" end
        options.delete(:description)
        ("<div class='#{wrapperClass}'>" +
          description +
          "<div class='controls'>" + 
            "<label for='#{field}' class='control-group'>" + 
              super(field, *args) +
              errorSpan +
              (help_block ? @template.capture(&help_block) : "") +
              label_text +
            "</label>" +
            "</div>" +
          "</div>"
        ).html_safe
      end
    end
  end
end

@allard - I'm trying out your version, but I'm getting this error on a check_box, where it's complaining about the labelOptions variable.

undefined local variable or method `labelOptions' for #<BootstrapFormBuilder::FormBuilder:0x007fb6a32c9fa0>

It's coming from this line in your code:

label_text = options[:label_text] || labelOptions[:text] || ""

I don't think your code is properly handling the creation of the labelOptions variable. I see no reference to its creation in your code.

In my usage, I am calling it like this in my erb file:

<%= f.check_box :remember_me %>
Owner
allard commented May 13, 2012

Spot on @mattslay. I just use :label_text when tested with this check box and forgot to include the line to grab the labelOptions. And from request by @jamiepenney I've now forked the gist with my updated version with this thing fixed.

I've tried @allard's version, and as far as placing the label inline with the checkbox, it does that just fine. (Although the text is a little too close to the checkbox for my liking, but I'm sure that's a CSS issue)

See this screenshot which compares the original version by @jamiepenny compared to the inline version by @allard:

http://www.screencast.com/t/jOn92rvMFsq

allard commented May 14, 2012

While it could technically be possible to insert an additional space or so to get the padding between the element and the label, I think it's definitely a css issue. I use:

input.checkbox {
  margin: 0 0 2px 0;
}

Also, don't forget that you can also use :description => 'something' if you want to add a left hand label as well with my version.

allard commented Jun 5, 2012

I'm pretty sure my previous css override isn't needed any more. There was some class that was missing or something that's been fixed in the latest gist. I've also added inline help messages to my gist for those interested so you can do:

<%= f.text_field :email, :help => "Please enter a valid email address" %>

and the help message will show up to the right of the input.

Is there any reason we couldn't go ahead and set the id attribute on the submit button to 'submit' ? I like having an id attribute set as for each input control, even the Submit button. It is helpful in jquery and general css, plus it's helpful in Capybara tests. Is there a potential conflict of having 2 or more submit buttons on a form? If so, any ideas on how to create unique id's? I know I can pass in an id as a hash option to f.submit(), but 99% of the time I would call it "submit", so I thought it could be done at this lower level?

def submit(value, options = {}, *args)
  super(value, {:class => "btn btn-primary", :id => "submit"}.merge(options), *args)
end
allard commented Jun 13, 2012
Owner

Hey again Jamie. Been around the block a few times now and still keep coming back to here :)
Quick question on what you reckon it would take to implement something like negative-captcha with your form builder.? I'm getting better but something like this would / could still be outta my league.
Would be nice if you're builder had it's own captcha rolled into it (prolly a negative or honeypot type so no images or files required, just hidden fields with clever keys for names kinda thing) :).

Cheers again for the work.

Owner

Probably outside of the scope of this code sorry. It would require changes on the controller side, which isn't something I want to add.

You could probably hack a hidden field captcha into it on your own if you want though. Something like this:

module_eval do
    define_method "bootstrap_captcha_form_for" do |record, *args, &block|

      new_block = lambda { |f| 
          f.hidden ("put your hidden field options here") 
          block.call f
      }
      bootstrap_form_for record, *args, &block
    end
end

This was all composed in browser, so I don't even know if it will run. Will give you something to start with at least.

I need some help getting this to work with the collection_select() helper:

collection_select(:contact, :church_office_id, ChurchOffice.all(), :id, :name)

Heres a link to a screen shot of the generated form using the regular Rails helper:

http://www.screencast.com/t/ZJdQJcUkwC

But when I added 'collection_select' to the list of basic_helpers, (so it will wrap everything in Bootstrap markup), it gives this error at run time:

undefined method 'merge' for :name:Symbol

I guess there needs to be more code to handle all those extra parameters that collection_select() uses. I looked at the code, but I simply do not know enough about Rails/Ruby at this time to see what is needed to handle this.

Here is a screenshot of the error page:
http://www.screencast.com/t/zf8oUXsg4iz

Ideally, just like we can do with checkboxes, I'd love to be able to pass in a caption for the label that would be generated right above the selection list box.

allard commented Jul 28, 2012

Jamie - I've fork allards version, and added support for f.collection_select(). I also refactored the bootstrap html wrapping out to some new handler methods, because it was beginning to have some redundant code now that more controls are being supported.

See my fork here: https://gist.github.com/3206827

Can somone explain what this alias stuff does? I don't see that it does anything? I commented it out and everything still seems to run fine.

  # First alias old method
  class_eval("alias super_#{name.to_s} #{name}")
Owner
Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment