bootstrap_form 4.0.0.alpha1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eb7cf943e5416aff7a98a51666bee424be65eec12c55244a327c88d21a6d68d
4
- data.tar.gz: 1ef48e511095bbab5bbef27ec4542019872f25fd5504834e51b1a253fd6dc1bf
3
+ metadata.gz: 02eeb0d89ce2c25d6c38aba37ec7ea381e8110ea31ac771cde6a0f92dc36823e
4
+ data.tar.gz: 3b8448de1c213e51e9a5b34b4c79328318297f0ee30b724f73a65d79f871bb70
5
5
  SHA512:
6
- metadata.gz: 4022101ea8932a6ad3198cb9b517e65db477203633cbc31b9db4cb554ebf90cb73745ba4fe4f80dd3a5aca272a5ee92d18fc8955d391ece5e7eb6899f43600af
7
- data.tar.gz: 5536da85294e55e980ed267576f84433e3adca060ecaad50594b7e600b9bb5d49a414ab75c559bb841e04f0ac1f336b24a64a0aaa05501f9e947e8e138a7d2cc
6
+ metadata.gz: 6abcc632e3ccda2b53d839b32cf8f1189a0eb8ccdff59864de232ebfee82df795ed77530bb7de10f12b2de0fecf75a318845030159b73a17e87aedc087150474
7
+ data.tar.gz: e111e67482e8ed21d54e441d82be365658aad122306d50571d0ae95e68cb7bb344af8906b4b35774a60cafef5757efe6566c642b795e20c3277ab4d23db9467d
@@ -12,6 +12,32 @@
12
12
 
13
13
  * Your contribution here!
14
14
 
