bootstrap_form 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer.json +38 -0
  3. data/.github/workflows/ruby.yml +19 -7
  4. data/.gitignore +21 -7
  5. data/.rubocop.yml +25 -15
  6. data/CHANGELOG.md +3 -0
  7. data/CONTRIBUTING.md +53 -111
  8. data/DOCKER.md +95 -0
  9. data/Gemfile +7 -1
  10. data/README.md +166 -100
  11. data/RELEASING.md +4 -10
  12. data/Rakefile +34 -16
  13. data/bootstrap_form.gemspec +1 -1
  14. data/compose.yml +36 -0
  15. data/gemfiles/7.2.gemfile +10 -0
  16. data/gemfiles/8.0.gemfile +10 -0
  17. data/gemfiles/8.1.gemfile +9 -0
  18. data/gemfiles/common.gemfile +10 -9
  19. data/gemfiles/edge.gemfile +5 -1
  20. data/lib/bootstrap_form/action_view_extensions/form_helper.rb +6 -6
  21. data/lib/bootstrap_form/components/labels.rb +17 -13
  22. data/lib/bootstrap_form/components/validation.rb +6 -3
  23. data/lib/bootstrap_form/configuration.rb +17 -2
  24. data/lib/bootstrap_form/engine.rb +12 -0
  25. data/lib/bootstrap_form/form_builder.rb +14 -6
  26. data/lib/bootstrap_form/form_group.rb +4 -4
  27. data/lib/bootstrap_form/form_group_builder.rb +17 -8
  28. data/lib/bootstrap_form/helpers/bootstrap.rb +15 -9
  29. data/lib/bootstrap_form/helpers/field.rb +2 -0
  30. data/lib/bootstrap_form/inputs/base.rb +11 -6
  31. data/lib/bootstrap_form/inputs/check_box.rb +13 -6
  32. data/lib/bootstrap_form/inputs/collection_check_boxes.rb +3 -8
  33. data/lib/bootstrap_form/inputs/collection_radio_buttons.rb +1 -0
  34. data/lib/bootstrap_form/inputs/file_field.rb +1 -1
  35. data/lib/bootstrap_form/inputs/inputs_collection.rb +67 -11
  36. data/lib/bootstrap_form/inputs/radio_button.rb +2 -1
  37. data/lib/bootstrap_form/inputs/range_field.rb +1 -1
  38. data/lib/bootstrap_form/inputs/rich_text_area.rb +2 -0
  39. data/lib/bootstrap_form/inputs/submit.rb +3 -1
  40. data/lib/bootstrap_form/inputs/text_area.rb +2 -0
  41. data/lib/bootstrap_form/inputs/time_zone_select.rb +1 -1
  42. data/lib/bootstrap_form/version.rb +4 -2
  43. data/lib/bootstrap_form.rb +4 -15
  44. metadata +17 -18
  45. data/.yarnrc +0 -5
  46. data/Dockerfile +0 -22
  47. data/docker-compose-system-test.yml +0 -45
  48. data/docker-compose.yml +0 -29
  49. data/gemfiles/6.1.gemfile +0 -4
  50. data/gemfiles/7.0.gemfile +0 -5
  51. data/gemfiles/7.1.gemfile +0 -5
data/RELEASING.md CHANGED
@@ -10,28 +10,22 @@ Follow these steps to release a new version of bootstrap_form to rubygems.org.
10
10
 
11
11
  ## How to release
12
12
 
