bootstrap_form 5.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1627869d85826ca2fb85c7b4257cf66cd26adc270fabe10bc05246b248eb26a
4
- data.tar.gz: 2e5ec700c4c51b92aafe31633e92470318f7377740ba2da7e9f7738197c8ffe3
3
+ metadata.gz: '080b03659b41c88ebbce90498ef35b440c8f41f557b290369a7193c3d8ae3e3e'
4
+ data.tar.gz: 6659f30906b3c7ce3b01f276b7e12dc777ef8fdd0213977aeb42dccdd7b5ef0d
5
5
  SHA512:
6
- metadata.gz: d3f8a72059cee34fd75e6998f47662efd59f59c47cad3e998dea936a85ea8e38f059f37dc89794ebabc324c6e86ad7f8d50458c60db5b3a94f28d18dbdfa965a
7
- data.tar.gz: 7207758b1628e88675c151ffc4d59256a37cc484d27ac7ee000eca6f2b32365cc2daf48c6541867c24757fdde7bff72911390dda25d070516ceb72c3dbc6beb7
6
+ metadata.gz: 4a39ff618a9f881079640c35c345a1e64e376259bad7eeaf20442a22804083ec1661b2d2babb9ef4dbcf420eb7b5681f2907dc68df20688c3234bf1ebc776a98
7
+ data.tar.gz: df4ce96547bd4e460d5e6960262c74619a377a7ae47d5dcac3e930c0a39f175a8853017e4536419a8e179c91c53930a8902a53be176c61dea2013a5cf9b492c6
@@ -11,12 +11,12 @@ jobs:
11
11
  env:
12
12
  BUNDLE_GEMFILE: gemfiles/7.2.gemfile
13
13
  steps:
14
- - uses: actions/checkout@v3
14
+ - uses: actions/checkout@v5
15
15
  with:
16
16
  fetch-depth: 0
17
17
  - uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: 3.2.9
19
+ ruby-version: 3.2
20
20
  bundler-cache: true
21
21
  # Disabled since it requires access not granted by GitHub Actions for PRs
22
22
  # - name: Danger
@@ -34,10 +34,13 @@ jobs:
34
34
  matrix:
35
35
  ruby-version: [ '3.4', '3.3', '3.2', 'ruby-head' ]
36
36
  gemfile: [ '8.1', '8.0', '7.2', 'edge' ]
37
+ exclude:
38
+ - ruby-version: 3.2
39
+ - gemfile: 'edge'
37
40
  env:
38
41
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
39
42
  steps:
40
- - uses: actions/checkout@v3
43
+ - uses: actions/checkout@v5
41
44
  - name: Set up Ruby
42
45
  uses: ruby/setup-ruby@v1
43
46
  with:
@@ -48,13 +51,15 @@ jobs:
48
51
  Demo:
49
52
  runs-on: ubuntu-latest
50
53
  steps:
51
- - uses: actions/checkout@v3
54
+ - uses: actions/checkout@v5
52
55
  - name: Set up Ruby
53
56
  uses: ruby/setup-ruby@v1
54
57
  with:
55
58
  working-directory: demo
56
59
  bundler-cache: true
57
- ruby-version: 3.4.2
60
+ ruby-version: 3.4
61
+ env:
62
+ BUNDLE_GEMFILE: '../Gemfile'
58
63
  - name: Run tests
59
64
  working-directory: demo
60
65
  run: bundle exec rake test:all
data/.gitignore CHANGED
@@ -29,6 +29,7 @@ gemfiles/*.lock
29
29
  /.ruby-version
30
30
  Vagrantfile
31
31
  .vagrant
32
+ .yarnrc
32
33
  **/.yarn/**/cache
33
34
  **/.yarn/install-state.gz
34
35
  **/.yarn/unplugged
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ AllCops:
6
6
  DisplayCopNames: true
7
7
  DisplayStyleGuide: true
8
8
  TargetRubyVersion: 3.2
9
- TargetRailsVersion: 7.1
9
+ TargetRailsVersion: 7.2
10
10
  NewCops: enable
