# 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 |
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:
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.
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
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.
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.
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}")
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.