diff --git a/CHANGELOG.md b/CHANGELOG.md index 708f1d1cf..a2b6f7a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ In addition to these necessary markup changes, the bootstrap_form API itself has ### New features +* Support for Rails 5.1 `form_with` - [@lcreid](https://github.com/lcreid). * Support Bootstrap v4's [Custom Checkboxes and Radios](https://getbootstrap.com/docs/4.0/components/forms/#checkboxes-and-radios-1) with a new `custom: true` option * Allow HTML in help translations by using the `_html` suffix on the key - [@unikitty37](https://github.com/unikitty37) * Your contribution here! diff --git a/README.md b/README.md index 29a78c44e..b95812456 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Bootstrap v4-style forms into your Rails application. ## Requirements * Ruby 2.2.2+ -* Rails 5.0+ +* Rails 5.0+ (Rails 5.1+ for `bootstrap_form_with`) * Bootstrap 4.0.0+ ## Installation @@ -84,6 +84,53 @@ If your form is not backed by a model, use the `bootstrap_form_tag`. Usage of th <% end %> ``` +### `bootstrap_form_with` (Rails 5.1+) + +Note that `form_with` in Rails 5.1 does not add IDs to form elements and labels by default, which are both important to Bootstrap markup. This behavior is corrected in Rails 5.2. + +To get started, just use the `bootstrap_form_with` helper in place of `form_with`. Here's an example: + +```erb +<%= bootstrap_form_with(model: @user, local: true) do |f| %> + <%= f.email_field :email %> + <%= f.password_field :password %> + <%= f.check_box :remember_me %> + <%= f.submit "Log In" %> +<% end %> +``` + +This generates: + +```html +
+ +
+ + +
+
+ + + A good password should be at least six characters long +
+
+ +
+ +
+``` + +`bootstrap_form_with` supports both the `model:` and `url:` use cases +in `form_with`. + +`form_with` has some important differences compared to `form_for` and `form_tag`, and these differences apply to `bootstrap_form_with`. A good summary of the differences can be found at: https://m.patrikonrails.com/rails-5-1s-form-with-vs-old-form-helpers-3a5f72a8c78a, or in the [Rails documentation](api.rubyonrails.org). + +### Future Compatibility + +The Rails team has [suggested](https://github.com/rails/rails/issues/25197) that `form_for` and `form_tag` may be deprecated and then removed in future versions of Rails. `bootstrap_form` will continue to support `bootstrap_form_for` and `bootstrap_form_tag` as long as Rails supports `form_for` and `form_tag`. + ## Form Helpers This gem wraps the following Rails form helpers: diff --git a/lib/bootstrap_form/form_builder.rb b/lib/bootstrap_form/form_builder.rb index d0e9fcb6f..2b22da707 100644 --- a/lib/bootstrap_form/form_builder.rb +++ b/lib/bootstrap_form/form_builder.rb @@ -235,7 +235,10 @@ def form_group(*args, &block) end def fields_for_with_bootstrap(record_name, record_object = nil, fields_options = {}, &block) - fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options? + if record_object.is_a?(Hash) && record_object.extractable_options? + fields_options = record_object + record_object = nil + end fields_options[:layout] ||= options[:layout] fields_options[:label_col] = fields_options[:label_col].present? ? "#{fields_options[:label_col]}" : options[:label_col] fields_options[:control_col] ||= options[:control_col] @@ -246,6 +249,10 @@ def fields_for_with_bootstrap(record_name, record_object = nil, fields_options = bootstrap_method_alias :fields_for + # the Rails `fields` method passes its options + # to the builder, so there is no need to write a `bootstrap_form` helper + # for the `fields` method. + private def horizontal? @@ -357,11 +364,11 @@ def form_group_builder(method, options, html_options = nil) label_text ||= options.delete(:label) end - form_group_options.merge!(label: { + form_group_options[:label] = { text: label_text, class: label_class, skip_required: options.delete(:skip_required) - }) + } end form_group(method, form_group_options) do @@ -370,12 +377,18 @@ def form_group_builder(method, options, html_options = nil) end def convert_form_tag_options(method, options = {}) - options[:name] ||= method - options[:id] ||= method + unless @options[:skip_default_ids] + options[:name] ||= method + options[:id] ||= method + end options end def generate_label(id, name, options, custom_label_col, group_layout) + # id is the caller's options[:id] at the only place this method is called. + # The options argument is a small subset of the options that might have + # been passed to generate_label's caller, and definitely doesn't include + # :id. options[:for] = id if acts_like_form_tag classes = [options[:class]] @@ -398,7 +411,6 @@ def generate_label(id, name, options, custom_label_col, group_layout) else label(name, options[:text], options.except(:text)) end - end def generate_help(name, help_text) @@ -467,6 +479,5 @@ def get_help_text_by_i18n_key(name) help_text end end - end end diff --git a/lib/bootstrap_form/helper.rb b/lib/bootstrap_form/helper.rb index 3caadb94b..15ed2502d 100644 --- a/lib/bootstrap_form/helper.rb +++ b/lib/bootstrap_form/helper.rb @@ -4,12 +4,7 @@ module Helper def bootstrap_form_for(object, options = {}, &block) options.reverse_merge!({builder: BootstrapForm::FormBuilder}) - options[:html] ||= {} - options[:html][:role] ||= 'form' - - if options[:layout] == :inline - options[:html][:class] = [options[:html][:class], "form-inline"].compact.join(" ") - end + options = process_options(options) temporarily_disable_field_error_proc do form_for(object, options, &block) @@ -22,6 +17,31 @@ def bootstrap_form_tag(options = {}, &block) bootstrap_form_for("", options, &block) end + def bootstrap_form_with(options = {}, &block) + options.reverse_merge!(builder: BootstrapForm::FormBuilder) + + options = process_options(options) + + temporarily_disable_field_error_proc do + form_with(options, &block) + end + end + + private + + def process_options(options) + options[:html] ||= {} + options[:html][:role] ||= 'form' + + if options[:layout] == :inline + options[:html][:class] = [options[:html][:class], 'form-inline'].compact.join(' ') + end + + options + end + + public + def temporarily_disable_field_error_proc original_proc = ActionView::Base.field_error_proc ActionView::Base.field_error_proc = proc { |input, instance| input } diff --git a/test/bootstrap_fields_test.rb b/test/bootstrap_fields_test.rb index d241a27f0..f143b15e4 100644 --- a/test/bootstrap_fields_test.rb +++ b/test/bootstrap_fields_test.rb @@ -142,6 +142,30 @@ class BootstrapFieldsTest < ActionView::TestCase assert_equivalent_xml expected, @builder.text_area(:comments) end + if ::Rails::VERSION::STRING > '5.1' && ::Rails::VERSION::STRING < '5.2' + test "text areas are wrapped correctly form_with Rails 5.1" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, form_with_builder.text_area(:comments) + end + end + + if ::Rails::VERSION::STRING > '5.2' + test "text areas are wrapped correctly form_with Rails 5.2+" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, form_with_builder.text_area(:comments) + end + end + test "text fields are wrapped correctly" do expected = <<-HTML.strip_heredoc
@@ -250,6 +274,7 @@ class BootstrapFieldsTest < ActionView::TestCase test "fields_for correctly passes inline style from parent builder" do @user.address = Address.new(street: '123 Main Street') + # NOTE: This test works with even if you use `fields_for_without_bootstrap` output = bootstrap_form_for(@user, layout: :inline) do |f| f.fields_for :address do |af| af.text_field(:street) diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 17d0341df..8faf54bef 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -14,6 +14,19 @@ class BootstrapFormTest < ActionView::TestCase assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| nil } end + if ::Rails::VERSION::STRING >= '5.1' + # No need to test 5.2 separately for this case, since 5.2 does *not* + # generate a default ID for the form element. + test "default-style forms bootstrap_form_with Rails 5.1+" do + expected = <<-HTML.strip_heredoc +
+ +
+ HTML + assert_equivalent_xml expected, bootstrap_form_with(model: @user) { |f| nil } + end + end + test "inline-style forms" do expected = <<-HTML.strip_heredoc
diff --git a/test/test_helper.rb b/test/test_helper.rb index 754416bb7..c52b3bcfc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -36,6 +36,13 @@ def setup_test_fixture }) end + # Originally only used in one test file but placed here in case it's needed in others in the future. + def form_with_builder + builder = nil + bootstrap_form_with(model: @user) { |f| builder = f } + builder + end + def sort_attributes doc doc.dup.traverse do |node| if node.is_a?(Nokogiri::XML::Element) @@ -86,5 +93,4 @@ def assert_equivalent_xml(expected, actual) ).to_s(:color) } end - end