Skip to content

Instantly share code, notes, and snippets.

@jamiepenney
Created February 14, 2012 19:57
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save jamiepenney/1829710 to your computer and use it in GitHub Desktop.
Save jamiepenney/1829710 to your computer and use it in GitHub Desktop.
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
@mattslay
Copy link

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
Copy link

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
Copy link

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.

@mattslay
Copy link

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
Copy link

allard commented Jun 13, 2012 via email

@jamiepenney
Copy link
Author

jamiepenney commented Jun 13, 2012 via email

@drew1two
Copy link

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.

@jamiepenney
Copy link
Author

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.

@drew1two
Copy link

drew1two commented Jun 18, 2012 via email

@mattslay
Copy link

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
Copy link

allard commented Jul 28, 2012 via email

@mattslay
Copy link

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

@mattslay
Copy link

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}")

@jamiepenney
Copy link
Author

jamiepenney commented Jul 30, 2012 via email

@jamiepenney
Copy link
Author

jamiepenney commented Jul 30, 2012 via email

@mattslay
Copy link

mattslay commented Jul 30, 2012 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment