bootstrap_form 5.0.0 → 5.2.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +31 -16
  3. data/.gitignore +15 -3
  4. data/.rubocop.yml +3 -1
  5. data/.yarnrc +5 -0
  6. data/CHANGELOG.md +53 -2
  7. data/CONTRIBUTING.md +110 -22
  8. data/Dangerfile +5 -5
  9. data/Dockerfile +6 -11
  10. data/Gemfile +5 -23
  11. data/README.md +805 -54
  12. data/RELEASING.md +16 -10
  13. data/Rakefile +7 -6
  14. data/bootstrap_form.gemspec +7 -10
  15. data/docker-compose.yml +9 -48
  16. data/gemfiles/6.0.gemfile +1 -1
  17. data/gemfiles/6.1.gemfile +1 -1
  18. data/gemfiles/7.0.gemfile +5 -0
  19. data/gemfiles/common.gemfile +28 -0
  20. data/gemfiles/edge.gemfile +3 -2
  21. data/lib/bootstrap_form/components/hints.rb +13 -4
  22. data/lib/bootstrap_form/components/labels.rb +4 -10
  23. data/lib/bootstrap_form/components/validation.rb +36 -19
  24. data/lib/bootstrap_form/form_builder.rb +9 -6
  25. data/lib/bootstrap_form/form_group.rb +15 -8
  26. data/lib/bootstrap_form/form_group_builder.rb +10 -7
  27. data/lib/bootstrap_form/helpers/bootstrap.rb +12 -9
  28. data/lib/bootstrap_form/helpers/field.rb +26 -0
  29. data/lib/bootstrap_form/helpers.rb +1 -0
  30. data/lib/bootstrap_form/inputs/check_box.rb +26 -8
  31. data/lib/bootstrap_form/inputs/collection_check_boxes.rb +5 -1
  32. data/lib/bootstrap_form/inputs/collection_select.rb +1 -1
  33. data/lib/bootstrap_form/inputs/grouped_collection_select.rb +1 -1
  34. data/lib/bootstrap_form/inputs/radio_button.rb +15 -10
  35. data/lib/bootstrap_form/version.rb +2 -1
  36. data/lib/bootstrap_form.rb +1 -1
  37. metadata +17 -88
  38. data/demo/.postcssrc.yml +0 -3
  39. data/demo/README.md +0 -17
  40. data/demo/Rakefile +0 -6
  41. data/demo/app/assets/config/manifest.js +0 -1
  42. data/demo/app/assets/stylesheets/actiontext.scss +0 -38
  43. data/demo/app/assets/stylesheets/application.scss +0 -1
  44. data/demo/app/controllers/application_controller.rb +0 -2
  45. data/demo/app/controllers/bootstrap_controller.rb +0 -14
  46. data/demo/app/helpers/bootstrap_helper.rb +0 -27
  47. data/demo/app/javascript/channels/consumer.js +0 -6
  48. data/demo/app/javascript/channels/index.js +0 -5
  49. data/demo/app/javascript/packs/application.js +0 -11
  50. data/demo/app/models/address.rb +0 -3
  51. data/demo/app/models/application_record.rb +0 -3
  52. data/demo/app/models/faux_user.rb +0 -9
  53. data/demo/app/models/super_user.rb +0 -2
  54. data/demo/app/models/user.rb +0 -11
  55. data/demo/app/views/active_storage/blobs/_blob.html.erb +0 -14
  56. data/demo/app/views/bootstrap/form.html.erb +0 -67
  57. data/demo/app/views/layouts/application.html.erb +0 -68
  58. data/demo/bin/bundle +0 -3
  59. data/demo/bin/rails +0 -4
  60. data/demo/bin/rake +0 -4
  61. data/demo/bin/setup +0 -36
  62. data/demo/bin/update +0 -31
  63. data/demo/bin/webpack +0 -15
  64. data/demo/bin/webpack-dev-server +0 -15
  65. data/demo/bin/yarn +0 -11
  66. data/demo/config/application.rb +0 -21
  67. data/demo/config/boot.rb +0 -5
  68. data/demo/config/database.yml +0 -21
  69. data/demo/config/environment.rb +0 -5
  70. data/demo/config/environments/development.rb +0 -60
  71. data/demo/config/environments/production.rb +0 -48
  72. data/demo/config/environments/test.rb +0 -46
  73. data/demo/config/initializers/application_controller_renderer.rb +0 -8
  74. data/demo/config/initializers/assets.rb +0 -14
  75. data/demo/config/initializers/backtrace_silencers.rb +0 -7
  76. data/demo/config/initializers/cookies_serializer.rb +0 -5
  77. data/demo/config/initializers/filter_parameter_logging.rb +0 -4
  78. data/demo/config/initializers/inflections.rb +0 -16
  79. data/demo/config/initializers/mime_types.rb +0 -4
  80. data/demo/config/initializers/wrap_parameters.rb +0 -14
  81. data/demo/config/locales/en.yml +0 -33
  82. data/demo/config/puma.rb +0 -56
  83. data/demo/config/routes.rb +0 -5
  84. data/demo/config/spring.rb +0 -6
  85. data/demo/config/storage.yml +0 -35
  86. data/demo/config/webpack/development.js +0 -5
  87. data/demo/config/webpack/environment.js +0 -3
  88. data/demo/config/webpack/production.js +0 -5
  89. data/demo/config/webpack/test.js +0 -5
  90. data/demo/config/webpacker.yml +0 -92
  91. data/demo/config.ru +0 -5
  92. data/demo/db/schema.rb +0 -69
  93. data/demo/log/.keep +0 -0
  94. data/demo/package.json +0 -17
  95. data/demo/public/favicon.ico +0 -0
  96. data/demo/test/fixtures/action_text/rich_texts.yml +0 -4
  97. data/demo/yarn.lock +0 -6257
  98. data/gemfiles/5.2.gemfile +0 -4
