Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
42d2c5a
Test case for #417.
lcreid Feb 3, 2018
d7ef709
Undoing mess created on wrong branch.
lcreid Feb 3, 2018
187aa69
Remove test that accidentally leaked in from another branch.
lcreid Feb 4, 2018
b25ab77
Working but needs cleanup #417.
lcreid Feb 4, 2018
463486e
Partially cleaned up.
lcreid Feb 4, 2018
9e66706
Remove unneeded method and parameters.
lcreid Feb 4, 2018
e83bf64
Test selects error.
lcreid Feb 5, 2018
a2b4586
Test selects error.
lcreid Feb 5, 2018
e48451d
time_zone_select works for new error placement.
lcreid Feb 5, 2018
b2d832a
Add tests for date, time, datetime selects.
lcreid Feb 5, 2018
073a1e3
Add tests for date, time, datetime selects.
lcreid Feb 5, 2018
844e133
date, time, datetime tests pass with old implementation.
lcreid Feb 5, 2018
ebab68a
Merge branch 'new-builder' into 417-input-group
lcreid Feb 5, 2018
0a73344
Date, time, and datetime for new implementation.
lcreid Feb 5, 2018
365dbf2
Tests for file and collection selects.
lcreid Feb 5, 2018
5cc7869
Merge branch 'error-tests' into 417-input-group
lcreid Feb 5, 2018
a15d06d
File and collection selects with new implementation.
lcreid Feb 5, 2018
d12e146
Merge branch 'master' into 417-input-group
lcreid Feb 15, 2018
3983dbb
Merge branch 'master' into 417-input-group
lcreid Feb 18, 2018
4d7248c
Keep tests but revert lib to try another approach.
lcreid Feb 18, 2018
7c53f24
form_group tests passing.
lcreid Feb 18, 2018
24def25
Clean up and add back commented test.
lcreid Feb 18, 2018
f60ed6c
checkbox and radio button tests still have old behaviour.
lcreid Feb 18, 2018
c0aaf19
Clean up and block append and prepend.
lcreid Feb 19, 2018
ab7532a
Tests for #418.
lcreid Feb 19, 2018
bc435b4
Extract method wrapped_radio from radio_button.
lcreid Feb 19, 2018
8b83237
Extract method unwrapped_radio.
lcreid Feb 19, 2018
85fd80d
Finish extract method unwrapped_radio.
lcreid Feb 19, 2018
6e7170b
Merge branch 'refactor-419' into 419-check-radio-errors-b
lcreid Feb 19, 2018
f334e77
radio errors in right place.
lcreid Feb 19, 2018
ee8f4ba
Radio errors and help done.
lcreid Feb 19, 2018
2e43e1b
Extract methods wrapped and unwrapped check box.
lcreid Feb 20, 2018
84798e0
Merge branch 'refactor-419' into 419-check-radio-errors-b
lcreid Feb 20, 2018
7734cc3
Check box errors and help done.
lcreid Feb 20, 2018
eda293f
Move code to make it easier to see diffs.
lcreid Feb 20, 2018
5ab63b5
Add comments.
lcreid Feb 20, 2018
63c0933
Improve testing and fix defect.
lcreid Feb 21, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 157 additions & 73 deletions lib/bootstrap_form/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ def initialize(object_name, object, template, options)

define_method(with_method_name) do |name, options = {}|
form_group_builder(name, options) do
prepend_and_append_input(options) do
send(without_method_name, name, options)
end
send(without_method_name, name, options)
end
end

Expand All @@ -52,6 +50,7 @@ def initialize(object_name, object, template, options)
without_method_name = "#{method_name}_without_bootstrap"

define_method(with_method_name) do |name, options = {}, html_options = {}|
prevent_prepend_and_append!(options)
form_group_builder(name, options, html_options) do
content_tag(:div, send(without_method_name, name, options, html_options), class: control_specific_class(method_name))
end
Expand All @@ -61,6 +60,7 @@ def initialize(object_name, object, template, options)
end