13
+ In the `bootstrap_form` repository (not a fork):
14
+
13
15
  1. Determine which would be the correct next version number according to [semver](http://semver.org/).
14
16
  2. Update the version in `./lib/bootstrap_form/version.rb`.
15
17
  3. Make sure that you have all the gems necessary for testing and releasing.
16
18
 
17
- BUNDLE_GEMFILE=gemfiles/7.1.gemfile bundle update
19
+ BUNDLE_GEMFILE=gemfiles/7.2.gemfile bundle update
18
20
 
19
21
  4. **Ensure the tests are passing by running the tests**
20
22
 
21
23
  (There should be no errors or warnings.)
22
24
 
23
- BUNDLE_GEMFILE=gemfiles/7.1.gemfile bundle exec rake test
24
-
25
- 5. **Ensure the demo tests are passing by running**
26
-
27
- cd demo
28
- bundle update
29
25
  bundle exec rake test:all
30
- cd -
31
26
 
32
- (You will have failures unless you're running on a Mac configured so the screenshots will be identical. This is something that we're hoping to fix.)
27
+ You will have failures in the system tests unless you're running on Linux. Chrome on each operating system renders slightly differently.
33
28
 
34
- 6. Update the GitHub diff links at the beginning of `CHANGELOG.md` (The pattern should be obvious when you look at them).
35
29
  7. Update the installation instructions in `README.md` to use the new version.
36
30
  8. Commit the CHANGELOG and version changes in a single commit; the message should be "Preparing vX.Y.Z" where `X.Y.Z` is the version being released.
37
31
  9. Tag, push to GitHub, and publish to rubygems.org:
data/Rakefile CHANGED
@@ -1,30 +1,48 @@
1
1
  begin
2
- require 'bundler/setup'
3
-
4
- require 'bundler/gem_tasks'
2
+ require "bundler/setup"
3
+ require "bundler/gem_tasks"
5
4
  require "minitest/test_task"
6
- require 'rdoc/task'
7
- require 'rubocop/rake_task'
8
- rescue LoadError
9
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ require "rdoc/task"
6
+ require "rubocop/rake_task"
7
+ rescue LoadError => e
8
+ puts "You must run `bundle install` to run rake tasks (#{e.message})"
10
9
  end
11
10
 
12
11
  RDoc::Task.new(:rdoc) do |rdoc|
13
- rdoc.rdoc_dir = 'rdoc'
14
- rdoc.title = 'BootstrapForm'
15
- rdoc.options << '--line-numbers'
16
- rdoc.rdoc_files.include('README.md')
17
- rdoc.rdoc_files.include('lib/**/*.rb')
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "BootstrapForm"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
18
17
  end
19
18
 
20
19
  Minitest::TestTask.create(:test) do |t|
21
- t.libs << "test"
22
- t.libs << "lib"
23
- t.warning = false
20
+ t.warning = true
24
21
  t.test_globs = ["test/**/*_test.rb"]
25
22
  end
26
23
 
27
- desc 'Run RuboCop checks'
24
+ desc "Run RuboCop checks"
28
25
  RuboCop::RakeTask.new(:rubocop)
29
26
 
30
27
  task default: %i[test rubocop:autocorrect]
28
+
29
+ namespace :test do
30
+ desc "Run tests for all supported Rails versions, with current Ruby version"
31
+ task :all do
32
+ gemfiles.each do |gemfile|
33
+ system("BUNDLE_GEMFILE=#{gemfile} rake test")
34
+ end
35
+
36
+ Dir.chdir("demo")
37
+ system("BUNDLE_GEMFILE= rake test:all")
38
+ end
39
+ end
40
+
41
+ desc "Update gem .lock files e.g. for changed Ruby version"
42
+ task :update_gemfile_locks do
43
+ gemfiles.append("Gemfile").each do |gemfile|
44
+ system("BUNDLE_GEMFILE=#{gemfile} bundle update --bundler")
45
+ end
46
+ end
47
+
48
+ def gemfiles = Dir.glob("gemfiles/*.gemfile").reject { |f| File.basename(f) == "common.gemfile" }
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.bindir = "exe"
24
24
  s.require_paths = ["lib"]
25
25
 
26
- s.required_ruby_version = ">= 3.0"
26
+ s.required_ruby_version = ">= 3.2"
27
27
 
28
28
  s.add_dependency("actionpack", BootstrapForm::REQUIRED_RAILS_VERSION)
29
29
  s.add_dependency("activemodel", BootstrapForm::REQUIRED_RAILS_VERSION)
data/compose.yml ADDED
@@ -0,0 +1,36 @@
1
+ # Set up the Selenium container based on the Selenium official:
2
+ # https://github.com/SeleniumHQ/docker-selenium/blob/trunk/docker-compose-v3.yml
3
+ # And:
4
+ # https://medium.com/@retrorubies/chrome-as-a-service-for-rails-testing-b1a45e70fec1
5
+ services:
6
+ web:
7
+ image: lenchoreyes/jade:rails-app-${RUBY_VERSION:-3.3}-sqlite-${DISTRO:-bookworm}
8
+ stdin_open: true
9
+ tty: true
10
+ volumes:
11
+ - .:/app:cached
12
+ environment:
13
+ - HISTFILE=/app/.bash_history
14
+ - SELENIUM_HOST=selenium
15
+ - SELENIUM_PORT=4444
16
+ - TEST_APP_HOST=web
17
+ - TEST_APP_PORT=3001
18
+ ports:
19
+ - "3000"
20
+ - "3001"
21
+ command: /bin/bash
22
+
23
+ selenium:
24
+ image: selenium/standalone-chrome:133.0
25
+ shm_size: 2gb
26
+ logging:
27
+ driver: none
28
+ stdin_open: true
29
+ tty: true
30
+ environment:
31
+ - LANG=en_CA.UTF-8
32
+ - LANGUAGE=en_CA.UTF-8
33
+ - LANG_WHERE=CA
34
+ - LANG_WHICH=en
35
+ ports:
36
+ - "7900"
@@ -0,0 +1,10 @@
1
+ gems = "#{__dir__}/common.gemfile"
2
+ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
+
4
+ gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
+ gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "minitest", "~> 5.0"
7
+ gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
8
+ gem "rails", "~> 7.2.0"
9
+ gem "sprockets-rails", require: "sprockets/railtie"
10
+ gem "sqlite3"
@@ -0,0 +1,10 @@
1
+ gems = "#{__dir__}/common.gemfile"
2
+ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
+
4
+ gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
+ gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "minitest", "~> 5.0"
7
+ gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
8
+ gem "propshaft"
9
+ gem "rails", "~> 8.0.1"
10
+ gem "sqlite3"
@@ -0,0 +1,9 @@
1
+ gems = "#{__dir__}/common.gemfile"
2
+ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
+
4
+ gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
+ gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
7
+ gem "propshaft"
8
+ gem "rails", "~> 8.1.0"
9
+ gem "sqlite3"
@@ -4,23 +4,24 @@ gemspec path: File.dirname(__dir__)
4
4
 
5
5
  # To test with different Rails versions, use the files in `./gemfiles`
6
6
 
7
- group :development do
8
- gem "htmlbeautifier"
9
- gem "puma"
10
- gem "rubocop-performance", require: false
11
- gem "rubocop-rails", require: false
12
- end
13
-
14
7
  group :test do
8
+ gem "capybara-screenshot-diff", require: false
9
+ gem "chunky_png", "~> 1.4"
15
10
  gem "diffy"
16
11
  gem "equivalent-xml"
12
+ gem "minitest-mock"
17
13
  gem "mocha"
18
- gem "sqlite3"
14
+ gem "selenium-webdriver"
19
15
  end
20
16
 
21
17
  group :development, :test do
22
18
  gem "debug"
23
- gem "pry-byebug"
19
+ gem "htmlbeautifier"
20
+ gem "ostruct" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.5.0")
21
+ gem "puma"
22
+ gem "rubocop-performance", require: false
23
+ gem "rubocop-rails", require: false
24
+ gem "warning"
24
25
  end
25
26
 
26
27
  group :ci do
@@ -1,5 +1,9 @@
1
1
  gems = "#{__dir__}/common.gemfile"
2
2
  eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
+ gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
+ gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
7
+ gem "propshaft"
4
8
  gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
5
- gem "sprockets-rails", require: "sprockets/railtie"
9
+ gem "sqlite3"
@@ -33,20 +33,20 @@ module BootstrapForm
33
33
  end
34
34
  end
35
35
 
36
- def bootstrap_form_tag(options={}, &block)
36
+ def bootstrap_form_tag(options={}, &)
37
37
  options[:acts_like_form_tag] = true
38
38
 
39
- bootstrap_form_for("", options, &block)
39
+ bootstrap_form_for("", options, &)
40
40
  end
41
41
 
42
- def bootstrap_fields_for(record_name, record_object=nil, options={}, &block)
42
+ def bootstrap_fields_for(record_name, record_object=nil, options={}, &)
43
43
  options[:builder] = BootstrapForm::FormBuilder
44
- fields_for(record_name, record_object, options, &block)
44
+ fields_for(record_name, record_object, options, &)
45
45
  end
46
46
 
47
- def bootstrap_fields(scope=nil, model: nil, **options, &block)
47
+ def bootstrap_fields(scope=nil, model: nil, **options, &)
48
48
  options[:builder] = BootstrapForm::FormBuilder
49
- fields(scope, model: model, **options, &block)
49
+ fields(scope, model: model, **options, &)
50
50
  end
51
51
 
52
52
  private
@@ -10,16 +10,8 @@ module BootstrapForm
10
10
  def generate_label(id, name, options, custom_label_col, group_layout)
11
11
  return if options.blank?
12
12
 
13
- # id is the caller's options[:id] at the only place this method is called.
14
- # The options argument is a small subset of the options that might have
15
- # been passed to generate_label's caller, and definitely doesn't include
16
- # :id.
17
- options[:for] = id if acts_like_form_tag
18
-
19
- options[:class] = label_classes(name, options, custom_label_col, group_layout)
20
- options.delete(:class) if options[:class].none?
21
-
22
- label(name, label_text(name, options), options.except(:text))
13
+ prepare_label_options(id, name, options, custom_label_col, group_layout)
14
+ label(name, label_text(name, options[:text]), options.except(:text))
23
15
  end
24
16
 
25
17
  def label_classes(name, options, custom_label_col, group_layout)
@@ -34,7 +26,7 @@ module BootstrapForm
34
26
 
35
27
  def label_layout_classes(custom_label_col, group_layout)
36
28
  if layout_horizontal?(group_layout)
37
- ["col-form-label", (custom_label_col || label_col)]
29
+ ["col-form-label", custom_label_col || label_col]
38
30
  elsif layout_inline?(group_layout)
39
31
  %w[form-label me-sm-2]
40
32
  else
@@ -42,14 +34,26 @@ module BootstrapForm
42
34
  end
43
35
  end
44
36
 
45
- def label_text(name, options)
46
- label = options[:text] || object&.class&.try(:human_attribute_name, name)&.html_safe # rubocop:disable Rails/OutputSafety
37
+ def label_text(name, text)
38
+ label = text || object&.class&.try(:human_attribute_name, name)&.html_safe # rubocop:disable Rails/OutputSafety, Style/SafeNavigationChainLength
47
39
  if label_errors && error?(name)
48
40
  (" ".html_safe + get_error_messages(name)).prepend(label)
49
41
  else
50
42
  label
51
43
  end
52
44
  end
45
+
46
+ def prepare_label_options(id, name, options, custom_label_col, group_layout)
47
+ # id is the caller's options[:id] at the only place this method is called.
48
+ # The options argument is a small subset of the options that might have
49
+ # been passed to generate_label's caller, and definitely doesn't include
50
+ # :id.
51
+ options[:for] = id if acts_like_form_tag
52
+
53
+ options[:class] = label_classes(name, options, custom_label_col, group_layout)
54
+ options.delete(:class) if options[:class].none?
55
+ options[:id] = aria_feedback_id(name:, id:) if error?(name) && label_errors
56
+ end
53
57
  end
54
58
  end
55
59
  end
@@ -61,17 +61,16 @@ module BootstrapForm
61
61
  error?(name) && inline_errors
62
62
  end
63
63
 
64
- def generate_error(name)
64
+ def generate_error(name, id)
65
65
  return unless inline_error?(name)
66
66
 
67
67
  help_text = get_error_messages(name)
68
68
  help_klass = "invalid-feedback"
69
69
  help_tag = :div
70
70
 
71
- content_tag(help_tag, help_text, class: help_klass)
71
+ content_tag(help_tag, help_text, class: help_klass, id: aria_feedback_id(id:, name:))
72
72
  end
73
73
 
74
- # rubocop:disable Metrics/AbcSize
75
74
  def get_error_messages(name)
76
75
  object.class.try(:reflections)&.each do |association_name, a|
77
76
  next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
@@ -85,6 +84,10 @@ module BootstrapForm
85
84
  safe_join(object.errors[name], ", ")
86
85
  end
87
86
  # rubocop:enable Metrics/AbcSize
87
+
88
+ def aria_feedback_id(name:, id: nil)
89
+ id.present? ? "#{id}_feedback" : field_id(name, :feedback)
90
+ end
88
91
  end
89
92
  end
90
93
  end
@@ -7,16 +7,31 @@ module BootstrapForm
7
7
  when nil
8
8
  @default_form_attributes = {}
9
9
  when Hash
10
+ BootstrapForm.deprecator.warn(<<~MESSAGE.squish)
11
+ BootstrapForm::Configuration#default_form_attributes= will be removed in a future release.
12
+ Please use BootstrapForm.config.default_form_attributes= instead.
13
+ MESSAGE
10
14
  @default_form_attributes = attributes
15
+ BootstrapForm.config.default_form_attributes = attributes
11
16
  else
12
17
  raise ArgumentError, "Unsupported default_form_attributes #{attributes.inspect}"
13
18
  end
14
19
  end
15
20
 
16
21
  def default_form_attributes
17
- return @default_form_attributes if defined? @default_form_attributes
22
+ BootstrapForm.deprecator.warn(<<~MESSAGE.squish)
23
+ BootstrapForm::Configuration#default_form_attributes will be removed in a future release.
24
+ Please use BootstrapForm.config.default_form_attributes instead.
25
+ MESSAGE
26
+ BootstrapForm.config.default_form_attributes
27
+ end
28
+ end
29
+
30
+ mattr_accessor :config, default: ActiveSupport::OrderedOptions.new
18
31
 
19
- {}
32
+ class << self
33
+ def configure
34
+ yield(config) if block_given?
20
35
  end
21
36
  end
22
37
  end
@@ -6,5 +6,17 @@ module BootstrapForm
6
6
  class Engine < Rails::Engine
7
7
  config.eager_load_namespaces << BootstrapForm
8
8
  config.autoload_paths << File.expand_path("lib", __dir__)
9
+
10
+ config.bootstrap_form = BootstrapForm.config
11
+ config.bootstrap_form.default_form_attributes ||= {}
12
+ config.bootstrap_form.group_around_collections = Rails.env.development? if config.bootstrap_form.group_around_collections.nil?
13
+
14
+ initializer "bootstrap_form.configure" do |app|
15
+ BootstrapForm.config = app.config.bootstrap_form
16
+ end
17
+
18
+ initializer "bootstrap_form.deprecator" do |app|
19
+ app.deprecators[:bootstrap_form] = BootstrapForm.deprecator
20
+ end
9
21
  end
10
22
  end
@@ -1,10 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # require 'bootstrap_form/aliasing'
2
4
 
3
5
  module BootstrapForm
4
- class FormBuilder < ActionView::Helpers::FormBuilder
6
+ class FormBuilder < ActionView::Helpers::FormBuilder # rubocop:disable Metrics/ClassLength
5
7
  attr_reader :layout, :label_col, :control_col, :has_error, :inline_errors,
6
8
  :label_errors, :acts_like_form_tag
7
9
 
10
+ class << self
11
+ def redefine_rich_text_area?
12
+ ActionView::Helpers::FormBuilder.instance_methods.any? { _1 == :rich_text_area }
13
+ end
14
+ end
15
+
8
16
  include BootstrapForm::Helpers::Field
9
17
  include BootstrapForm::Helpers::Bootstrap
10
18
 
@@ -32,7 +40,7 @@ module BootstrapForm
32
40
  include BootstrapForm::Inputs::PhoneField
33
41
  include BootstrapForm::Inputs::RadioButton
34
42
  include BootstrapForm::Inputs::RangeField
35
- include BootstrapForm::Inputs::RichTextArea
43
+ include BootstrapForm::Inputs::RichTextArea if redefine_rich_text_area?
36
44
  include BootstrapForm::Inputs::SearchField
37
45
  include BootstrapForm::Inputs::Select
38
46
  include BootstrapForm::Inputs::Submit
@@ -72,10 +80,10 @@ module BootstrapForm
72
80
  .compact.uniq, " ")