11
11
  Exclude:
12
12
  - bin/*
@@ -25,7 +25,7 @@ AllCops:
25
25
  - gemfiles/vendor/bundle/**/*
26
26
  - vendor/bundle/**/*
27
27
  - Guardfile
28
- - Rakefile
28
+ - test/dummy/**/*
29
29
  - vendor/**/*
30
30
 
31
31
  Layout/LineLength:
@@ -54,11 +54,7 @@ Metrics/ClassLength:
54
54
  - "test/**/*"
55
55
 
56
56
  Metrics/MethodLength:
57
- Max: 12
58
- Exclude:
59
- - "demo/db/migrate/*"
60
- - "demo/test/**/*"
61
- - "test/**/*"
57
+ Enabled: false
62
58
 
63
59
  Naming/MemoizedInstanceVariableName:
64
60
  EnforcedStyleForLeadingUnderscores: optional
@@ -73,6 +69,10 @@ Rails/ApplicationRecord:
73
69
  Exclude:
74
70
  - "demo/db/migrate/**"
75
71
 
72
+ Rails/RakeEnvironment:
73
+ Exclude:
74
+ - "Rakefile"
75
+
76
76
  Rails/RefuteMethods:
77
77
  Enabled: false
78
78
 
@@ -94,9 +94,6 @@ Style/DoubleNegation:
94
94
  Style/EmptyMethod:
95
95
  Enabled: false
96
96
 
97
- Style/FrozenStringLiteralComment:
98
- Enabled: false
99
-
100
97
  Style/NumericPredicate:
101
98
  Enabled: false
102
99
 
@@ -105,3 +102,18 @@ Style/StringLiterals:
105
102
 
106
103
  Style/TrivialAccessors:
107
104
  AllowPredicates: true