def file_field_with_bootstrap(name, options = {})
prevent_prepend_and_append!(options)
options = options.reverse_merge(control_class: 'form-control-file')
form_group_builder(name, options) do
file_field_without_bootstrap(name, options)
Expand All @@ -71,15 +71,14 @@ def file_field_with_bootstrap(name, options = {})

def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block)
form_group_builder(method, options, html_options) do
prepend_and_append_input(options) do
select_without_bootstrap(method, choices, options, html_options, &block)
end
select_without_bootstrap(method, choices, options, html_options, &block)
end
end

bootstrap_method_alias :select

def collection_select_with_bootstrap(method, collection, value_method, text_method, options = {}, html_options = {})
prevent_prepend_and_append!(options)
form_group_builder(method, options, html_options) do
collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
end
Expand All @@ -88,6 +87,7 @@ def collection_select_with_bootstrap(method, collection, value_method, text_meth
bootstrap_method_alias :collection_select

def grouped_collection_select_with_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
prevent_prepend_and_append!(options)
form_group_builder(method, options, html_options) do
grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
end
Expand All @@ -96,6 +96,7 @@ def grouped_collection_select_with_bootstrap(method, collection, group_method, g
bootstrap_method_alias :grouped_collection_select

def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, html_options = {})
prevent_prepend_and_append!(options)
form_group_builder(method, options, html_options) do
time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
end
Expand All @@ -104,7 +105,32 @@ def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {},
bootstrap_method_alias :time_zone_select

def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
prevent_prepend_and_append!(options)
options = options.symbolize_keys!

wrapped_check_box(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do
unwrapped_check_box(name, options, checked_value, unchecked_value, &block)
end
end

bootstrap_method_alias :check_box

private
def wrapped_check_box(custom: false, disabled: false, inline: false)
if custom
wrapper_classes = ["custom-control", "custom-checkbox"]
wrapper_classes.append("custom-control-inline") if inline
else
wrapper_classes = ["form-check"]
wrapper_classes << "form-check-inline" if inline
end
wrapper_class = wrapper_classes.compact.join(" ")
content_tag(:div, class: wrapper_class) do
yield
end
end

def unwrapped_check_box(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
check_box_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
check_box_classes = [check_box_options[:class]]
check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
Expand All @@ -126,45 +152,56 @@ def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_
# https://github.com/rails/rails/blob/c57e7239a8b82957bcb07534cb7c1a3dcef71864/actionview/lib/action_view/helpers/tags/base.rb#L116-L118
if options[:multiple]
label_name =
"#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}"
"#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}"
end

label_classes = [options[:label_class]]
label_classes << hide_class if options[:hide_label]

if options[:custom]
div_class = ["custom-control", "custom-checkbox"]
div_class.append("custom-control-inline") if options[:inline]
if options[:skip_label]
checkbox_html
elsif options[:custom]
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
content_tag(:div, class: div_class.compact.join(" ")) do
if options[:skip_label]
checkbox_html
else
# TODO: Notice we don't seem to pass the ID into the custom control.
checkbox_html.concat(label(label_name, label_description, class: label_class))
end
end
# TODO: Notice we don't seem to pass the ID into the custom control.
checkbox_html.concat(label(label_name, label_description, class: label_class))
else
wrapper_class = "form-check"
wrapper_class += " form-check-inline" if options[:inline]
label_class = label_classes.prepend("form-check-label").compact.join(" ")
content_tag(:div, class: wrapper_class) do
if options[:skip_label]
checkbox_html
else
checkbox_html
.concat(label(label_name,
label_description,
{ class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
end
end
checkbox_html
.concat(label(label_name,
label_description,
{ class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
end
end

bootstrap_method_alias :check_box
public

def radio_button_with_bootstrap(name, value, *args)
prevent_prepend_and_append!(options)
options = args.extract_options!.symbolize_keys!

wrapped_radio(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do
unwrapped_radio(name, value, options, *args)
end
end

bootstrap_method_alias :radio_button

private
def wrapped_radio(custom: false, disabled: false, inline: false)
if custom
wrapper_classes = ["custom-control", "custom-radio"]
wrapper_classes.append("custom-control-inline") if inline
else
wrapper_classes = ["form-check"]
wrapper_classes.append("form-check-inline") if inline
wrapper_classes.append("disabled") if disabled
end
wrapper_class = wrapper_classes.compact.join(" ")
content_tag(:div, class: wrapper_class) do
yield
end
end

def unwrapped_radio(name, value, options, *args)
radio_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
radio_classes = [options[:class]]
radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
Expand All @@ -176,52 +213,68 @@ def radio_button_with_bootstrap(name, value, *args)
args << radio_options
radio_html = radio_button_without_bootstrap(name, value, *args)

disabled_class = " disabled" if options[:disabled]
label_classes = [options[:label_class]]
label_classes = [options[:label_class]]
label_classes << hide_class if options[:hide_label]

if options[:custom]
div_class = ["custom-control", "custom-radio"]
div_class.append("custom-control-inline") if options[:inline]
if options[:skip_label]
radio_html
elsif options[:custom]
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
content_tag(:div, class: div_class.compact.join(" ")) do
if options[:skip_label]
radio_html
else
# TODO: Notice we don't seem to pass the ID into the custom control.
radio_html.concat(label(name, options[:label], value: value, class: label_class))
end
end
# TODO: Notice we don't seem to pass the ID into the custom control.
radio_html.concat(label(name, options[:label], value: value, class: label_class))
else
wrapper_class = "form-check"
wrapper_class += " form-check-inline" if options[:inline]
label_class = label_classes.prepend("form-check-label").compact.join(" ")
content_tag(:div, class: "#{wrapper_class}#{disabled_class}") do
if options[:skip_label]
radio_html
else
radio_html
.concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
end
end
radio_html
.concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
end
end

bootstrap_method_alias :radio_button

def collection_check_boxes_with_bootstrap(*args)
html = inputs_collection(*args) do |name, value, options|
options[:multiple] = true
check_box(name, options, value, nil)
public

def collection_check_boxes_with_bootstrap(outer_name, collection, outer_value, text, outer_options = {})
prevent_prepend_and_append!(outer_options)
# This next line is because the options get munged in the legacy code.
help = outer_options[:help]
# Use begin..ensure so block returns the html and resets the in...collection flag.
begin
# The following is an ugly way to prevent `form_group` from outputting
# the error on the wrapper, when Bootstrap 4 wants it to be in the last
# element.
self.in_radio_checkbox_collection = true
html = inputs_collection(outer_name, collection, outer_value, text, outer_options) do |name, value, options, i|
options[:multiple] = true
wrapped_check_box(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do
check_box_html = unwrapped_check_box(name, options, value, nil)
check_box_html.concat(generate_help(name, help)) if i == collection.size - 1
check_box_html
end
end
ensure
self.in_radio_checkbox_collection = false
end
hidden_field(args.first,{value: "", multiple: true}).concat(html)
hidden_field(outer_name, value: "", multiple: true).concat(html)
end

bootstrap_method_alias :collection_check_boxes

def collection_radio_buttons_with_bootstrap(*args)
inputs_collection(*args) do |name, value, options|
radio_button(name, value, options)
def collection_radio_buttons_with_bootstrap(outer_name, collection, outer_value, text, outer_options = {})
prevent_prepend_and_append!(outer_options)
# This next line is because the options get munged in the legacy code.
help = outer_options[:help]
# Use begin..ensure so block returns the html and resets the in...collection flag.
begin
# The following is an ugly way to prevent `form_group` from outputting
# the error on the wrapper, when Bootstrap 4 wants it to be in the last
# element.
self.in_radio_checkbox_collection = true
inputs_collection(outer_name, collection, outer_value, text, outer_options) do |name, value, options, i|
wrapped_radio(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do
radio_html = unwrapped_radio(name, value, options)
radio_html.concat(generate_help(name, help)) if i == collection.size - 1
radio_html
end
end
ensure
self.in_radio_checkbox_collection = false
end
end

Expand All @@ -235,10 +288,9 @@ def form_group(*args, &block)
options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal
options[:class] << " #{feedback_class}" if options[:icon]

content_tag(:div, options.except(:id, :label, :help, :icon, :label_col, :control_col, :layout)) do
content_tag(:div, options.except(:append, :id, :label, :help, :icon, :input_group_class, :label_col, :control_col, :layout, :prepend)) do
label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label]
control = capture(&block).to_s
control.concat(generate_help(name, options[:help]).to_s)
control = prepend_and_append_input(name, options, &block).to_s

if get_group_layout(options[:layout]) == :horizontal
control_class = options[:control_col] || control_col
Expand Down Expand Up @@ -274,8 +326,28 @@ def fields_for_with_bootstrap(record_name, record_object = nil, fields_options =

private

def horizontal?
layout == :horizontal
def layout_default?(field_layout = nil)
[:default, nil].include? layout_in_effect(field_layout)
end

def layout_horizontal?(field_layout = nil)
layout_in_effect(field_layout) == :horizontal
end

def layout_inline?(field_layout = nil)
layout_in_effect(field_layout) == :inline
end

def field_inline_override?(field_layout = nil)
field_layout == :inline && layout != :inline
end

# true and false should only come from check_box and radio_button,
# and those don't have a :horizontal layout
def layout_in_effect(field_layout)
field_layout = :inline if field_layout == true
field_layout = :default if field_layout == false
field_layout || layout
end

def get_group_layout(group_layout)
Expand All @@ -294,6 +366,14 @@ def default_control_col
"col-sm-10"
end

def in_radio_checkbox_collection?
@in_radio_checkbox_collection ||= false
end

def in_radio_checkbox_collection=(state)
@in_radio_checkbox_collection = state
end

def hide_class
"sr-only" # still accessible for screen readers
end
Expand Down Expand Up @@ -366,6 +446,10 @@ def form_group_builder(method, options, html_options = nil)
class: wrapper_class
}

form_group_options[:append] = options.delete(:append) if options[:append]
form_group_options[:prepend] = options.delete(:prepend) if options[:prepend]
form_group_options[:input_group_class] = options.delete(:input_group_class) if options[:input_group_class]

if wrapper_options.is_a?(Hash)
form_group_options.merge!(wrapper_options)
end
Expand Down Expand Up @@ -455,7 +539,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block)
form_group_builder(name, options) do
inputs = ""

collection.each do |obj|
collection.each_with_index do |obj, i|
input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text))

input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
Expand All @@ -467,7 +551,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block)
end

input_options.delete(:class)
inputs << block.call(name, input_value, input_options)
inputs << block.call(name, input_value, input_options, i)
end

inputs.html_safe
Expand Down
14 changes: 12 additions & 2 deletions lib/bootstrap_form/helpers/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,28 @@ def custom_control(*args, &block)
form_group_builder(name, options, &block)
end

def prepend_and_append_input(options, &block)
def prepend_and_append_input(name, options, &block)
help = options[:help]
options = options.extract!(:prepend, :append, :input_group_class)
input_group_class = ["input-group", options[:input_group_class]].compact.join(' ')

input = capture(&block)
input = capture(&block) || "".html_safe

input = content_tag(:div, input_group_content(options[:prepend]), class: 'input-group-prepend') + input if options[:prepend]
input << content_tag(:div, input_group_content(options[:append]), class: 'input-group-append') if options[:append]
input << generate_help(name, help).to_s unless in_radio_checkbox_collection?
input = content_tag(:div, input, class: input_group_class) unless options.empty?
input
end

# Some helpers don't currently accept prepend and append. However, it's not
# clear if that's corrent. In the meantime, strip to options before calling
# methods that don't accept prepend and append.
def prevent_prepend_and_append!(options)
options.delete(:append)
options.delete(:prepend)
end

def input_group_content(content)
return content if content.match(/btn/)
content_tag(:span, content, class: 'input-group-text')
Expand Down
Loading