73
81
  end
74
82
 
75
- def fields_for_with_bootstrap(record_name, record_object=nil, fields_options={}, &block)
83
+ def fields_for_with_bootstrap(record_name, record_object=nil, fields_options={}, &)
76
84
  fields_options = fields_for_options(record_object, fields_options)
77
85
  record_object = nil if record_object.is_a?(Hash) && record_object.extractable_options?
78
- fields_for_without_bootstrap(record_name, record_object, fields_options, &block)
86
+ fields_for_without_bootstrap(record_name, record_object, fields_options, &)
79
87
  end
80
88
 
81
89
  bootstrap_alias :fields_for
@@ -90,9 +98,9 @@ module BootstrapForm
90
98
  field_options = fields_options
91
99
  field_options = record_object if record_object.is_a?(Hash) && record_object.extractable_options?
92
100
  %i[layout control_col inline_errors label_errors].each do |option|
93
- field_options[option] ||= options[option]
101
+ field_options[option] = field_options.key?(option) ? field_options[option] : options[option]
94
102
  end
95
- field_options[:label_col] = field_options[:label_col].present? ? (field_options[:label_col]).to_s : options[:label_col]
103
+ field_options[:label_col] = field_options[:label_col].present? ? field_options[:label_col].to_s : options[:label_col]
96
104
  field_options