105
+
106
+ Style/FrozenStringLiteralComment:
107
+ Enabled: true
108
+ EnforcedStyle: always # or 'always' or 'never' depending on your preference
109
+ SafeAutoCorrect: true # Set to true for safe autocorrection, false if you need to review changes
110
+ Exclude:
111
+ - ./**/Gemfile*
112
+ - bootstrap_form.gemspec
113
+ - Dangerfile
114
+ - demo/config/**/*
115
+ - demo/config.ru
116
+ - demo/db/migrate/**/*
117
+ - gemfiles/*
118
+ - Rakefile
119
+ - Vagrantfile
data/CONTRIBUTING.md CHANGED
@@ -6,9 +6,26 @@ We want everyone to feel welcome to contribute. We encourage respectful exchange
6
6
 
7
7
  There are a number of ways you can contribute to `bootstrap_form`:
8
8
 
9
- - Fix a bug or add a new feature
10
- - Add to the documentation
11
- - Review pull requests
9
+ - Test pre-release versions of the gem, by using the `main` branch.
10
+ - Fix a bug or add a new feature.
11
+ - Add to the documentation.
12
+ - Review pull requests.
13
+
14
+ ## Testing
15
+
16
+ `bootstrap_form` has an extensive automated test suite. But there are a multitude of combinations of use cases for this gem. It's impossible to cover all the combinations.
17
+
18
+ You can help us test by using the `main` branch from GitHub, rather than a released version of the gem. To do so, replace the `bootstrap_form` line in your `Gemfile` with:
19
+
20
+ ```
21
+ gem "bootstrap_form", git: "https://bootstrap-ruby/bootstrap_form"
22
+ ```
23
+
24
+ and run `bundle update`.
25
+
26
+ If you run into problems, you can always switch back to the released version of `bootstrap_form` from your provider of gems. Please [raise an issue](https://github.com/bootstrap-ruby/bootstrap_form/issues/new/choose) if you think you found a problem.
27
+
28
+ And a big thank-you for helping us test `bootstrap_form`.
12
29
 
13
30
  ## Code Contributions
14
31
 
data/Gemfile CHANGED
@@ -4,7 +4,9 @@ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
4
4
  require "#{__dir__}/lib/bootstrap_form/version"
5
5
 
6
6
  gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
7
+ gem "cssbundling-rails"
7
8
  gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
9
+ gem "jsbundling-rails"
8
10
  gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
9
11
  gem "rails", BootstrapForm::REQUIRED_RAILS_VERSION
10
12
  gem "propshaft"
data/README.md CHANGED
@@ -52,7 +52,7 @@ You can use this gem with other ways of installing Bootstrap, but how to do so i
52
52
  Once Bootstrap is installed, add the `bootstrap_form` gem to your `Gemfile`:
53
53
 
54
54
  ```ruby
55
- gem "bootstrap_form", "~> 5.5"
55
+ gem "bootstrap_form", "~> 5.6"
56
56
  ```
57
57
 
58
58
  Then:
@@ -120,6 +120,8 @@ This generates the following HTML:
120
120
  </form>
121
121
  ```
122
122
 
123
+ Note: All examples in this README are generated with the configuration option `group_around_collections` set to `true`. See the [Configuration](#configuration) section.
124
+
123
125
  ### bootstrap_form_tag
124
126
 
125
127
  If your form is not backed by a model, use the `bootstrap_form_tag`. Usage of this helper is the same as `bootstrap_form_for`, except no model object is passed in as the first argument. Here's an example:
@@ -226,13 +228,14 @@ Generated HTML:
226
228
 
227
229
  ## Configuration
228
230
 
229
- `bootstrap_form` can be used out-of-the-box without any configuration. However, `bootstrap_form` does have an optional configuration file at `config/initializers/bootstrap_form.rb` for setting options that affect all generated forms in an application.
231
+ `bootstrap_form` can be used out-of-the-box without any configuration. However, `bootstrap_form` does have an optional configuration file at `config/initializers/bootstrap_form.rb` for setting options that affect all generated forms in an application, or for enabling or disabling new functionality that might not be compatible with your application.
230
232
 
231
233
  The current configuration options are:
232
234
 
233
235
  | Option | Default value | Description |
234
236
  |---------------------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
235
- | `default_form_attributes` | | `bootstrap_form` versions 3 and 4 added a role="form" attribute to all forms. The W3C validator will raise a **warning** on forms with a role="form" attribute. `bootstrap_form` version 5 drops this attribute by default. Set this option to `{ role: "form" }` to make forms non-compliant with W3C, but generate the `role="form"` attribute like `bootstrap_form` versions 3 and 4. |
237
+ | `default_form_attributes` | {} | `bootstrap_form` versions 3 and 4 added a role="form" attribute to all forms. The W3C validator will raise a **warning** on forms with a role="form" attribute. `bootstrap_form` version 5 drops this attribute by default. Set this option to `{ role: "form" }` to make forms non-compliant with W3C, but generate the `role="form"` attribute like `bootstrap_form` versions 3 and 4. |
238
+ | `group_around_collections` | false | Historically, `bootstrap_form` generated a wrapper around `collection_checkboxes` and `collection_radio_buttons` using the same `form_group` as individual controls used. This markup caused accessibility problems. Setting `group_around_collections = true` will generate collections of checkboxes and radio buttons wrapper in a `<fieldset>` with the text as a `<legend>` (https://www.w3.org/WAI/tutorials/forms/grouping/). This _will_ make visible changes to pages that use the collection methods.<br/><br/>The default for this option will be changed to `true` in a future version. |
236
239
 
237
240
  Example:
238
241
 
@@ -781,8 +784,8 @@ This generates:
781
784
  This generates:
782
785
 
783
786
  ```html
784
- <div class="mb-3">
785
- <label class="form-label" for="user_skill_level">Skill level</label>
787
+ <div aria-labelledby="user_skill_level" class="mb-3" role="group">
788
+ <div class="form-label" id="user_skill_level">Skill level</div>
786
789
  <div class="form-check">
787
790
  <input class="form-check-input" id="user_skill_level_1" name="user[skill_level]" type="radio" value="1">
788
791
  <label class="form-check-label" for="user_skill_level_1">Mind reading</label>
@@ -793,8 +796,8 @@ This generates:
793
796
  </div>
794
797
  </div>
795
798
  <input id="user_skills" name="user[skills][]" type="hidden" value="">
796
- <div class="mb-3">
797
- <label class="form-label" for="user_skills">Skills</label>
799
+ <div aria-labelledby="user_skills" class="mb-3" role="group">
800
+ <div class="form-label" id="user_skills">Skills</div>
798
801
  <div class="form-check">
799
802
  <input class="form-check-input" id="user_skills_1" name="user[skills][]" type="checkbox" value="1">
800
803
  <label class="form-check-label" for="user_skills_1">Mind reading</label>
@@ -829,8 +832,8 @@ To add `data-` attributes to a collection of radio buttons, map your models to a
829
832
  This generates:
830
833
 
831
834
  ```html
832
- <div class="mb-3">
833
- <label class="form-label" for="user_misc">Misc</label>
835
+ <div aria-labelledby="user_misc" class="mb-3" role="group">
836
+ <div class="form-label" id="user_misc">Misc</div>
834
837
  <div class="form-check">
835
838
  <input class="form-check-input" id="user_misc_1" name="user[misc]" type="radio" value="1">
836
839
  <label class="form-check-label" for="user_misc_1">Foo</label>
@@ -1417,7 +1420,7 @@ This generates:
1417
1420
  </form>
1418
1421
  ```
1419
1422
 
1420
- A form-level `layout: :inline` can't be overridden because of the way Bootstrap 4 implements in-line layouts. One possible work-around is to leave the form-level layout as default, and specify the individual fields as `layout: :inline`, except for the fields(s) that should be other than in-line.
1423
+ A form-level `layout: :inline` can't be overridden because of the way Bootstrap implements in-line layouts. One possible work-around is to leave the form-level layout as default, and specify the individual fields as `layout: :inline`, except for the fields(s) that should be other than in-line.
1421
1424
 
1422
1425
  ### Floating Labels
1423
1426
 
@@ -1490,38 +1493,38 @@ Generated HTML:
1490
1493
  <form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
1491
1494
  <div class="mb-3">
1492
1495
  <label class="form-label required" for="user_email">Email</label>
1493
- <input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1494
- <div class="invalid-feedback">is invalid</div>
1496
+ <input aria-describedby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1497
+ <div class="invalid-feedback" id="user_email_feedback">is invalid</div>
1495
1498
  </div>
1496
- <div class="mb-3">
1497
- <label class="form-label" for="user_misc">Misc</label>
1499
+ <div aria-labelledby="user_misc" class="mb-3" role="group">
1500
+ <div class="form-label" id="user_misc">Misc</div>
1498
1501
  <div class="form-check">
1499
- <input checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
1502
+ <input aria-describedby="user_misc_feedback" checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
1500
1503
  <label class="form-check-label" for="user_misc_1">Mind reading</label>
1501
1504
  </div>
1502
1505
  <div class="form-check">
1503
- <input class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
1506
+ <input aria-describedby="user_misc_feedback" class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
1504
1507
  <label class="form-check-label" for="user_misc_2">Farming</label>
1505
- <div class="invalid-feedback">is invalid</div>
1508
+ <div class="invalid-feedback" id="user_misc_feedback">is invalid</div>
1506
1509
  </div>
1507
1510
  </div>
1508
1511
  <input id="user_preferences" name="user[preferences][]" type="hidden" value="">
1509
- <div class="mb-3">
1510
- <label class="form-label" for="user_preferences">Preferences</label>
1512
+ <div aria-labelledby="user_preferences" class="mb-3" role="group">
1513
+ <div class="form-label" id="user_preferences">Preferences</div>
1511
1514
  <div class="form-check">
1512
- <input checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
1515
+ <input aria-describedby="user_preferences_feedback" checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
1513
1516
  <label class="form-check-label" for="user_preferences_1">Good</label>
1514
1517
  </div>
1515
1518
  <div class="form-check">
1516
- <input class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
1519
+ <input aria-describedby="user_preferences_feedback" class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
1517
1520
  <label class="form-check-label" for="user_preferences_2">Bad</label>
1518
- <div class="invalid-feedback">is invalid</div>
1521
+ <div class="invalid-feedback" id="user_preferences_feedback">is invalid</div>
1519
1522
  </div>
1520
1523
  </div>
1521
1524
  <div class="mb-3">
1522
1525
  <label class="form-label" for="user_address_attributes_street">Street</label>
1523
- <input class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
1524
- <div class="invalid-feedback">is invalid</div>
1526
+ <input aria-describedby="user_address_attributes_street_feedback" class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
1527
+ <div class="invalid-feedback" id="user_address_attributes_street_feedback">is invalid</div>
1525
1528
  </div>
1526
1529
  </form>
1527
1530
  ```
@@ -1551,8 +1554,8 @@ Generated HTML:
1551
1554
  ```html
1552
1555
  <form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
1553
1556
  <div class="mb-3">
1554
- <label class="form-label required text-danger" for="user_email">Email is invalid</label>
1555
- <input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1557
+ <label class="form-label required text-danger" for="user_email" id="user_email_feedback">Email is invalid</label>
1558
+ <input aria-describedby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1556
1559
  </div>
1557
1560
  </form>
1558
1561
  ```
@@ -1649,7 +1652,7 @@ Which outputs:
1649
1652
  ```html
1650
1653
  <form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
1651
1654
  <input autocomplete="off" class="is-invalid" disabled type="hidden">
1652
- <div class="invalid-feedback">Email is invalid</div>
1655
+ <div class="invalid-feedback" id="user_email_feedback">Email is invalid</div>
1653
1656
  </form>
1654
1657
  ```
1655
1658
 
@@ -1670,7 +1673,7 @@ Which outputs:
1670
1673
  ```html
1671
1674
  <form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
1672
1675
  <input autocomplete="off" class="is-invalid" disabled type="hidden">
1673
- <div class="invalid-feedback">is invalid</div>
1676
+ <div class="invalid-feedback" id="user_email_feedback">is invalid</div>
1674
1677
  </form>
1675
1678
  ```
1676
1679
 
@@ -1689,7 +1692,7 @@ Which outputs:
1689
1692
  ```html
1690
1693
  <form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
1691
1694
  <input autocomplete="off" class="is-invalid" disabled type="hidden">
1692
- <div class="custom-error">Email is invalid</div>
1695
+ <div class="custom-error" id="user_email_feedback">Email is invalid</div>
1693
1696
  </form>
1694
1697
  ```
1695
1698
 
data/RELEASING.md CHANGED
@@ -10,6 +10,8 @@ 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.
@@ -24,7 +26,6 @@ Follow these steps to release a new version of bootstrap_form to rubygems.org.
24
26
 
25
27
  You will have failures in the system tests unless you're running on Linux. Chrome on each operating system renders slightly differently.
26
28
 
27
- 6. Update the GitHub diff links at the beginning of `CHANGELOG.md` (The pattern should be obvious when you look at them).
28
29
  7. Update the installation instructions in `README.md` to use the new version.
29
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.
30
31
  9. Tag, push to GitHub, and publish to rubygems.org:
data/Rakefile CHANGED
@@ -1,30 +1,27 @@
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]
@@ -32,22 +29,20 @@ task default: %i[test rubocop:autocorrect]
32
29
  namespace :test do
33
30
  desc "Run tests for all supported Rails versions, with current Ruby version"
34
31
  task :all do
35
- original_gemfile = ENV["BUNDLE_GEMFILE"]
36
- gemfiles = Dir.glob("gemfiles/*.gemfile").reject { |f| File.basename(f) == "common.gemfile" }
37
- gemfiles.each do |f|
38
- ENV["BUNDLE_GEMFILE"] = f
39
- system("bundle check") || system("bundle install")
40
- system("bundle exec rake test")
32
+ gemfiles.each do |gemfile|
33
+ system("BUNDLE_GEMFILE=#{gemfile} rake test")
41
34
  end
42
35
 
43
- original_directory = Dir.pwd
44
36
  Dir.chdir("demo")
45
- ENV.delete("BUNDLE_GEMFILE")
46
- system("bundle check") || system("bundle install")
47
- system("bundle exec rake test:all")
37
+ system("BUNDLE_GEMFILE= rake test:all")
38
+ end
39
+ end
48
40
 
49
- ensure
50
- original_gemfile.nil? ? ENV.delete("BUNDLE_GEMFILE") : ENV["BUNDLE_GEMFILE"] = original_gemfile
51
- Dir.chdir(original_directory) unless original_directory.nil?
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")
52
45
  end
53
46
  end
47
+
48
+ def gemfiles = Dir.glob("gemfiles/*.gemfile").reject { |f| File.basename(f) == "common.gemfile" }
data/gemfiles/7.2.gemfile CHANGED
@@ -3,6 +3,7 @@ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
4
  gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
5
  gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "minitest", "~> 5.0"
6
7
  gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
7
8
  gem "rails", "~> 7.2.0"
8
9
  gem "sprockets-rails", require: "sprockets/railtie"
data/gemfiles/8.0.gemfile CHANGED
@@ -3,6 +3,7 @@ eval File.read(gems), binding, gems # rubocop: disable Security/Eval
3
3
 
4
4
  gem "bigdecimal" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
5
5
  gem "drb" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
6
+ gem "minitest", "~> 5.0"
6
7
  gem "mutex_m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
7
8
  gem "propshaft"
8
9
  gem "rails", "~> 8.0.1"
@@ -4,22 +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"
14
+ gem "selenium-webdriver"
18
15
  end
19
16
 
20
17
  group :development, :test do
21
18
  gem "debug"
19
+ gem "htmlbeautifier"
22
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"
23
25
  end
24
26
 
25
27
  group :ci do
@@ -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)
@@ -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, Style/SafeNavigationChainLength
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,14 +61,14 @@ 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
74
  def get_error_messages(name)
@@ -84,6 +84,10 @@ module BootstrapForm
84
84
  safe_join(object.errors[name], ", ")
85
85
  end
86
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
87
91
  end
88
92
  end
89
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # require 'bootstrap_form/aliasing'
2
4
 
3
5
  module BootstrapForm
@@ -23,7 +23,7 @@ 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
@@ -7,6 +7,16 @@ module BootstrapForm
7
7
  private
8
8
 
9
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, &)
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
@@ -64,20 +69,21 @@ module BootstrapForm
64
69
  end
65
70
 
66
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
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, &)
84
+ def input_with_error(name, id, &)
79
85
  input = capture(&)
80
- input << generate_error(name)
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
@@ -24,6 +24,9 @@ module BootstrapForm
24
24
 
25
25
  def bootstrap_select_group(field_name)
26
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)
27
30
  html_options = html_options.reverse_merge(control_class: "form-select")
28
31
  form_group_builder(name, options, html_options) do
29
32
  form_group_content_tag(name, field_name, "#{field_name}_without_bootstrap", options, html_options)
@@ -13,7 +13,7 @@ module BootstrapForm
13
13
  content = tag.div(class: check_box_wrapper_class(options), **options[:wrapper].to_h.except(:class)) do
14
14
  html = check_box_without_bootstrap(name, check_box_options(name, options), checked_value, unchecked_value)
15
15
  html << check_box_label(name, options, checked_value, &block) unless options[:skip_label]
16
- html << generate_error(name) if options[:error_message]
16
+ html << generate_error(name, options[:id]) if options[:error_message]
17
17
  html
18
18
  end
19
19
  wrapper(content, options)
@@ -41,6 +41,7 @@ module BootstrapForm
41
41
  :inline, :label, :label_class, :label_col, :layout, :skip_label,
42
42
  :switch, :wrapper, :wrapper_class)
43
43
  check_box_options[:class] = check_box_classes(name, options)
44
+ check_box_options[:aria] = { describedby: aria_feedback_id(id: options[:id], name:) } if error?(name)
44
45
  check_box_options.merge!(required_field_options(options, name))
45
46
  end
46
47
 
@@ -9,6 +9,7 @@ module BootstrapForm
9
9
 
10
10
  included do
11
11
  def collection_check_boxes_with_bootstrap(*args)
12
+ args[4]&.delete(:id)
12
13
  html = inputs_collection(*args) do |name, value, options|
13
14
  options[:multiple] = true
14
15
  check_box(name, options, value, nil)
@@ -9,6 +9,7 @@ module BootstrapForm
9
9
 
10
10
  included do
11
11
  def collection_radio_buttons_with_bootstrap(*args)
12
+ args[4]&.delete(:id)
12
13
  inputs_collection(*args) do |name, value, options|
13
14
  radio_button(name, value, options)
14
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BootstrapForm
2
4
  module Inputs
3
5
  module InputsCollection
@@ -5,23 +7,21 @@ module BootstrapForm
5
7
 
6
8
  private
7
9
 
8
- def inputs_collection(name, collection, value, text, options={})
9
- options[:label] ||= { class: group_label_class(options[:layout]) }
10
+ def inputs_collection(name, collection, value, text, options={}, &)
11
+ options[:label] ||= { class: group_label_class(field_layout(options)) }
10
12
  options[:inline] ||= layout_inline?(options[:layout])
11
13
 
12
- form_group_builder(name, options) do
13
- inputs = ActiveSupport::SafeBuffer.new
14
-
15
- collection.each_with_index do |obj, i|
16
- input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
17
- input_options = form_group_collection_input_options(options, text, obj, i, input_value, collection)
18
- inputs << yield(name, input_value, input_options)
19
- end
14
+ return group_inputs_collection(name, collection, value, text, options, &) if BootstrapForm.config.group_around_collections
20
15
 
21
- inputs
16
+ form_group_builder(name, options) do
17
+ render_collection(name, collection, value, text, options, &)
22
18
  end
23
19
  end
24
20
 
21
+ def field_layout(options)
22
+ (:inline if options[:inline] == true) || options[:layout]
23
+ end
24
+
25
25
  def group_label_class(field_layout)
26
26
  if layout_horizontal?(field_layout)
27
27
  group_label_class = "col-form-label #{label_col} pt-0"
@@ -52,6 +52,62 @@ module BootstrapForm
52
52
  checked == input_value || Array(checked).try(:include?, input_value) ||
53
53
  checked == obj || Array(checked).try(:include?, obj)
54
54
  end
55
+
56
+ def group_inputs_collection(name, collection, value, text, options={}, &)
57
+ group_builder(name, options) do
58
+ render_collection(name, collection, value, text, options, &)
59
+ end
60
+ end
61
+
62
+ def render_collection(name, collection, value, text, options={}, &)
63
+ inputs = ActiveSupport::SafeBuffer.new
64
+
65
+ collection.each_with_index do |obj, i|
66
+ input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
67
+ input_options = form_group_collection_input_options(options, text, obj, i, input_value, collection)
68
+ inputs << yield(name, input_value, input_options)
69
+ end
70
+
71
+ inputs
72
+ end
73
+
74
+ def group_builder(method, options, html_options=nil, &)
75
+ form_group_builder_wrapper(method, options, html_options) do |form_group_options, no_wrapper|
76
+ if no_wrapper
77
+ yield
78
+ else
79
+ field_group(method, form_group_options, &)
80
+ end
81
+ end
82
+ end
83
+
84
+ def field_group(name, options, &)
85
+ options[:class] = form_group_classes(options)
86
+
87
+ tag.div(
88
+ **options.except(
89
+ :add_control_col_class, :append, :control_col, :floating, :help, :icon, :id,
90
+ :input_group_class, :label, :label_col, :layout, :prepend
91
+ ),
92
+ aria: { labelledby: group_label_div_id(id: options[:id], name:) },
93
+ role: :group
94
+ ) do
95
+ group_label_div = generate_group_label_div(name, options)
96
+ prepare_label_options(options[:id], name, options[:label], options[:label_col], options[:layout])
97
+ form_group_content(group_label_div, generate_help(name, options[:help]), options, &)
98
+ end
99
+ end
100
+
101
+ def generate_group_label_div(name, options)
102
+ group_label_div_class = options.dig(:label, :class) || "form-label"
103
+
104
+ tag.div(
105
+ **{ class: group_label_div_class }.compact,
106
+ id: group_label_div_id(id: options[:id], name:)
107
+ ) { label_text(name, options.dig(:label, :text)) }
108
+ end
109
+
110
+ def group_label_div_id(id:, name:) = id || field_id(name)
55
111
  end
56
112
  end
57
113
  end
@@ -14,7 +14,7 @@ module BootstrapForm
14
14
  tag.div(**wrapper_attributes) do
15
15
  html = radio_button_without_bootstrap(name, value, radio_button_options(name, options))
16
16
  html << radio_button_label(name, value, options) unless options[:skip_label]
17
- html << generate_error(name) if options[:error_message]
17
+ html << generate_error(name, options[:id]) if options[:error_message]
18
18
  html
19
19
  end
20
20
  end
@@ -28,6 +28,7 @@ module BootstrapForm
28
28
  radio_button_options = options.except(:class, :label, :label_class, :error_message, :help,
29
29
  :inline, :hide_label, :skip_label, :wrapper, :wrapper_class)
30
30
  radio_button_options[:class] = radio_button_classes(name, options)
31
+ radio_button_options[:aria] = { describedby: aria_feedback_id(id: options[:id], name:) } if error?(name)
31
32
  radio_button_options.merge!(required_field_options(options, name))
32
33
  end
33
34
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BootstrapForm
2
4
  module Inputs
3
5
  module Submit
@@ -10,7 +10,7 @@ module BootstrapForm
10
10
  def time_zone_select_with_bootstrap(method, priority_zones=nil, options={}, html_options={})
11
11
  html_options = html_options.reverse_merge(control_class: "form-select")
12
12
  form_group_builder(method, options, html_options) do
13
- input_with_error(method) do
13
+ input_with_error(method, options[:id]) do
14
14
  time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
15
15
  end
16
16
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BootstrapForm
2
- VERSION = "5.5.0".freeze
3
- REQUIRED_RAILS_VERSION = ">= 7.2".freeze
4
+ VERSION = "5.6.0"
5
+ REQUIRED_RAILS_VERSION = ">= 7.2"
4
6
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view"
2
4
  require "action_pack"
3
5
  require "bootstrap_form/action_view_extensions/form_helper"
6
+ require "bootstrap_form/configuration"
4
7
 
5
8
  module BootstrapForm
6
9
  extend ActiveSupport::Autoload
@@ -23,12 +26,8 @@ module BootstrapForm
23
26
  BootstrapForm::Inputs.eager_load!
24
27
  end
25
28
 
26
- def config
27
- @config ||= BootstrapForm::Configuration.new
28
- end
29
-
30
- def configure
31
- yield config
29
+ def deprecator
30
+ @deprecator ||= ActiveSupport::Deprecation.new("a future release", "BootstrapForm")
32
31
  end
33
32
  end
34
33
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootstrap_form
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.0
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Potenza
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2025-10-30 00:00:00.000000000 Z
12
+ date: 2026-01-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -53,7 +53,6 @@ files:
53
53
  - ".github/workflows/ruby.yml"
54
54
  - ".gitignore"
55
55
  - ".rubocop.yml"
56
- - ".yarnrc"
57
56
  - CHANGELOG.md
58
57
  - CODE_OF_CONDUCT.md
59
58
  - CONTRIBUTING.md
data/.yarnrc DELETED
@@ -1,5 +0,0 @@
1
- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
- # yarn lockfile v1
3
-
4
-
5
- lastUpdateCheck 1761786460258