15
+ ## [4.0.0][] (2018-10-27)
16
+
17
+ 🚨 **This release adds support for Bootstrap v4 and drops support for Bootstrap v3.** 🚨
18
+
19
+ If your app uses Bootstrap v3, you should continue using bootstrap_form 2.7.x instead.
20
+
21
+ Bootstrap v3 and v4 are very different, and thus bootstrap_form now produces different markup in order to target v4. The changes are too many to list here; you can refer to Bootstrap's [Migrating to v4](https://getbootstrap.com/docs/4.0/migration/) page for a detailed explanation.
22
+
23
+ In addition to these necessary markup changes, the bootstrap_form API itself has the following important changes in this release.
24
+
25
+ ### Breaking changes
26
+
27
+ * See [Migrating to v4](https://getbootstrap.com/docs/4.0/migration/).
28
+
29
+ ### New features
30
+
31
+ * [#476] Give a way to pass classes to the `div.form-check` wrapper for check boxes and radio buttons - [@lcreid](https://github.com/lcreid).
32
+ * [461](https://github.com/bootstrap-ruby/bootstrap_form/pull/461): default form-inline class applied to parent content div on date select helpers. Can override with a :skip_inline option on the field helper - [@lancecarlson](https://github.com/lancecarlson).
33
+ * The `button`, `submit`, and `primary` helpers can now receive an additional option, `extra_class`. This option allows us to specify additional CSS classes to be added to the corresponding button/input, _while_ maintaining the original default ones. E.g., a primary button with an `extra_class` 'test-button' will have its final CSS classes declaration as 'btn btn-primary test-button'.
34
+
35
+ ### Bugfixes
36
+
37
+ * [#347](https://github.com/bootstrap-ruby/bootstrap_form/issues/347) Fix `wrapper_class` and `wrapper` options for helpers that have `html_options`.
38
+ * [#472](https://github.com/bootstrap-ruby/bootstrap_form/pull/472) Use `id` option value as `for` attribute of label for custom checkboxes and radio buttons.
39
+ * [#478](https://github.com/bootstrap-ruby/bootstrap_form/issues/478) Fix offset for form group without label when multiple label widths are specified.
40
+
15
41
 
16
42
  ## [4.0.0.alpha1][] (2018-06-16)
17
43
 
@@ -49,6 +75,7 @@ In addition to these necessary markup changes, the bootstrap_form API itself has
49
75
  * [#449](https://github.com/bootstrap-ruby/bootstrap_form/pull/449): Passing `.form-row` overrides default `.form-group.row` in horizontal layouts.
50
76
  * Added an option to the `submit` (and `primary`, by transitivity) form tag helper, `render_as_button`, which when truthy makes the submit button render as a button instead of an input. This allows you to easily provide further styling to your form submission buttons, without requiring you to reinvent the wheel and use the `button` helper (and having to manually insert the typical Bootstrap classes). - [@jsaraiva](https://github.com/jsaraiva).
51
77
  * Add `:error_message` option to `check_box` and `radio_button`, so they can output validation error messages if needed. [@lcreid](https://github.com/lcreid).
78
+ * Your contribution here!
52
79
 
53
80
  ### Bugfixes
54
81
 
@@ -216,7 +243,8 @@ Features:
216
243
  - Added support for bootstrap_form_tag (@baldwindavid)
217
244
 
218
245
 
219
- [Pending Release]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v4.0.0.alpha1...HEAD
246
+ [Pending Release]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v4.0.0...HEAD
247
+ [4.0.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v4.0.0.alpha1...v4.0.0
220
248
  [4.0.0.alpha1]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.7.0...v4.0.0.alpha1
221
249
  [2.7.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.6.0...v2.7.0
222
250
  [2.6.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.5.3...v2.6.0
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
  # gem "rails", "~> 5.2.0.beta2"
7
7
 
8
8
  group :development do
9
+ gem "chandler", ">= 0.7.0"
9
10
  gem "htmlbeautifier"
10
11
  end
11
12
 
data/README.md CHANGED
@@ -21,7 +21,7 @@ Bootstrap v4-style forms into your Rails application.
21
21
  Add it to your Gemfile:
22
22
 
23
23
  ```ruby
24
- gem "bootstrap_form", ">= 4.0.0.alpha1"
24
+ gem "bootstrap_form", ">= 4.0.0"
25
25
  ```
26
26
 
27
27
  Then:
@@ -348,9 +348,15 @@ To display checkboxes and radios inline, pass the `inline: true` option:
348
348
  <% end %>
349
349
  ```
350
350
 
351
+ Check boxes and radio buttons are wrapped in a `div.form-check`. You can add classes to this `div` with the `:wrapper_class` option:
352
+
353
+ ```erb
354
+ <%= f.radio_button :skill_level, 0, label: "Novice", inline: true, wrapper_class: "w-auto" %>
355
+ ```
356
+
351
357
  #### Collections
352
358
 
353
- `bootstrap_form` also provides helpers that automatically creates the
359
+ `bootstrap_form` also provides helpers that automatically create the
354
360
  `form_group` and the `radio_button`s or `check_box`es for you:
355
361
 
356
362
  ```erb
@@ -401,7 +407,7 @@ this defining these selects as `inline-block` and a width of `auto`.
401
407
 
402
408
  ### Submit Buttons
403
409
 
404
- The `btn btn-secondary` css classes are automatically added to your submit
410
+ The `btn btn-secondary` CSS classes are automatically added to your submit
405
411
  buttons.
406
412
 
407
413
  ```erb
@@ -441,6 +447,29 @@ are equivalent, and each of them both be rendered as
441
447
  <button name="button" type="submit" class="btn btn-primary">Save changes <span class="fa fa-save"></span></button>
442
448
  ```
443
449
 
450
+ If you wish to add additional CSS classes to your button, while keeping the
451
+ default ones, you can use the `extra_class` option. This is particularly useful
452
+ for adding extra details to buttons (without forcing you to repeat the
453
+ Bootstrap classes), or for element targeting via CSS classes.
454
+ Be aware, however, that using the `class` option will discard any extra classes
455
+ you add. As an example, the following button declarations
456
+
457
+ ```erb
458
+ <%= f.primary "My Nice Button", extra_class: 'my-button' %>
459
+
460
+ <%= f.primary "My Button", class: 'my-button' %>
461
+ ```
462
+
463
+ will be rendered as
464
+
465
+ ```html
466
+ <input type="submit" value="My Nice Button" class="btn btn-primary my-button" />
467
+
468
+ <input type="submit" value="My Button" class="my-button" />
469
+ ```
470
+
471
+ (some unimportant HTML attributes have been removed for simplicity)
472
+
444
473
  ### Accessing Rails Form Helpers
445
474
 
446
475
  If you want to use the original Rails form helpers for a particular field,
@@ -0,0 +1,25 @@
1
+ # Releasing
2
+
3
+ Follow these steps to release a new version of bootstrap_form to rubygems.org.
4
+
5
+ ## Prerequisites
6
+
7
+ * You must have commit rights to the bootstrap_form repository.
8
+ * You must have push rights for the bootstrap_form gem on rubygems.org.
9
+ * You must be using Ruby >= 2.2.
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).
11
+
12
+ ## How to release
13
+
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`.**
16
+ 3. Determine which would be the correct next version number according to [semver](http://semver.org/).
17
+ 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
23
+ 6. Update the installation instructions in `README.md` to use the new version.
24
+ 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).
data/Rakefile CHANGED
@@ -24,4 +24,10 @@ Rake::TestTask.new(:test) do |t|
24
24
  t.verbose = false
25
25
  end
26
26
 
27
+ # This automatically updates GitHub Releases whenever we `rake release` the gem
28
+ task "release:rubygem_push" do
29
+ require "chandler/tasks"
30
+ Rake.application.invoke_task("chandler:push")
31
+ end
32
+
27
33
  task default: :test
@@ -53,7 +53,11 @@ module BootstrapForm
53
53
 
54
54
  define_method(with_method_name) do |name, options = {}, html_options = {}|
55
55
  form_group_builder(name, options, html_options) do
56
- content_tag(:div, class: control_specific_class(method_name)) do
56
+ html_class = control_specific_class(method_name)
57
+ if @layout == :horizontal && !options[:skip_inline].present?
58
+ html_class = "#{html_class} form-inline"
59
+ end
60
+ content_tag(:div, class: html_class) do
57
61
  input_with_error(name) do
58
62
  send(without_method_name, name, options, html_options)
59
63
  end
@@ -126,14 +130,24 @@ module BootstrapForm
126
130
 
127
131
  def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
128
132
  options = options.symbolize_keys!
129
- check_box_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label)
133
+ check_box_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label, :wrapper_class)
130
134
  check_box_classes = [check_box_options[:class]]
131
135
  check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
132
136
  check_box_classes << "is-invalid" if has_error?(name)
137
+
138
+ label_classes = [options[:label_class]]
139
+ label_classes << hide_class if options[:hide_label]
140
+
133
141
  if options[:custom]
134
142
  check_box_options[:class] = (["custom-control-input"] + check_box_classes).compact.join(' ')
143
+ wrapper_class = ["custom-control", "custom-checkbox"]
144
+ wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
145
+ label_class = label_classes.prepend("custom-control-label").compact.join(" ")
135
146
  else
136
147
  check_box_options[:class] = (["form-check-input"] + check_box_classes).compact.join(' ')
148
+ wrapper_class = ["form-check"]
149
+ wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
150
+ label_class = label_classes.prepend("form-check-label").compact.join(" ")
137
151
  end
138
152
 
139
153
  checkbox_html = check_box_without_bootstrap(name, check_box_options, checked_value, unchecked_value)
@@ -149,39 +163,19 @@ module BootstrapForm
149
163
  "#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s}"
150
164
  end
151
165
 
152
- label_classes = [options[:label_class]]
153
- label_classes << hide_class if options[:hide_label]
166
+ label_options = { class: label_class }
167
+ label_options[:for] = options[:id] if options[:id].present?
154
168
 
155
- if options[:custom]
156
- div_class = ["custom-control", "custom-checkbox"]
157
- div_class.append("custom-control-inline") if layout_inline?(options[:inline])
158
- label_class = label_classes.prepend("custom-control-label").compact.join(" ")
159
- content_tag(:div, class: div_class.compact.join(" ")) do
160
- html = if options[:skip_label]
161
- checkbox_html
162
- else
163
- # TODO: Notice we don't seem to pass the ID into the custom control.
164
- checkbox_html.concat(label(label_name, label_description, class: label_class))
165
- end
166
- html.concat(generate_error(name)) if options[:error_message]
167
- html
168
- end
169
- else
170
- wrapper_class = "form-check"
171
- wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
172
- label_class = label_classes.prepend("form-check-label").compact.join(" ")
173
- content_tag(:div, class: wrapper_class) do
174
- html = if options[:skip_label]
175
- checkbox_html
176
- else
177
- checkbox_html
178
- .concat(label(label_name,
179
- label_description,
180
- { class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
181
- end
182
- html.concat(generate_error(name)) if options[:error_message]
183
- html
169
+ wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
170
+
171
+ content_tag(:div, class: wrapper_class.compact.join(" ")) do
172
+ html = if options[:skip_label]
173
+ checkbox_html
174
+ else
175
+ checkbox_html.concat(label(label_name, label_description, label_options))
184
176
  end
177
+ html.concat(generate_error(name)) if options[:error_message]
178
+ html
185
179
  end
186
180
  end
187
181
 
@@ -189,49 +183,41 @@ module BootstrapForm
189
183
 
190
184
  def radio_button_with_bootstrap(name, value, *args)
191
185
  options = args.extract_options!.symbolize_keys!
192
- radio_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label)
186
+ radio_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label, :wrapper_class)
193
187
  radio_classes = [options[:class]]
194
188
  radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
195
189
  radio_classes << "is-invalid" if has_error?(name)
196
- if options[:custom]
197
- radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(' ')
198
- else
199
- radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(' ')
200
- end
201
- radio_html = radio_button_without_bootstrap(name, value, radio_options)
202
190
 
203
- disabled_class = " disabled" if options[:disabled]
204
191
  label_classes = [options[:label_class]]
205
192
  label_classes << hide_class if options[:hide_label]
206
193
 
207
194
  if options[:custom]
208
- div_class = ["custom-control", "custom-radio"]
209
- div_class.append("custom-control-inline") if layout_inline?(options[:inline])
195
+ radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(' ')
196
+ wrapper_class = ["custom-control", "custom-radio"]
197
+ wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
210
198
  label_class = label_classes.prepend("custom-control-label").compact.join(" ")
211
- content_tag(:div, class: div_class.compact.join(" ")) do
212
- html = if options[:skip_label]
213
- radio_html
214
- else
215
- # TODO: Notice we don't seem to pass the ID into the custom control.
216
- radio_html.concat(label(name, options[:label], value: value, class: label_class))
217
- end
218
- html.concat(generate_error(name)) if options[:error_message]
219
- html
220
- end
221
199
  else
222
- wrapper_class = "form-check"
223
- wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
200
+ radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(' ')
201
+ wrapper_class = ["form-check"]
202
+ wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
203
+ wrapper_class.append("disabled") if options[:disabled]
224
204
  label_class = label_classes.prepend("form-check-label").compact.join(" ")
225
- content_tag(:div, class: "#{wrapper_class}#{disabled_class}") do
226
- html = if options[:skip_label]
227
- radio_html
228
- else
229
- radio_html
230
- .concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
231
- end
232
- html.concat(generate_error(name)) if options[:error_message]
233
- html
205
+ end
206
+ radio_html = radio_button_without_bootstrap(name, value, radio_options)
207
+
208
+ label_options = { value: value, class: label_class }
209
+ label_options[:for] = options[:id] if options[:id].present?
210
+
211
+ wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
212
+
213
+ content_tag(:div, class: wrapper_class.compact.join(" ")) do
214
+ html = if options[:skip_label]
215
+ radio_html
216
+ else
217
+ radio_html.concat(label(name, options[:label], label_options))
234
218
  end
219
+ html.concat(generate_error(name)) if options[:error_message]
220
+ html
235
221
  end
236
222
  end
237
223
 
@@ -340,7 +326,7 @@ module BootstrapForm
340
326
  end
341
327
 
342
328
  def offset_col(label_col)
343
- label_col.sub(/^col-(\w+)-(\d)$/, 'offset-\1-\2')
329
+ label_col.gsub(/\bcol-(\w+)-(\d)\b/, 'offset-\1-\2')
344
330
  end
345
331
 
346
332
  def default_control_col
@@ -392,6 +378,10 @@ module BootstrapForm
392
378
 
393
379
  def form_group_builder(method, options, html_options = nil)
394
380
  options.symbolize_keys!
381
+
382
+ wrapper_class = options.delete(:wrapper_class)
383
+ wrapper_options = options.delete(:wrapper)
384
+
395
385
  html_options.symbolize_keys! if html_options
396
386
 
397
387
  # Add control_class; allow it to be overridden by :control_class option
@@ -402,8 +392,6 @@ module BootstrapForm
402
392
 
403
393
  options = convert_form_tag_options(method, options) if acts_like_form_tag
404
394
 
405
- wrapper_class = css_options.delete(:wrapper_class)
406
- wrapper_options = css_options.delete(:wrapper)
407
395
  help = options.delete(:help)
408
396
  icon = options.delete(:icon)
409
397
  label_col = options.delete(:label_col)
@@ -1,18 +1,19 @@
1
1
  module BootstrapForm
2
2
  module Helpers
3
3
  module Bootstrap
4
+
4
5
  def button(value = nil, options = {}, &block)
5
- options.reverse_merge! class: 'btn btn-secondary'
6
+ setup_css_class 'btn btn-secondary', options
6
7
  super
7
8
  end
8
9
 
9
10
  def submit(name = nil, options = {})
10
- options.reverse_merge! class: 'btn btn-secondary'
11
+ setup_css_class 'btn btn-secondary', options
11
12
  super
12
13
  end
13
14
 
14
15
  def primary(name = nil, options = {}, &block)
15
- options.reverse_merge! class: 'btn btn-primary'
16
+ setup_css_class 'btn btn-primary', options
16
17
 
17
18
  if options[:render_as_button] || block_given?
18
19
  options.except! :render_as_button
@@ -34,9 +35,11 @@ module BootstrapForm
34
35
  end
35
36
 
36
37
  def error_summary
37
- content_tag :ul, class: 'rails-bootstrap-forms-error-summary' do
38
- object.errors.full_messages.each do |error|
39
- concat content_tag(:li, error)
38
+ if object.errors.any?
39
+ content_tag :ul, class: 'rails-bootstrap-forms-error-summary' do
40
+ object.errors.full_messages.each do |error|
41
+ concat content_tag(:li, error)
42
+ end
40
43
  end
41
44
  end
42
45
  end
@@ -64,7 +67,7 @@ module BootstrapForm
64
67
  control_class: [options[:control_class], static_class].compact.join(" ")
65
68
  })
66
69
 
67
- static_options[:value] = object.send(name) if static_options[:value].nil?
70
+ static_options[:value] = object.send(name) unless static_options.has_key?(:value)
68
71
 
69
72
  text_field_with_bootstrap(name, static_options)
70
73
  end
@@ -102,6 +105,19 @@ module BootstrapForm
102
105
  def static_class
103
106
  "form-control-plaintext"
104
107
  end
108
+
109
+
110
+ private
111
+
112
+ def setup_css_class(the_class, options = {})
113
+ unless options.has_key? :class
114
+ if extra_class = options.delete(:extra_class)
115
+ the_class = "#{the_class} #{extra_class}"
116
+ end
117
+ options[:class] = the_class
118
+ end
119
+ end
120
+
105
121
  end
106
122
  end
107
123
  end
@@ -1,3 +1,3 @@
1
1
  module BootstrapForm
2
- VERSION = "4.0.0.alpha1".freeze
2
+ VERSION = "4.0.0".freeze
3
3
  end
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: 4.0.0.alpha1
4
+ version: 4.0.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: 2018-06-16 00:00:00.000000000 Z
12
+ date: 2018-10-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -42,6 +42,7 @@ files:
42
42
  - Gemfile
43
43
  - LICENSE.txt
44
44
  - README.md
45
+ - RELEASING.md
45
46
  - Rakefile
46
47
  - UPGRADE-4.0.md
47
48
  - app/assets/stylesheets/rails_bootstrap_forms.css
@@ -109,12 +110,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
110
  version: 2.2.2
110
111
  required_rubygems_version: !ruby/object:Gem::Requirement
111
112
  requirements:
112
- - - ">"
113
+ - - ">="
113
114
  - !ruby/object:Gem::Version
114
- version: 1.3.1
115
+ version: '0'
115
116
  requirements: []
116
117
  rubyforge_project:
117
- rubygems_version: 2.7.7
118
+ rubygems_version: 2.7.6
118
119
  signing_key:
119
120
  specification_version: 4
120
121
  summary: Rails form builder that makes it easy to style forms using Bootstrap 4