97
105
  end
98
106
 
@@ -23,21 +23,21 @@ module BootstrapForm
23
23
  html_class = control_specific_class(field_name)
24
24
  html_class = "#{html_class} col-auto g-3" if @layout == :horizontal && options[:skip_inline].blank?
25
25
  tag.div(class: html_class) do
26
- input_with_error(name) do
26
+ input_with_error(name, options[:id]) do
27
27
  send(without_field_name, name, options, html_options)
28
28
  end
29
29
  end
30
30
  end
31
31
 
32
- def form_group_content(label, help_text, options, &block) # rubocop:disable Metrics/AbcSize
32
+ def form_group_content(label, help_text, options, &)
33
33
  label ||= ActiveSupport::SafeBuffer.new
34
34
  if group_layout_horizontal?(options[:layout])
35
- label + tag.div(capture(&block) + help_text, class: form_group_control_class(options))
35
+ label + tag.div(capture(&) + help_text, class: form_group_control_class(options))
36
36
  else
37
37
  content = ActiveSupport::SafeBuffer.new
38
38
  # Floating labels need to be rendered after the field
39
39
  content << label unless options[:floating]
40
- content << capture(&block)
40
+ content << capture(&)
41
41
  content << label if options[:floating]
42
42
  content << help_text if help_text