data/RELEASING.md CHANGED
@@ -6,20 +6,26 @@ Follow these steps to release a new version of bootstrap_form to rubygems.org.
6
6
 
7
7
  * You must have commit rights to the bootstrap_form repository.
8
8
  * You must have push rights for the bootstrap_form gem on rubygems.org.
9
- * You must be using Ruby >= 2.5.
10
- * Your GitHub credentials must be available to Chandler via `~/.netrc` or an environment variable, [as explained here](https://github.com/mattbrictson/chandler#2-configure-credentials).
9
+ * You must be using a Ruby version that is not end-of-life.
11
10
 
12
11
  ## How to release
13
12
 
14
- 1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
15
- 2. **Ensure the tests are passing by running `bundle exec rake`.**
13
+ 1. Run `BUNDLE_GEMFILE=gemfiles/7.0.gemfile bundle update` to make sure that you have all the gems necessary for testing and releasing.
14
+ 2. **Ensure the tests are passing by running the tests**
15
+
16
+ (There should be no errors or warnings.)
17
+
18
+ BUNDLE_GEMFILE=gemfiles/7.0.gemfile bundle exec rake test
19
+
20
+ 2. **Ensure the demo tests are passing by running**
21
+
22
+ cd demo
23
+ bundle update
24
+ bundle exec rake test:all
25
+
16
26
  3. Determine which would be the correct next version number according to [semver](http://semver.org/).
17
27
  4. Update the version in `./lib/bootstrap_form/version.rb`.
18
- 5. Update the `CHANGELOG.md` (for an illustration of these steps, refer to the [4.0.0.alpha1 commit](https://github.com/bootstrap-ruby/bootstrap_form/commit/8aac3667931a16537ab68038ec4cebce186bd596#diff-4ac32a78649ca5bdd8e0ba38b7006a1e) as an example):
19
- * Rename the Pending Release section to `[version][] (date)` with appropriate values `version` and `date`
20
- * Remove the "Your contribution here!" bullets from the release notes
21
- * Add a new Pending Release section at the top of the file with a template for contributors to fill in, including "Your contribution here!" bullets
22
- * Add the appropriate GitHub diff links to the footer of the document
28
+ 5. Update the GitHub diff links at the beginning of `CHANGELOG.md` (The pattern should be obvious when you look at them).
23
29
  6. Update the installation instructions in `README.md` to use the new version.
24
30
  7. 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.
25
- 8. Run `bundle exec rake release`; this will tag, push to GitHub, publish to rubygems.org, and upload the latest CHANGELOG entry to the [GitHub releases page](https://github.com/bootstrap-ruby/bootstrap_form/releases).
31
+ 8. Run `bundle exec rake release`; this will tag, push to GitHub, and publish to rubygems.org.
data/Rakefile CHANGED
@@ -17,12 +17,13 @@ end
17
17
 
18
18
  require 'bundler/gem_tasks'
19
19
 
20
- require 'rake/testtask'
20
+ require "minitest/test_task"
21
21
 
22
- Rake::TestTask.new(:test) do |t|
23
- t.libs << 'test'
24
- t.pattern = 'test/**/*_test.rb'
25
- t.verbose = false
22
+ Minitest::TestTask.create(:test) do |t|
23
+ t.libs << "test"
24
+ t.libs << "lib"
25
+ t.warning = false
26
+ t.test_globs = ["test/**/*_test.rb"]
26
27
  end
27
28
 
28
29
  # This automatically updates GitHub Releases whenever we `rake release` the gem
@@ -34,4 +35,4 @@ end
34
35
  desc 'Run RuboCop checks'
35
36
  RuboCop::RakeTask.new(:rubocop)
36
37
 
37
- task default: %i[test rubocop]
38
+ task default: %i[test rubocop:autocorrect]
@@ -3,31 +3,28 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  require "bootstrap_form/version"
5
5
 
6
- REQUIRED_RAILS_VERSION = ">= 5.2".freeze
7
-
8
6
  Gem::Specification.new do |s|
9
7
  s.name = "bootstrap_form"
10
8
  s.version = BootstrapForm::VERSION
11
9
  s.authors = ["Stephen Potenza", "Carlos Lopes"]
12
10
  s.email = ["potenza@gmail.com", "carlos.el.lopes@gmail.com"]
13
11
  s.homepage = "https://github.com/bootstrap-ruby/bootstrap_form"
14
- s.summary = "Rails form builder that makes it easy to style forms using "\
12
+ s.summary = "Rails form builder that makes it easy to style forms using " \
15
13
  "Bootstrap 5"
16
- s.description = "bootstrap_form is a rails form builder that makes it super "\
14
+ s.description = "bootstrap_form is a rails form builder that makes it super " \
17
15
  "easy to create beautiful-looking forms using Bootstrap 5"
18
16
  s.license = "MIT"
17
+ s.metadata = { "rubygems_mfa_required" => "true" }
19
18
 
20
19
  s.files = `git ls-files -z`.split("\x0").reject do |f|
21
- f.match(%r{^(test)/})
20
+ f.match(%r{^(test)/|^(demo)/})
22
21
  end
23
22
 
24
23
  s.bindir = "exe"
25
24
  s.require_paths = ["lib"]
26
25
 
27
- s.required_ruby_version = ">= 2.5"
28
-
29
- s.add_dependency("actionpack", REQUIRED_RAILS_VERSION)
30
- s.add_dependency("activemodel", REQUIRED_RAILS_VERSION)
26
+ s.required_ruby_version = ">= 3.0"
31
27
 
32
- s.add_development_dependency("rails", REQUIRED_RAILS_VERSION)
28
+ s.add_dependency("actionpack", BootstrapForm::REQUIRED_RAILS_VERSION)
29
+ s.add_dependency("activemodel", BootstrapForm::REQUIRED_RAILS_VERSION)
33
30
  end
data/docker-compose.yml CHANGED
@@ -1,70 +1,31 @@
1
- version: '2.4'
1
+ version: '3.3'
2
2
 
3
3
  services:
4
4
  app: &app
5
5
  build:
6
6
  context: .
7
7
  args:
8
- RUBY_VERSION: '2.7.1'
9
- NODE_MAJOR: '12'
10
- YARN_VERSION: '1.22.4'
11
- BUNDLER_VERSION: '2.1.4'
12
- image: bootstrap-form:0.0.1
8
+ NODE_MAJOR: "12"
9
+ YARN_VERSION: "1.22.4"
10
+ RUBY_VERSION: ${RUBY_VERSION}
11
+ image: bootstrap-form:latest-$RUBY_VERSION
13
12
  tmpfs:
14
13
  - /tmp
15
14
 
16
- backend: &backend
15
+ shell: &shell
17
16
  <<: *app
18
17
  stdin_open: true
19
18
  tty: true
20
19
  volumes:
21
20
  - .:/app:cached
22
- - rails_cache:/app/tmp/cache
23
- - bundle:/usr/local/bundle
24
- - node_modules:/app/node_modules
25
- - packs:/app/public/packs
26
21
  environment:
22
+ - SSH_AUTH_SOCK=/ssh-agent
27
23
  - NODE_ENV=development
28
24
  - RAILS_ENV=${RAILS_ENV:-development}
29
25
  - BOOTSNAP_CACHE_DIR=/usr/local/bundle/_bootsnap
30
26
  - WEBPACKER_DEV_SERVER_HOST=webpacker
31
27
  - WEB_CONCURRENCY=1
32
28
  - HISTFILE=/app/.bash_history
33
- - EDITOR=vi
34
-
35
- shell:
36
- <<: *backend
37
- command: /bin/bash
38
- ports:
39
- - '3000:3000'
40
-
41
- server:
42
- <<: *backend
43
- command: sh -c "cd demo/app && bundle exec rails server -b 0.0.0.0"
44
- ports:
45
- - '3000:3000'
46
-
47
- test:
48
- <<: *backend
49
- command: rake test
50
-
51
- webpacker:
52
- <<: *app
53
- command: ./bin/webpack-dev-server
54
29
  ports:
55
- - '3035:3035'
56
- volumes:
57
- - .:/app:cached
58
- - bundle:/usr/local/bundle
59
- - node_modules:/app/node_modules
60
- - packs:/app/public/packs
61
- environment:
62
- - NODE_ENV=${NODE_ENV:-development}
63
- - RAILS_ENV=${RAILS_ENV:-development}
64
- - WEBPACKER_DEV_SERVER_HOST=0.0.0.0
65
-
66
- volumes:
67
- bundle:
68
- node_modules:
69
- rails_cache:
70
- packs:
30
+ - "3000:3000"
31
+ command: /bin/bash
data/gemfiles/6.0.gemfile CHANGED
@@ -1,4 +1,4 @@
1
- gems = "#{File.dirname __dir__}/Gemfile"
1
+ gems = "#{__dir__}/common.gemfile"
2
2
  eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
4
  gem "rails", "~> 6.0.0"
data/gemfiles/6.1.gemfile CHANGED
@@ -1,4 +1,4 @@
1
- gems = "#{File.dirname __dir__}/Gemfile"
1
+ gems = "#{__dir__}/common.gemfile"
2
2
  eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
4
  gem "rails", "~> 6.1.0"
@@ -0,0 +1,5 @@
1
+ gems = "#{__dir__}/common.gemfile"
2
+ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
+
4
+ gem "rails", "~> 7.0.2"
5
+ gem "sprockets-rails", require: "sprockets/railtie"
@@ -0,0 +1,28 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: File.dirname(__dir__)
4
+
5
+ # To test with different Rails versions, use the files in `./gemfiles`
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
+ group :test do
15
+ gem "diffy"
16
+ gem "equivalent-xml"
17
+ gem "mocha"
18
+ gem "sqlite3"
19
+ end
20
+
21
+ group :development, :test do
22
+ gem "debug"
23
+ gem "pry-byebug"
24
+ end
25
+
26
+ group :ci do
27
+ gem "danger"
28
+ end
@@ -1,4 +1,5 @@
1
- gems = "#{File.dirname __dir__}/Gemfile"
1
+ gems = "#{__dir__}/common.gemfile"
2
2
  eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
- gem "rails", git: "https://github.com/rails/rails.git"
4
+ gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
5
+ gem "sprockets-rails", require: "sprockets/railtie"
@@ -8,7 +8,7 @@ module BootstrapForm
8
8
  private
9
9
 
10
10
  def generate_help(name, help_text)
11
- return if help_text == false || inline_error?(name)
11
+ return if help_text == false
12
12
 
13
13
  help_klass ||= "form-text text-muted"
14
14
  help_text ||= get_help_text_by_i18n_key(name)
@@ -20,10 +20,10 @@ module BootstrapForm
20
20
  def get_help_text_by_i18n_key(name)
21
21
  return unless object
22
22
 
23
- partial_scope = if object.class.respond_to?(:model_name)
24
- object.class.model_name.name
23
+ partial_scope = if object_class.respond_to?(:model_name)
24
+ object_class.model_name.name
25
25
  else
26
- object.class.name
26
+ object_class.name
27
27
  end
28
28
 
29
29
  # First check for a subkey :html, as it is also accepted by i18n, and the
@@ -38,6 +38,15 @@ module BootstrapForm
38
38
  help_text
39
39
  end
40
40
 
41
+ def object_class
42
+ if !object.class.is_a?(ActiveModel::Naming) &&
43
+ object.respond_to?(:klass) && object.klass.is_a?(ActiveModel::Naming)
44
+ object.klass
45
+ else
46
+ object.class
47
+ end
48
+ end
49
+
41
50
  def scoped_help_text(name, partial_scope)
42
51
  underscored_scope = "activerecord.help.#{partial_scope.underscore}"
43
52
  downcased_scope = "activerecord.help.#{partial_scope.downcase}"
@@ -24,14 +24,8 @@ module BootstrapForm
24
24
 
25
25
  def label_classes(name, options, custom_label_col, group_layout)
26
26
  classes = ["form-label", options[:class], label_layout_classes(custom_label_col, group_layout)]
27
-
28
- case options.delete(:required)
29
- when true
30
- classes << "required"
31
- when nil, :default
32
- classes << "required" if required_attribute?(object, name)
33
- end
34
-
27
+ classes << "required" if required_field_options(options, name)[:required]
28
+ options.delete(:required)
35
29
  classes << "text-danger" if label_errors && error?(name)
36
30
  classes.flatten.compact
37
31
  end
@@ -46,9 +40,9 @@ module BootstrapForm
46
40
 
47
41
  def label_text(name, options)
48
42
  if label_errors && error?(name)
49
- (options[:text] || object.class.human_attribute_name(name)).to_s.concat(" #{get_error_messages(name)}")
43
+ (options[:text] || object.class.human_attribute_name(name)).to_s + " #{get_error_messages(name)}"
50
44
  else
51
- options[:text]
45
+ options[:text] || object&.class.try(:human_attribute_name, name)
52
46
  end
53
47
  end
54
48
  end
@@ -8,35 +8,45 @@ module BootstrapForm
8
8
  private
9
9
 
10
10
  def error?(name)
11
- object.respond_to?(:errors) && !(name.nil? || object.errors[name].empty?)
11
+ name && object.respond_to?(:errors) && (object.errors[name].any? || association_error?(name))
12
+ end
13
+
14
+ def association_error?(name)
15
+ object.class.reflections.any? do |association_name, a|
16
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
17
+ next unless a.foreign_key == name.to_s
18
+
19
+ object.errors[association_name].any?
20
+ end
12
21
  end
13
22
 
14
23
  def required_attribute?(obj, attribute)
15
24
  return false unless obj && attribute
16
25
 
17
26
  target = obj.instance_of?(Class) ? obj : obj.class
27
+ return false unless target.respond_to? :validators_on
18
28
 
19
- target_validators = if target.respond_to? :validators_on
20
- target.validators_on(attribute).map(&:class)
21
- else
22
- []
23
- end
24
-
25
- presence_validator?(target_validators)
29
+ presence_validator?(target_validators(target, attribute)) ||
30
+ required_association?(target, attribute)
26
31
  end
27
32
 
28
- def presence_validator?(target_validators)
29
- has_presence_validator = target_validators.include?(
30
- ActiveModel::Validations::PresenceValidator
31
- )
32
-
33
- if defined? ActiveRecord::Validations::PresenceValidator
34
- has_presence_validator |= target_validators.include?(
35
- ActiveRecord::Validations::PresenceValidator
36
- )
33
+ def required_association?(target, attribute)
34
+ target.reflections.find do |name, a|
35
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
36
+ next unless a.foreign_key == attribute.to_s
37
+
38
+ presence_validator?(target_validators(target, name))
37
39
  end
40
+ end
41
+
42
+ def target_validators(target, attribute)
43
+ target.validators_on(attribute).map(&:class)
44
+ end
38
45
 
39
- has_presence_validator
46
+ def presence_validator?(target_validators)
47
+ target_validators.include?(ActiveModel::Validations::PresenceValidator) ||
48
+ (defined?(ActiveRecord::Validations::PresenceValidator) &&
49
+ target_validators.include?(ActiveRecord::Validations::PresenceValidator))
40
50
  end
41
51
 
42
52
  def inline_error?(name)
@@ -54,7 +64,14 @@ module BootstrapForm
54
64
  end
55
65
 
56
66
  def get_error_messages(name)
57
- object.errors[name].join(", ")
67
+ messages = object.errors[name]
68
+ object.class.reflections.each do |association_name, a|
69
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
70
+ next unless a.foreign_key == name.to_s
71
+
72
+ messages << object.errors[association_name]
73
+ end
74
+ messages.join(", ")
58
75
  end
59
76
  end
60
77
  end
@@ -1,10 +1,12 @@
1
1
  # require 'bootstrap_form/aliasing'
2
2
 
3
+ # rubocop:disable Metrics/ClassLength
3
4
  module BootstrapForm
4
5
  class FormBuilder < ActionView::Helpers::FormBuilder
5
6
  attr_reader :layout, :label_col, :control_col, :has_error, :inline_errors,
6
7
  :label_errors, :acts_like_form_tag
7
8
 
9
+ include BootstrapForm::Helpers::Field
8
10
  include BootstrapForm::Helpers::Bootstrap
9
11
 
10
12
  include BootstrapForm::FormGroupBuilder
@@ -67,13 +69,14 @@ module BootstrapForm
67
69
 
68
70
  return unless options[:layout] == :inline
69
71
 
70
- options[:html][:class] = [options[:html][:class], "col-auto", "g-3"].compact.join(" ")
72
+ options[:html][:class] =
73
+ ([*options[:html][:class]&.split(/\s+/)] + %w[row row-cols-auto g-3 align-items-center])
74
+ .compact.uniq.join(" ")
71
75
  end
72
76
 
73
77
  def fields_for_with_bootstrap(record_name, record_object=nil, fields_options={}, &block)
74
78
  fields_options = fields_for_options(record_object, fields_options)
75
- record_object.is_a?(Hash) && record_object.extractable_options? &&
76
- record_object = nil
79
+ record_object = nil if record_object.is_a?(Hash) && record_object.extractable_options?
77
80
  fields_for_without_bootstrap(record_name, record_object, fields_options, &block)
78
81
  end
79
82
 
@@ -87,8 +90,7 @@ module BootstrapForm
87
90
 
88
91
  def fields_for_options(record_object, fields_options)
89
92
  field_options = fields_options
90
- record_object.is_a?(Hash) && record_object.extractable_options? &&
91
- field_options = record_object
93
+ field_options = record_object if record_object.is_a?(Hash) && record_object.extractable_options?
92
94
  %i[layout control_col inline_errors label_errors].each do |option|
93
95
  field_options[option] ||= options[option]
94
96
  end
@@ -114,7 +116,7 @@ module BootstrapForm
114
116
  end
115
117
 
116
118
  def hide_class
117
- "sr-only" # still accessible for screen readers
119
+ "visually-hidden" # still accessible for screen readers
118
120
  end
119
121
 
120
122
  def control_class
@@ -130,3 +132,4 @@ module BootstrapForm
130
132
  end
131
133
  end
132
134
  end
135
+ # rubocop:enable Metrics/ClassLength
@@ -32,15 +32,18 @@ module BootstrapForm
32
32
  end
33
33
  end
34
34
 
35
- def form_group_content(label, help_text, options, &block)
35
+ def form_group_content(label, help_text, options, &block) # rubocop:disable Metrics/AbcSize
36
+ label ||= ActiveSupport::SafeBuffer.new
36
37
  if group_layout_horizontal?(options[:layout])
37
- concat(label).concat(tag.div(capture(&block) + help_text, class: form_group_control_class(options)))
38
+ label + tag.div(capture(&block) + help_text, class: form_group_control_class(options))
38
39
  else
40
+ content = ActiveSupport::SafeBuffer.new
39
41
  # Floating labels need to be rendered after the field
40
- concat(label) unless options[:floating]
41
- concat(capture(&block))
42
- concat(label) if options[:floating]
43
- concat(help_text) if help_text
42
+ content << label unless options[:floating]
43
+ content << capture(&block)
44
+ content << label if options[:floating]
45
+ content << help_text if help_text
46
+ content
44
47
  end
45
48
  end
46
49
 
@@ -52,12 +55,16 @@ module BootstrapForm
52
55
  end
53
56
 
54
57
  def form_group_classes(options)
55
- classes = ["mb-3", options[:class].try(:split)].flatten.compact
58
+ classes = options[:class] == false ? [] : (options[:class] || form_group_default_class).split
56
59
  classes << "row" if horizontal_group_with_gutters?(options[:layout], classes)
57
60
  classes << "col-auto g-3" if field_inline_override?(options[:layout])
58
61
  classes << feedback_class if options[:icon]
59
62
  classes << "form-floating" if options[:floating]
60
- classes
63
+ classes.presence
64
+ end
65
+
66
+ def form_group_default_class
67
+ (layout == :inline ? "col" : "mb-3")
61
68
  end
62
69
 
63
70
  def horizontal_group_with_gutters?(layout, classes)
@@ -28,8 +28,7 @@ module BootstrapForm
28
28
  def form_group_builder_options(options, method)
29
29
  options.symbolize_keys!
30
30
  options = convert_form_tag_options(method, options) if acts_like_form_tag
31
- options[:required] = form_group_required(options) if !options[:skip_label] && options.key?(:skip_required)
32
- options
31
+ options.merge!(required_field_options(options, method))
33
32
  end
34
33
 
35
34
  def convert_form_tag_options(method, options={})
@@ -77,11 +76,15 @@ module BootstrapForm
77
76
  classes
78
77
  end
79
78
 
80
- def form_group_required(options)
81
- return unless options.key?(:skip_required)
82
-
83
- warn "`:skip_required` is deprecated, use `:required: false` instead"
84
- options[:skip_required] ? false : :default
79
+ def form_group_required(options, method)
80
+ if options[:skip_required]
81
+ warn "`:skip_required` is deprecated, use `:required: false` instead"
82
+ false
83
+ elsif options.key?(:required)
84
+ options[:required]
85
+ else
86
+ required_attribute?(object, method)
87
+ end
85
88
  end
86
89
 
87
90
  def form_group_css_options(method, html_options, options)
@@ -8,7 +8,7 @@ module BootstrapForm
8
8
 
9
9
  def submit(name=nil, options={})
10
10
  setup_css_class "btn btn-secondary", options
11
- super
11
+ layout == :inline ? form_group { super } : super
12
12
  end
13
13
 
14
14
  def primary(name=nil, options={}, &block)
@@ -27,17 +27,20 @@ module BootstrapForm
27
27
  return unless object.respond_to?(:errors) && object.errors.full_messages.any?
28
28
 
29
29
  tag.div class: css do
30
- concat tag.p title
31
- concat error_summary unless options[:error_summary] == false
30
+ if options[:error_summary] == false
31
+ title
32
+ else
33
+ tag.p(title) + error_summary
34
+ end
32
35
  end
33
36
  end
34
37
 
35
38
  def error_summary
36
39
  return unless object.errors.any?
37
40
 
38
- tag.ul class: "rails-bootstrap-forms-error-summary" do
39
- object.errors.full_messages.each do |error|
40
- concat tag.li(error)
41
+ tag.ul(class: "rails-bootstrap-forms-error-summary") do
42
+ object.errors.full_messages.reduce(ActiveSupport::SafeBuffer.new) do |acc, error|
43
+ acc << tag.li(error)
41
44
  end
42
45
  end
43
46
  end
@@ -79,12 +82,12 @@ module BootstrapForm
79
82
  end
80
83
 
81
84
  def prepend_and_append_input(name, options, &block)
82
- options = options.extract!(:prepend, :append, :input_group_class)
85
+ options = options.extract!(:prepend, :append, :input_group_class).compact
83
86
 
84
87
  input = capture(&block) || ActiveSupport::SafeBuffer.new
85
88
 
86
89
  input = attach_input(options, :prepend) + input + attach_input(options, :append)
87
- input += generate_error(name)
90
+ input << generate_error(name)
88
91
  options.present? &&
89
92
  input = tag.div(input, class: ["input-group", options[:input_group_class]].compact)
90
93
  input
@@ -96,7 +99,7 @@ module BootstrapForm
96
99
  end
97
100
 
98
101
  def input_group_content(content)
99
- return content if /btn/.match?(content)
102
+ return content if content.include?("btn")
100
103
 
101
104
  tag.span(content, class: "input-group-text")
102
105
  end
@@ -0,0 +1,26 @@
1
+ module BootstrapForm
2
+ module Helpers
3
+ module Field
4
+ def required_field_options(options, method)
5
+ required = required_field?(options, method)
6
+ {}.tap do |option|
7
+ option[:required] = required
8
+ option[:aria] = { required: true } if required
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def required_field?(options, method)
15
+ if options[:skip_required]
16
+ warn "`:skip_required` is deprecated, use `:required: false` instead"
17
+ false
18
+ elsif options.key?(:required)
19
+ options[:required]
20
+ else
21
+ required_attribute?(object, method)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,5 +5,6 @@ module BootstrapForm
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Bootstrap
8
+ autoload :Field
8
9
  end
9
10
  end