43
43
  content
@@ -6,7 +6,17 @@ module BootstrapForm
6
6
 
7
7
  private
8
8
 
9
- def form_group_builder(method, options, html_options=nil, &block)
9
+ def form_group_builder(method, options, html_options=nil, &)
10
+ form_group_builder_wrapper(method, options, html_options) do |form_group_options, no_wrapper|
11
+ if no_wrapper
12
+ yield
13
+ else
14
+ form_group(method, form_group_options, &)
15
+ end
16
+ end
17
+ end
18
+
19
+ def form_group_builder_wrapper(method, options, html_options=nil)
10
20
  no_wrapper = options[:wrapper] == false
11
21
 
12
22
  options = form_group_builder_options(options, method)
@@ -18,11 +28,7 @@ module BootstrapForm
18
28
  :hide_label, :skip_required, :label_as_placeholder, :wrapper_class, :wrapper
19
29
  )
20
30
 
21
- if no_wrapper
22
- yield
23
- else
24
- form_group(method, form_group_options, &block)
25
- end
31
+ yield(form_group_options, no_wrapper)
26
32
  end
27
33
 
28
34
  def form_group_builder_options(options, method)
@@ -92,13 +98,16 @@ module BootstrapForm
92
98
  # Add control_class; allow it to be overridden by :control_class option
93
99
  control_classes = css_options.delete(:control_class) { control_class }
94
100
  css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ")
95
- css_options[:class] << " is-invalid" if error?(method)
101
+ if error?(method)
102
+ css_options[:class] << " is-invalid"
103
+ css_options[:aria] = { describedby: aria_feedback_id(id: options[:id], name: method) }
104
+ end
96
105
  css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder]
97
106
  css_options
98
107
  end
99
108
 
100
109
  def form_group_placeholder(options, method)
101
- form_group_label_text(options[:label]) || object.class.human_attribute_name(method)
110
+ form_group_label_text(options[:label]) || (object && object.class.human_attribute_name(method)) || method.to_s.humanize # rubocop:disable Style/SafeNavigation
102
111
  end
103
112
  end
104
113
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BootstrapForm
2
4
  module Helpers
3
5
  module Bootstrap
@@ -32,7 +34,10 @@ module BootstrapForm
32
34
  hide_attribute_name = options[:hide_attribute_name] || false
33
35
  custom_class = options[:custom_class] || false
34
36
 
35
- tag.div class: custom_class || "invalid-feedback" do
37
+ tag.div(
38
+ class: custom_class || "invalid-feedback",
39
+ id: aria_feedback_id(id: options[:id], name:)
40
+ ) do
36
41
  errors = if hide_attribute_name
37
42
  object.errors[name]
38
43
  else
@@ -56,28 +61,29 @@ module BootstrapForm
56
61
  text_field_with_bootstrap(name, static_options)
57
62
  end
58
63
 
59
- def custom_control(*args, &block)
64
+ def custom_control(*args, &)
60
65
  options = args.extract_options!
61
66
  name = args.first
62
67
 
63
- form_group_builder(name, options, &block)
68
+ form_group_builder(name, options, &)
64
69
  end
65
70
 
66
- def prepend_and_append_input(name, options, &block)
71
+ def prepend_and_append_input(name, options, &)
72
+ id = options[:id]
67
73
  options = options.extract!(:prepend, :append, :input_group_class).compact
68
74
 
69
- input = capture(&block) || ActiveSupport::SafeBuffer.new
75
+ input = capture(&) || ActiveSupport::SafeBuffer.new
70
76
 
71
77
  input = attach_input(options, :prepend) + input + attach_input(options, :append)
72
- input << generate_error(name)
78
+ input << generate_error(name, id)
73
79
  options.present? &&
74
80
  input = tag.div(input, class: ["input-group", options[:input_group_class]].compact)
75
81
  input
76
82
  end
77
83
 
78
- def input_with_error(name, &block)
79
- input = capture(&block)
80
- input << generate_error(name)
84
+ def input_with_error(name, id, &)
85
+ input = capture(&)
86
+ input << generate_error(name, id)
81
87
  end
82
88
 
83
89
  def input_group_content(content)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BootstrapForm
2
4
  module Helpers
3
5
  module Field
@@ -6,13 +6,15 @@ module BootstrapForm
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  class_methods do
9
- def bootstrap_field(field_name, control_class: nil)
10
- define_method "#{field_name}_with_bootstrap" do |name, options={ control_class: control_class }.compact|
9
+ def bootstrap_field(field_name)
10
+ define_method :"#{field_name}_with_bootstrap" do |name, options={}|
11
11
  warn_deprecated_layout_value(options)
12
+ options = options.reverse_merge(control_class: "form-range") if field_name == :range_field
13
+ options = options.reverse_merge(control_class: "form-control form-control-color") if field_name == :color_field
12
14
  form_group_builder(name, options) do
13
15
  prepend_and_append_input(name, options) do
14
16
  options[:placeholder] ||= name if options[:floating]
15
- send("#{field_name}_without_bootstrap".to_sym, name, options.except(:floating))
17
+ send(:"#{field_name}_without_bootstrap", name, options.except(:floating))
16
18
  end
17
19
  end
18
20
  end
@@ -21,7 +23,10 @@ module BootstrapForm
21
23
  end
22
24
 
23
25
  def bootstrap_select_group(field_name)
24
- define_method("#{field_name}_with_bootstrap") do |name, options={}, html_options={}|
26
+ define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}|
27
+ # Specifying the id for a select doesn't work. The Rails helpers need to generate
28
+ # what they generate, and that includes the ids for each select option.
29
+ options.delete(:id)
25
30
  html_options = html_options.reverse_merge(control_class: "form-select")
26
31
  form_group_builder(name, options, html_options) do
27
32
  form_group_content_tag(name, field_name, "#{field_name}_without_bootstrap", options, html_options)
@@ -38,8 +43,8 @@ module BootstrapForm
38
43
  # if your application does not include the actiontext dependency due to
39
44
  # `rich_text_area` not being defined.
40
45
  def bootstrap_alias(field_name)
41
- alias_method "#{field_name}_without_bootstrap".to_sym, field_name
42
- alias_method field_name, "#{field_name}_with_bootstrap".to_sym
46
+ alias_method :"#{field_name}_without_bootstrap", field_name
47
+ alias_method field_name, :"#{field_name}_with_bootstrap"
43
48
  rescue NameError # rubocop:disable Lint/SuppressedException
44
49
  end
45
50
  end