diff --git a/.gitignore b/.gitignore
index 8540dd815..385f6a529 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,14 @@
.bundle/
log/*.log
pkg/
-test/dummy/db/*.sqlite3
-test/dummy/log/*.log
-test/dummy/tmp/
+demo/db/*.sqlite3
+demo/log/*.log
+demo/tmp/
*.gem
.rbenv-gemsets
*.swp
Gemfile.lock
+test/gemfiles/*.lock
.ruby-version
+Vagrantfile
+.vagrant
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa41ccda6..786ff6855 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1,73 @@
## [Pending Release][]
-Bugfixes:
- - Your contribution here!
+### Breaking changes
-Features:
- - Your contribution here!
- * [#344](https://github.com/bootstrap-ruby/rails-bootstrap-forms/pull/344): Allow HTML in help translations by using the '_html' suffix on the key - [@unikitty37](https://github.com/unikitty37)
- * [#325](https://github.com/bootstrap-ruby/rails-bootstrap-forms/pull/325): Support :prepend and :append for the `select` helper - [@donv](https://github.com/donv).
+* Your contribution here!
+
+### New features
+
+* [#476] Give a way to pass classes to the `div.form-check` wrapper for check boxes and radio buttons - [@lcreid](https://github.com/lcreid).
+* [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).
+* Your contribution here!
+* 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'.
+
+### Bugfixes
+
+* Your contribution here!
+* [#347](https://github.com/bootstrap-ruby/bootstrap_form/issues/347) Fix `wrapper_class` and `wrapper` options for helpers that have `html_options`.
+* [#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.
+* [#478](https://github.com/bootstrap-ruby/bootstrap_form/issues/478) Fix offset for form group without label when multiple label widths are specified.
+
+
+## [4.0.0.alpha1][] (2018-06-16)
+
+🚨 **This release adds support for Bootstrap v4 and drops support for Bootstrap v3.** 🚨
+
+If your app uses Bootstrap v3, you should continue using bootstrap_form 2.7.x instead.
+
+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.
+
+In addition to these necessary markup changes, the bootstrap_form API itself has the following important changes in this release.
+
+### Breaking changes
+
+* Rails 4.x is no longer supported
+* Ruby 2.2 or newer is required
+* Built-in support for the `nested_form` gem has been completely removed
+* The `icon` option is no longer supported (Bootstrap v4 does not include icons)
+* The deprecated Rails methods `check_boxes_collection` and `radio_buttons_collection` have been removed
+* `hide_label: true` and `skip_label: true` on individual check boxes and radio buttons apply Bootstrap 4 markup. This means the appearance of a page may change if you're upgrading from the Bootstrap 3 version of `bootstrap_form`, and you used `check_box` or `radio_button` with either of those options
+* `static_control` will no longer properly show error messages. This is the result of bootstrap changes.
+* `static_control` will also no longer accept a block, use the `value` option instead.
+* `form_group` with a block that produces arbitrary text needs to be modified to produce validation error messages (see the UPGRADE-4.0 document). [@lcreid](https://github.com/lcreid).
+* `form_group` with a block that contains more than one `check_box` or `radio_button` needs to be modified to produce validation error messages (see the UPGRADE-4.0 document). [@lcreid](https://github.com/lcreid).
+* [#456](https://github.com/bootstrap-ruby/bootstrap_form/pull/456): Fix label `for` attribute when passing non-english characters using `collection_check_boxes` - [@ug0](https://github.com/ug0).
+* [#449](https://github.com/bootstrap-ruby/bootstrap_form/pull/449): Bootstrap 4 no longer mixes in `.row` in `.form-group`. `bootstrap_form` adds `.row` to `div.form-group` when layout is horizontal.
+
+### New features
+
+* Support for Rails 5.1 `form_with` - [@lcreid](https://github.com/lcreid).
+* Support Bootstrap v4's [Custom Checkboxes and Radios](https://getbootstrap.com/docs/4.0/components/forms/#checkboxes-and-radios-1) with a new `custom: true` option
+* Allow HTML in help translations by using the `_html` suffix on the key - [@unikitty37](https://github.com/unikitty37)
+* [#408](https://github.com/bootstrap-ruby/bootstrap_form/pull/408): Add option[:id] on static control #245 - [@duleorlovic](https://github.com/duleorlovic).
+* [#455](https://github.com/bootstrap-ruby/bootstrap_form/pull/455): Support for i18n `:html` subkeys in help text - [@jsaraiva](https://github.com/jsaraiva).
+* Adds support for `label_as_placeholder` option, which will set the label text as an input fields placeholder (and hiding the label for sr_only).
+* [#449](https://github.com/bootstrap-ruby/bootstrap_form/pull/449): Passing `.form-row` overrides default `.form-group.row` in horizontal layouts.
+* 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).
+* Add `:error_message` option to `check_box` and `radio_button`, so they can output validation error messages if needed. [@lcreid](https://github.com/lcreid).
+* Your contribution here!
+
+### Bugfixes
+
+* [#357](https://github.com/bootstrap-ruby/bootstrap_form/pull/357) if provided,
+ use html option `id` to specify `for` attribute on label
+ [@duleorlovic](https://github.com/duleorlovic)
## [2.7.0][] (2017-04-21)
Features:
- * [#325](https://github.com/bootstrap-ruby/rails-bootstrap-forms/pull/325): Support :prepend and :append for the `select` helper - [@donv](https://github.com/donv).
+ * [#325](https://github.com/bootstrap-ruby/bootstrap_form/pull/325): Support :prepend and :append for the `select` helper - [@donv](https://github.com/donv).
## [2.6.0][] (2017-02-03)
@@ -20,7 +75,7 @@ Bugfixes:
- Fix ambiguous first argument warning (#311, @mikenicklas)
Features:
- - Add a FormBuilder#custom_control helper [#289](https://github.com/bootstrap-ruby/rails-bootstrap-forms/pull/289)
+ - Add a FormBuilder#custom_control helper [#289](https://github.com/bootstrap-ruby/bootstrap_form/pull/289)
## [2.5.3][] (2016-12-23)
@@ -35,7 +90,7 @@ Bugfixes:
## [2.5.1][] (2016-09-23)
Bugfixes:
- - Fix getting help text for elements when using anonymous models (see [issue 282](https://github.com/bootstrap-ruby/rails-bootstrap-forms/issues/282))
+ - Fix getting help text for elements when using anonymous models (see [issue 282](https://github.com/bootstrap-ruby/bootstrap_form/issues/282))
## [2.5.0][] (2016-08-12)
@@ -128,7 +183,7 @@ Features:
## 2.1.0 (2014-04-01)
Moved GitHub repository from https://github.com/potenza/bootstrap_form
-to https://github.com/bootstrap-ruby/rails-bootstrap-forms
+to https://github.com/bootstrap-ruby/bootstrap_form
Bugfixes:
@@ -168,11 +223,12 @@ Features:
- Added support for bootstrap_form_tag (@baldwindavid)
-[Pending Release]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.7.0...HEAD
-[2.7.0]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.6.0...v2.7.0
-[2.6.0]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.5.3...v2.6.0
-[2.5.3]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.5.2...v2.5.3
-[2.5.2]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.5.1...v2.5.2
-[2.5.1]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.5.0...v2.5.1
-[2.5.0]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.4.0...v2.5.0
-[2.4.0]: https://github.com/bootstrap-ruby/rails-bootstrap-forms/compare/v2.3.0...v2.4.0
+[Pending Release]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v4.0.0.alpha1...HEAD
+[4.0.0.alpha1]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.7.0...v4.0.0.alpha1
+[2.7.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.6.0...v2.7.0
+[2.6.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.5.3...v2.6.0
+[2.5.3]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.5.2...v2.5.3
+[2.5.2]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.5.1...v2.5.2
+[2.5.1]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.5.0...v2.5.1
+[2.5.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.4.0...v2.5.0
+[2.4.0]: https://github.com/bootstrap-ruby/bootstrap_form/compare/v2.3.0...v2.4.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 62a02439b..86ae3e04b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@ The release of Bootstrap 4 and Rails 5.1 have implications for the future
direction of `bootstrap_form`. Don't worry. We plan to move this gem forward to
Bootstrap 4 and to support Rails 5.1 and beyond. If you're thinking of
contributing to `bootstrap_form`, please read
-[issue #361](https://github.com/bootstrap-ruby/rails-bootstrap-forms/issues/361).
+[issue #361](https://github.com/bootstrap-ruby/bootstrap_form/issues/361).
Your comments are welcome.
@@ -35,9 +35,9 @@ For the project. Optionally, create a branch you want to work on
- Update the README if necessary.
- Add a line to the CHANGELOG for your bug fix or feature.
-You may find using test/dummy application useful for development and debugging.
+You may find using demo application useful for development and debugging.
-- `cd test/dummy`
+- `cd demo`
- `rake db:schema:load`
- `rails s`
- Navigate to http://localhost:3000
@@ -50,8 +50,8 @@ You may find using test/dummy application useful for development and debugging.
### 6. Done!
Somebody will shortly review your pull request and if everything is good will be
-merged into master brach. Eventually gem will be published with your changes.
+merged into master branch. Eventually gem will be published with your changes.
---
-Thanks to all the great contributors over the years: https://github.com/bootstrap-ruby/rails-bootstrap-forms/graphs/contributors
+Thanks to all the great contributors over the years: https://github.com/bootstrap-ruby/bootstrap_form/graphs/contributors
diff --git a/Dangerfile b/Dangerfile
index bfd111356..8d8ef8d4f 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -45,7 +45,7 @@ end
# Did you remove the CHANGELOG's "Your contribution here!" line?
# ------------------------------------------------------------------------------
if has_changelog_changes
- if IO.read("CHANGELOG.md").scan(/^\s*- Your contribution here/i).count < 2
+ if IO.read("CHANGELOG.md").scan(/^\s*[-\*] Your contribution here/i).count < 3
fail(
"Please put the `- Your contribution here!` line back into CHANGELOG.md.",
:sticky => false
diff --git a/Gemfile b/Gemfile
index 1112f4f7b..1d8c858f5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,15 +6,17 @@ gemspec
# gem "rails", "~> 5.2.0.beta2"
group :development do
+ gem "chandler", ">= 0.7.0"
gem "htmlbeautifier"
end
group :test do
+ # can relax version requirement for Rails 5.2.beta3+
+ gem "minitest", "~> 5.10.3"
+
gem "diffy"
gem "equivalent-xml"
gem "mocha"
gem "sqlite3"
gem "timecop", "~> 0.7.1"
end
-
-gem "minitest", "~> 5.10.3"
diff --git a/README.md b/README.md
index a6102cc70..b0713c5a6 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-⚠️ **This documentation is for the master branch, which is not yet stable and targets Bootstrap v4.** If you are using Bootstrap v3, refer to the stable [legacy-2.7](https://github.com/bootstrap-ruby/rails-bootstrap-forms/tree/legacy-2.7) branch.
+⚠️ **This documentation is for the master branch, which is not yet stable and targets Bootstrap v4.** If you are using Bootstrap v3, refer to the stable [legacy-2.7](https://github.com/bootstrap-ruby/bootstrap_form/tree/legacy-2.7) branch.
---
# bootstrap_form
-[![Build Status](https://travis-ci.org/bootstrap-ruby/rails-bootstrap-forms.svg?branch=master)](https://travis-ci.org/bootstrap-ruby/rails-bootstrap-forms)
+[![Build Status](https://travis-ci.org/bootstrap-ruby/bootstrap_form.svg?branch=master)](https://travis-ci.org/bootstrap-ruby/bootstrap_form)
[![Gem Version](https://badge.fury.io/rb/bootstrap_form.svg)](https://rubygems.org/gems/bootstrap_form)
**bootstrap_form** is a Rails form builder that makes it super easy to integrate
@@ -12,18 +12,16 @@ Bootstrap v4-style forms into your Rails application.
## Requirements
-* Ruby 2.3+
-* Rails 5.0+
-* Bootstrap 4.0.0-beta.3+
+* Ruby 2.2.2+
+* Rails 5.0+ (Rails 5.1+ for `bootstrap_form_with`)
+* Bootstrap 4.0.0+
## Installation
Add it to your Gemfile:
```ruby
-gem "bootstrap_form",
- git: "https://github.com/bootstrap-ruby/rails-bootstrap-forms.git",
- branch: "master"
+gem "bootstrap_form", ">= 4.0.0.alpha1"
```
Then:
@@ -64,20 +62,14 @@ This generates the following HTML:
-
+
+
+
```
-### Nested Forms
-
-In order to active [nested_form](https://github.com/ryanb/nested_form) support,
-use `bootstrap_nested_form_for` instead of `bootstrap_form_for`.
-
### bootstrap_form_tag
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:
@@ -89,6 +81,53 @@ If your form is not backed by a model, use the `bootstrap_form_tag`. Usage of th
<% end %>
```
+### `bootstrap_form_with` (Rails 5.1+)
+
+Note that `form_with` in Rails 5.1 does not add IDs to form elements and labels by default, which are both important to Bootstrap markup. This behavior is corrected in Rails 5.2.
+
+To get started, just use the `bootstrap_form_with` helper in place of `form_with`. Here's an example:
+
+```erb
+<%= bootstrap_form_with(model: @user, local: true) do |f| %>
+ <%= f.email_field :email %>
+ <%= f.password_field :password %>
+ <%= f.check_box :remember_me %>
+ <%= f.submit "Log In" %>
+<% end %>
+```
+
+This generates:
+
+```html
+
+```
+
+`bootstrap_form_with` supports both the `model:` and `url:` use cases
+in `form_with`.
+
+`form_with` has some important differences compared to `form_for` and `form_tag`, and these differences apply to `bootstrap_form_with`. A good summary of the differences can be found at: https://m.patrikonrails.com/rails-5-1s-form-with-vs-old-form-helpers-3a5f72a8c78a, or in the [Rails documentation](api.rubyonrails.org).
+
+### Future Compatibility
+
+The Rails team has [suggested](https://github.com/rails/rails/issues/25197) that `form_for` and `form_tag` may be deprecated and then removed in future versions of Rails. `bootstrap_form` will continue to support `bootstrap_form_for` and `bootstrap_form_tag` as long as Rails supports `form_for` and `form_tag`.
+
## Form Helpers
This gem wraps the following Rails form helpers:
@@ -123,6 +162,8 @@ This gem wraps the following Rails form helpers:
* time_zone_select
* url_field
* week_field
+* submit
+* button
These helpers accept the same options as the standard Rails form helpers, with
a few extra options:
@@ -148,6 +189,12 @@ To add custom classes to the field's label:
<%= f.text_field :email, label_class: "custom-class" %>
```
+Or you can add the label as input placeholder instead (this automatically hides the label):
+
+```erb
+<%= f.text_field :email, label_as_placeholder: true %>
+```
+
#### Required Fields
A label that is associated with a required field is automatically annotated with
@@ -174,7 +221,7 @@ In cases where this behavior is undesirable, use the `skip_required` option:
### Input Elements / Controls
-To specify the class of the generated input, use the `control_class` option:
+To specify the class of the generated input tag, use the `control_class` option:
```erb
<%= f.text_field :email, control_class: "custom-class" %>
@@ -206,7 +253,7 @@ en:
help:
user:
password_html: "A good password should be at least six characters long"
-```
+```
If your model name has multiple words (like `SuperUser`), the key on the
translation file should be underscored (`super_user`).
@@ -214,25 +261,6 @@ translation file should be underscored (`super_user`).
You can override help translations for a particular field by passing the `help`
option or turn them off completely by passing `help: false`.
-### Icons
-
-To add an icon to a field, use the `icon` option and pass the icon name
-*without* the 'glyphicon' prefix:
-
-```erb
-<%= f.text_field :login, icon: "user" %>
-```
-
-This automatically adds the `has-feedback` class to the `form-group`:
-
-```html
-
-
-
-
-
-```
-
### Prepending and Appending Inputs
You can pass `prepend` and/or `append` options to input fields:
@@ -248,7 +276,7 @@ You can also prepend and append buttons. Note: The buttons must contain the
<%= f.text_field :search, append: link_to("Go", "#", class: "btn btn-secondary") %>
```
-To add a class to the input group wrapper, use `:input_group_class` option.
+To add a class to the input group wrapper, use the `:input_group_class` option.
```erb
<%= f.email_field :email, append: f.primary('Subscribe'), input_group_class: 'input-group-lg' %>
@@ -320,9 +348,15 @@ To display checkboxes and radios inline, pass the `inline: true` option:
<% end %>
```
+Check boxes and radio buttons are wrapped in a `div.form-check`. You can add classes to this `div` with the `:wrapper_class` option:
+
+```erb
+<%= f.radio_button :skill_level, 0, label: "Novice", inline: true, wrapper_class: "w-auto" %>
+```
+
#### Collections
-BootstrapForms also provides helpers that automatically creates the
+`bootstrap_form` also provides helpers that automatically create the
`form_group` and the `radio_button`s or `check_box`es for you:
```erb
@@ -344,13 +378,13 @@ You can create a static control like this:
<%= f.static_control :email %>
```
-Here's the output:
+Here's the output for a horizontal layout:
```html
-
test@email.com
+
```
@@ -368,12 +402,12 @@ You can also create a static control that isn't based on a model attribute:
The multiple selects that the date and time helpers (`date_select`,
`time_select`, `datetime_select`) generate are wrapped inside a
`div.rails-bootstrap-forms-[date|time|datetime]-select` tag. This is because
-Bootstrap automatically stylizes ours controls as `block`s. This wrapper fix
+Bootstrap automatically styles our controls as `block`s. This wrapper fixes
this defining these selects as `inline-block` and a width of `auto`.
### Submit Buttons
-The `btn btn-secondary` css classes are automatically added to your submit
+The `btn btn-secondary` CSS classes are automatically added to your submit
buttons.
```erb
@@ -393,6 +427,49 @@ You can specify your own classes like this:
<%= f.submit "Log In", class: "btn btn-success" %>
```
+If the `primary` helper receives a `render_as_button: true` option or a block,
+it will be rendered as an HTML button, instead of an input tag. This allows you
+to specify HTML content and styling for your buttons (such as adding
+illustrative icons to them). For example, the following statements
+
+```erb
+<%= f.primary "Save changes ".html_safe, render_as_button: true %>
+
+<%= f.primary do
+ concat 'Save changes '
+ concat content_tag(:span, nil, class: 'fa fa-save')
+ end %>
+```
+
+are equivalent, and each of them both be rendered as
+
+```html
+
+```
+
+If you wish to add additional CSS classes to your button, while keeping the
+default ones, you can use the `extra_class` option. This is particularly useful
+for adding extra details to buttons (without forcing you to repeat the
+Bootstrap classes), or for element targeting via CSS classes.
+Be aware, however, that using the `class` option will discard any extra classes
+you add. As an example, the following button declarations
+
+```erb
+<%= f.primary "My Nice Button", extra_class: 'my-button' %>
+
+<%= f.primary "My Button", class: 'my-button' %>
+```
+
+will be rendered as
+
+```html
+
+
+
+```
+
+(some unimportant HTML attributes have been removed for simplicity)
+
### Accessing Rails Form Helpers
If you want to use the original Rails form helpers for a particular field,
@@ -464,7 +541,7 @@ The `label_col` and `control_col` css classes can also be changed per control:
### Custom Field Layout
-The `layout` can be overridden per field:
+The form-level `layout` can be overridden per field, unless the form-level layout was `inline`:
```erb
<%= bootstrap_form_for(@user, layout: :horizontal) do |f| %>
@@ -477,19 +554,34 @@ The `layout` can be overridden per field:
<% end %>
```
-## Validation & Errors
+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.
+
+### Custom Form Element Styles
+
+The `custom` option can be used to replace the browser default styles for check boxes and radio buttons with dedicated Bootstrap styled form elements. Here's an example:
+
+```erb
+<%= bootstrap_form_for(@user) do |f| %>
+ <%= f.email_field :email %>
+ <%= f.password_field :password %>
+ <%= f.check_box :remember_me, custom: true %>
+ <%= f.submit "Log In" %>
+<% end %>
+```
+
+## Validation and Errors
### Inline Errors
-By default, fields that have validation errors will outlined in red and the
+By default, fields that have validation errors will be outlined in red and the
error will be displayed below the field. Rails normally wraps the fields in a
div (field_with_errors), but this behavior is suppressed. Here's an example:
```html
-
+
-
- can't be blank
+
+ can't be blank
```
@@ -594,6 +686,21 @@ Which outputs:
bootstrap_form follows standard rails conventions so it's i18n-ready. See more
here: http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
+## Other Tips and Edge Cases
+By their very nature, forms are extremely diverse. It would be extremely difficult to provide a gem that could handle every need. Here are some tips for handling edge cases.
+
+### Empty But Visible Labels
+Some third party plug-ins require an empty but visible label on an input control. The `hide_label` option generates a label that won't appear on the screen, but it's considered invisible and therefore doesn't work with such a plug-in. An empty label (e.g. `""`) causes the underlying Rails helper to generate a label based on the field's attribute's name.
+
+The solution is to use a zero-width character for the label, or some other "empty" HTML. For example:
+```
+label: "".html_safe
+```
+or
+```
+label: "".html_safe
+```
+
## Code Triage page
http://www.codetriage.com/potenza/bootstrap_form
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 000000000..f7e2dcaa7
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,25 @@
+# Releasing
+
+Follow these steps to release a new version of bootstrap_form to rubygems.org.
+
+## Prerequisites
+
+* You must have commit rights to the bootstrap_form repository.
+* You must have push rights for the bootstrap_form gem on rubygems.org.
+* You must be using Ruby >= 2.2.
+* 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).
+
+## How to release
+
+1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
+2. **Ensure the tests are passing by running `bundle exec rake`.**
+3. Determine which would be the correct next version number according to [semver](http://semver.org/).
+4. Update the version in `./lib/bootstrap_form/version.rb`.
+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):
+ * Rename the Pending Release section to `[version][] (date)` with appropriate values `version` and `date`
+ * Remove the "Your contribution here!" bullets from the release notes
+ * 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
+ * Add the appropriate GitHub diff links to the footer of the document
+6. Update the installation instructions in `README.md` to use the new version.
+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.
+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).
diff --git a/Rakefile b/Rakefile
index 708c7b858..568a060be 100644
--- a/Rakefile
+++ b/Rakefile
@@ -24,4 +24,10 @@ Rake::TestTask.new(:test) do |t|
t.verbose = false
end
+# This automatically updates GitHub Releases whenever we `rake release` the gem
+task "release:rubygem_push" do
+ require "chandler/tasks"
+ Rake.application.invoke_task("chandler:push")
+end
+
task default: :test
diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md
new file mode 100644
index 000000000..7b0348581
--- /dev/null
+++ b/UPGRADE-4.0.md
@@ -0,0 +1,79 @@
+# Upgrading to `bootstrap_form` 4.0
+We made every effort to make the upgrade from `bootstrap_form` v2.7 (Bootstrap 3) to `bootstrap_form` v4.0 (Bootstrap 4) as easy as possible. However, Bootstrap 4 is fundamentally different from Bootstrap 3, so some changes may be necessary in your code.
+
+## Bootstrap 4 Changes
+If you made use of Bootstrap classes or Javascript, you should read the [Bootstrap 4 migration guide](https://getbootstrap.com/docs/4.0/migration/).
+
+## Validation Error Messages
+With Bootstrap 4, in order for validation error messages to display, the message has to be a sibling of the `input` tag, and the `input` tag has to have the `.is-invalid` class. This was different from Bootstrap 3, and forced some changes to `bootstrap_form` that may affect programs that used `bootstrap_form` v2.7.
+
+### Arbitrary Text in `form_group` Blocks
+In `bootstrap_form` v2.7, it was possible to write something like this:
+```
+<%= bootstrap_form_for(@user) do |f| %>
+ <%= f.form_group(:email) do %>
+
Bar
+ <%= end %>
+<%= end %>
+```
+and, if `@user.email` had validation errors, it would render:
+```
+
+
Bar
+ can't be blank, is too short (minimum is 5 characters)
+
+```
+which would show an error message in red.
+
+That doesn't work in Bootstrap 4. Outputting error messages had to be moved to accommodate other changes, so `form_group` no longer outputs error messages unless whatever is inside the block is a `bootstrap_form` helper.
+
+One way to make the above behave the same in `bootstrap_form` v4.0 is to write it like this:
+
+```
+<%= bootstrap_form_for(@user) do |f| %>
+ <%= f.form_group(:email) do %>
+
Bar
+ <%= content_tag(:div, @user.errors[:email].join(", "), class: "invalid-feedback", style: "display: block;") unless @user.errors[:email].empty? %>
+ <%= end %>
+<%= end %>
+```
+
+### Check Boxes and Radio Buttons
+Bootstrap 4 marks up check boxes and radio buttons differently. In particular, Bootstrap 4 wraps the `input` and `label` tags in a `div.form-check` tag. Because validation error messages have to be siblings of the `input` tag, there is now an `error_message` option to `check_box` and `radio_button` to cause them to put the validation error messages inside the `div.form-check`.
+
+This change is mostly invisible to existing programs:
+
+- Since the default for `error_message` is false, use of `check_box` and `radio_button` all by themselves behaves the same as in `bootstrap_form` v2.7
+- All the `collection*` helpers that output radio buttons and check boxes arrange to produce the validation error message on the last check box or radio button of the group, like `bootstrap_form` v2.7 did
+
+There is one situation where an existing program will have to change. When rendering one or more check boxes or radio buttons inside a `form_group` block, the last call to `check_box` or `radio_button` in the block will have to have `error_message: true` added to its parameters, like this:
+
+```
+<%= bootstrap_form_for(@user) do |f| %>
+ <%= f.form_group(:education) do %>
+ <%= f.radio_button(:misc, "primary school") %>
+ <%= f.radio_button(:misc, "high school") %>
+ <%= f.radio_button(:misc, "university", error_message: true) %>
+ <%= end %>
+<%= end %>
+```
+
+## `form-group` and Horizontal Forms
+In Bootstrap 3, `.form-group` mixed in `.row`. In Bootstrap 4, it doesn't. So `bootstrap_form` automatically adds `.row` to the `div.form-group`s that it creates, if the form group is in a horizontal layout. When migrating forms from the Bootstrap 3 version of `bootstrap_form` to the Bootstrap 4 version, check all horizontal forms to be sure they're being rendered properly.
+
+Bootstrap 4 also provides a `.form-row`, which has smaller gutters than `.row`. If you specify ".form-row", `bootstrap_form` will replace `.row` with `.form-row` on the `div.form-group`. When calling `form_group` directly, do something like this:
+```
+bootstrap_form_for(@user, layout: "horizontal") do |f|
+ f.form_group class: "form-row" do
+ ...
+ end
+end
+```
+For the other helpers, do something like this:
+```
+bootstrap_form_for(@user, layout: "horizontal") do |f|
+ f.form_group class: "form-row" do
+ f.text_field wrapper_class: "form-row" # or f.text_field wrapper: { class: "form-row" }
+ ...
+ end
+end
diff --git a/bootstrap_form.gemspec b/bootstrap_form.gemspec
index 5a0a9699a..e83ef36fb 100644
--- a/bootstrap_form.gemspec
+++ b/bootstrap_form.gemspec
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
s.version = BootstrapForm::VERSION
s.authors = ["Stephen Potenza", "Carlos Lopes"]
s.email = ["potenza@gmail.com", "carlos.el.lopes@gmail.com"]
- s.homepage = "https://github.com/bootstrap-ruby/rails-bootstrap-forms"
+ s.homepage = "https://github.com/bootstrap-ruby/bootstrap_form"
s.summary = "Rails form builder that makes it easy to style forms using "\
"Bootstrap 4"
s.description = "bootstrap_form is a rails form builder that makes it super "\
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 000000000..8b2dc3ad8
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,17 @@
+# DEMO APP (Rails 5.2)
+
+### Usage
+
+- `rake db:schema:load`
+- `rails s`
+- Navigate to http://localhost:3000
+
+### Following files were added or changed:
+
+- db/schema.rb
+- config/{application, routes, boot}.rb
+- config/environments/{development, test}.rb
+- app/models/{address,user,super_user,faux_user}.rb
+- app/controllers/bootstrap_controller.rb
+- app/views/layouts/application.html.erb
+- app/views/bootstrap/form.html.erb
\ No newline at end of file
diff --git a/demo/Rakefile b/demo/Rakefile
new file mode 100644
index 000000000..e85f91391
--- /dev/null
+++ b/demo/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/demo/app/controllers/application_controller.rb b/demo/app/controllers/application_controller.rb
new file mode 100644
index 000000000..09705d12a
--- /dev/null
+++ b/demo/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/demo/app/controllers/bootstrap_controller.rb b/demo/app/controllers/bootstrap_controller.rb
new file mode 100644
index 000000000..371da3291
--- /dev/null
+++ b/demo/app/controllers/bootstrap_controller.rb
@@ -0,0 +1,16 @@
+class BootstrapController < ApplicationController
+
+ def form
+ @collection = [
+ Address.new(id: 1, street: 'Foo'),
+ Address.new(id: 2, street: 'Bar')
+ ]
+
+ @user = User.new
+
+ @user_with_error = User.new
+ @user_with_error.errors.add(:email)
+ @user_with_error.errors.add(:misc)
+ end
+
+end
diff --git a/demo/app/helpers/bootstrap_helper.rb b/demo/app/helpers/bootstrap_helper.rb
new file mode 100644
index 000000000..4b7a67ca0
--- /dev/null
+++ b/demo/app/helpers/bootstrap_helper.rb
@@ -0,0 +1,23 @@
+module BootstrapHelper
+
+ def form_with_source(&block)
+ form_html = capture(&block)
+
+ content_tag(:div, class: "example") do
+ codemirror = content_tag(:div, class: "code", style: "display: none") do
+ content_tag(:textarea, class: "codemirror") do
+ HtmlBeautifier.beautify(form_html.strip.gsub(">", ">\n").gsub("<", "\n<"))
+ end
+ end
+
+ toggle = content_tag(:button, class: "toggle btn btn-sm btn-info") do
+ "Show Source Code"
+ end
+
+ concat(form_html)
+ concat(toggle)
+ concat(codemirror)
+ end
+ end
+
+end
diff --git a/test/dummy/app/models/address.rb b/demo/app/models/address.rb
similarity index 100%
rename from test/dummy/app/models/address.rb
rename to demo/app/models/address.rb
diff --git a/demo/app/models/application_record.rb b/demo/app/models/application_record.rb
new file mode 100644
index 000000000..10a4cba84
--- /dev/null
+++ b/demo/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/test/dummy/app/models/faux_user.rb b/demo/app/models/faux_user.rb
similarity index 100%
rename from test/dummy/app/models/faux_user.rb
rename to demo/app/models/faux_user.rb
diff --git a/test/dummy/app/models/super_user.rb b/demo/app/models/super_user.rb
similarity index 100%
rename from test/dummy/app/models/super_user.rb
rename to demo/app/models/super_user.rb
diff --git a/test/dummy/app/models/user.rb b/demo/app/models/user.rb
similarity index 100%
rename from test/dummy/app/models/user.rb
rename to demo/app/models/user.rb
diff --git a/demo/app/views/bootstrap/form.html.erb b/demo/app/views/bootstrap/form.html.erb
new file mode 100644
index 000000000..039af7631
--- /dev/null
+++ b/demo/app/views/bootstrap/form.html.erb
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/bin/bundle b/demo/bin/bundle
new file mode 100755
index 000000000..f19acf5b5
--- /dev/null
+++ b/demo/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/test/dummy/bin/rails b/demo/bin/rails
similarity index 100%
rename from test/dummy/bin/rails
rename to demo/bin/rails
diff --git a/test/dummy/bin/rake b/demo/bin/rake
similarity index 100%
rename from test/dummy/bin/rake
rename to demo/bin/rake
diff --git a/demo/bin/setup b/demo/bin/setup
new file mode 100755
index 000000000..94fd4d797
--- /dev/null
+++ b/demo/bin/setup
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:setup'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/demo/bin/update b/demo/bin/update
new file mode 100755
index 000000000..58bfaed51
--- /dev/null
+++ b/demo/bin/update
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/demo/bin/yarn b/demo/bin/yarn
new file mode 100755
index 000000000..ec3db7b27
--- /dev/null
+++ b/demo/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path('..', __dir__)
+Dir.chdir(APP_ROOT) do
+ begin
+ exec "yarnpkg #{ARGV.join(' ')}"
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/test/dummy/config.ru b/demo/config.ru
similarity index 100%
rename from test/dummy/config.ru
rename to demo/config.ru
diff --git a/demo/config/application.rb b/demo/config/application.rb
new file mode 100644
index 000000000..adc98cac0
--- /dev/null
+++ b/demo/config/application.rb
@@ -0,0 +1,24 @@
+require_relative 'boot'
+
+require 'rails/all'
+
+Bundler.require(*Rails.groups)
+require "bootstrap_form"
+
+module Dummy
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+
+ if config.respond_to?(:load_defaults)
+ config.load_defaults [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join(".")
+ end
+
+ if config.respond_to?(:secret_key_base)
+ config.secret_key_base = "ignore"
+ end
+
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+ end
+end
diff --git a/demo/config/boot.rb b/demo/config/boot.rb
new file mode 100644
index 000000000..e334ad375
--- /dev/null
+++ b/demo/config/boot.rb
@@ -0,0 +1,5 @@
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __dir__)
+
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
diff --git a/test/dummy/config/database.yml b/demo/config/database.yml
similarity index 89%
rename from test/dummy/config/database.yml
rename to demo/config/database.yml
index 0d02f2498..1af1a8e91 100644
--- a/test/dummy/config/database.yml
+++ b/demo/config/database.yml
@@ -19,7 +19,3 @@ development:
test:
<<: *default
database: db/test.sqlite3
-
-production:
- <<: *default
- database: db/production.sqlite3
diff --git a/demo/config/environment.rb b/demo/config/environment.rb
new file mode 100644
index 000000000..426333bb4
--- /dev/null
+++ b/demo/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/demo/config/environments/development.rb b/demo/config/environments/development.rb
new file mode 100644
index 000000000..8a9751305
--- /dev/null
+++ b/demo/config/environments/development.rb
@@ -0,0 +1,60 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ if config.respond_to?(:active_storage)
+ config.active_storage.service = :local
+ end
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+end
diff --git a/test/dummy/config/environments/test.rb b/demo/config/environments/test.rb
similarity index 100%
rename from test/dummy/config/environments/test.rb
rename to demo/config/environments/test.rb
diff --git a/demo/config/initializers/application_controller_renderer.rb b/demo/config/initializers/application_controller_renderer.rb
new file mode 100644
index 000000000..89d2efab2
--- /dev/null
+++ b/demo/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# ActiveSupport::Reloader.to_prepare do
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
+# end
diff --git a/demo/config/initializers/assets.rb b/demo/config/initializers/assets.rb
new file mode 100644
index 000000000..4b828e80c
--- /dev/null
+++ b/demo/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join('node_modules')
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/demo/config/initializers/backtrace_silencers.rb
similarity index 100%
rename from test/dummy/config/initializers/backtrace_silencers.rb
rename to demo/config/initializers/backtrace_silencers.rb
diff --git a/demo/config/initializers/cookies_serializer.rb b/demo/config/initializers/cookies_serializer.rb
new file mode 100644
index 000000000..5a6a32d37
--- /dev/null
+++ b/demo/config/initializers/cookies_serializer.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/demo/config/initializers/filter_parameter_logging.rb
similarity index 100%
rename from test/dummy/config/initializers/filter_parameter_logging.rb
rename to demo/config/initializers/filter_parameter_logging.rb
diff --git a/test/dummy/config/initializers/inflections.rb b/demo/config/initializers/inflections.rb
similarity index 100%
rename from test/dummy/config/initializers/inflections.rb
rename to demo/config/initializers/inflections.rb
diff --git a/test/dummy/config/initializers/mime_types.rb b/demo/config/initializers/mime_types.rb
similarity index 100%
rename from test/dummy/config/initializers/mime_types.rb
rename to demo/config/initializers/mime_types.rb
diff --git a/test/dummy/config/initializers/wrap_parameters.rb b/demo/config/initializers/wrap_parameters.rb
similarity index 100%
rename from test/dummy/config/initializers/wrap_parameters.rb
rename to demo/config/initializers/wrap_parameters.rb
diff --git a/test/dummy/config/locales/en.yml b/demo/config/locales/en.yml
similarity index 100%
rename from test/dummy/config/locales/en.yml
rename to demo/config/locales/en.yml
diff --git a/demo/config/puma.rb b/demo/config/puma.rb
new file mode 100644
index 000000000..1e19380dc
--- /dev/null
+++ b/demo/config/puma.rb
@@ -0,0 +1,56 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked webserver processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory. If you use this option
+# you need to make sure to reconnect any threads in the `on_worker_boot`
+# block.
+#
+# preload_app!
+
+# If you are preloading your application and using Active Record, it's
+# recommended that you close any connections to the database before workers
+# are forked to prevent connection leakage.
+#
+# before_fork do
+# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
+# end
+
+# The code in the `on_worker_boot` will be called if you are using
+# clustered mode by specifying a number of `workers`. After each worker
+# process is booted, this block will be run. If you are using the `preload_app!`
+# option, you will want to use this block to reconnect to any threads
+# or connections that may have been created at application boot, as Ruby
+# cannot share connections between processes.
+#
+# on_worker_boot do
+# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
+# end
+#
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/test/dummy/config/routes.rb b/demo/config/routes.rb
similarity index 100%
rename from test/dummy/config/routes.rb
rename to demo/config/routes.rb
diff --git a/demo/config/spring.rb b/demo/config/spring.rb
new file mode 100644
index 000000000..9fa7863f9
--- /dev/null
+++ b/demo/config/spring.rb
@@ -0,0 +1,6 @@
+%w[
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+].each { |path| Spring.watch(path) }
diff --git a/demo/config/storage.yml b/demo/config/storage.yml
new file mode 100644
index 000000000..388b6e475
--- /dev/null
+++ b/demo/config/storage.yml
@@ -0,0 +1,35 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# keyfile: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket
+
+# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# path: your_azure_storage_path
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/demo/db/schema.rb b/demo/db/schema.rb
new file mode 100644
index 000000000..4020137a6
--- /dev/null
+++ b/demo/db/schema.rb
@@ -0,0 +1,24 @@
+ActiveRecord::Schema.define(version: 1) do
+
+ create_table :addresses, force: :cascade do |t|
+ t.integer :user_id
+ t.string :street
+ t.string :city
+ t.string :state
+ t.string :zip_code
+ t.timestamps
+ end
+
+ create_table :users, force: :cascade do |t|
+ t.string :email
+ t.string :password
+ t.text :comments
+ t.string :status
+ t.string :misc
+ t.text :preferences
+ t.boolean :terms, default: false
+ t.string :type
+ t.timestamps
+ end
+
+end
diff --git a/test/dummy/log/.keep b/demo/log/.keep
similarity index 100%
rename from test/dummy/log/.keep
rename to demo/log/.keep
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 000000000..caa2d7bb3
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/test/dummy/public/favicon.ico b/demo/public/favicon.ico
similarity index 100%
rename from test/dummy/public/favicon.ico
rename to demo/public/favicon.ico
diff --git a/lib/bootstrap_form/form_builder.rb b/lib/bootstrap_form/form_builder.rb
index 74871db44..09eb61adc 100644
--- a/lib/bootstrap_form/form_builder.rb
+++ b/lib/bootstrap_form/form_builder.rb
@@ -38,7 +38,7 @@ def initialize(object_name, object, template, options)
define_method(with_method_name) do |name, options = {}|
form_group_builder(name, options) do
- prepend_and_append_input(options) do
+ prepend_and_append_input(name, options) do
send(without_method_name, name, options)
end
end
@@ -53,7 +53,15 @@ def initialize(object_name, object, template, options)
define_method(with_method_name) do |name, options = {}, html_options = {}|
form_group_builder(name, options, html_options) do
- content_tag(:div, send(without_method_name, name, options, html_options), class: control_specific_class(method_name))
+ html_class = control_specific_class(method_name)
+ if @layout == :horizontal && !options[:skip_inline].present?
+ html_class = "#{html_class} form-inline"
+ end
+ content_tag(:div, class: html_class) do
+ input_with_error(name) do
+ send(without_method_name, name, options, html_options)
+ end
+ end
end
end
@@ -61,27 +69,29 @@ def initialize(object_name, object, template, options)
end
def file_field_with_bootstrap(name, options = {})
- form_group_builder(name, options.reverse_merge(control_class: nil)) do
- file_field_without_bootstrap(name, options)
+ options = options.reverse_merge(control_class: "custom-file-input")
+ form_group_builder(name, options) do
+ content_tag(:div, class: "custom-file") do
+ input_with_error(name) do
+ placeholder = options.delete(:placeholder) || "Choose file"
+ placeholder_opts = { class: "custom-file-label" }
+ placeholder_opts[:for] = options[:id] if options[:id].present?
+
+ input = file_field_without_bootstrap(name, options)
+ placeholder_label = label(name, placeholder, placeholder_opts)
+ concat(input)
+ concat(placeholder_label)
+ end
+ end
end
end
bootstrap_method_alias :file_field
- if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("4.1.0")
- def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block)
- form_group_builder(method, options, html_options) do
- prepend_and_append_input(options) do
- select_without_bootstrap(method, choices, options, html_options, &block)
- end
- end
- end
- else
- def select_with_bootstrap(method, choices, options = {}, html_options = {})
- form_group_builder(method, options, html_options) do
- prepend_and_append_input(options) do
- select_without_bootstrap(method, choices, options, html_options)
- end
+ def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block)
+ form_group_builder(method, options, html_options) do
+ prepend_and_append_input(method, options) do
+ select_without_bootstrap(method, choices, options, html_options, &block)
end
end
end
@@ -90,7 +100,9 @@ def select_with_bootstrap(method, choices, options = {}, html_options = {})
def collection_select_with_bootstrap(method, collection, value_method, text_method, options = {}, html_options = {})
form_group_builder(method, options, html_options) do
- collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
+ input_with_error(method) do
+ collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
+ end
end
end
@@ -98,7 +110,9 @@ def collection_select_with_bootstrap(method, collection, value_method, text_meth
def grouped_collection_select_with_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
form_group_builder(method, options, html_options) do
- grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
+ input_with_error(method) do
+ grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
+ end
end
end
@@ -106,7 +120,9 @@ def grouped_collection_select_with_bootstrap(method, collection, group_method, g
def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, html_options = {})
form_group_builder(method, options, html_options) do
- time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
+ input_with_error(method) do
+ time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
+ end
end
end
@@ -114,32 +130,52 @@ def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {},
def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
options = options.symbolize_keys!
- check_box_options = options.except(:label, :label_class, :help, :inline)
- check_box_options[:class] = ["form-check-input", check_box_options[:class]].compact.join(' ')
+ check_box_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label, :wrapper_class)
+ check_box_classes = [check_box_options[:class]]
+ check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
+ check_box_classes << "is-invalid" if has_error?(name)
+
+ label_classes = [options[:label_class]]
+ label_classes << hide_class if options[:hide_label]
+
+ if options[:custom]
+ check_box_options[:class] = (["custom-control-input"] + check_box_classes).compact.join(' ')
+ wrapper_class = ["custom-control", "custom-checkbox"]
+ wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
+ label_class = label_classes.prepend("custom-control-label").compact.join(" ")
+ else
+ check_box_options[:class] = (["form-check-input"] + check_box_classes).compact.join(' ')
+ wrapper_class = ["form-check"]
+ wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
+ label_class = label_classes.prepend("form-check-label").compact.join(" ")
+ end
- html = check_box_without_bootstrap(name, check_box_options, checked_value, unchecked_value)
+ checkbox_html = check_box_without_bootstrap(name, check_box_options, checked_value, unchecked_value)
label_content = block_given? ? capture(&block) : options[:label]
- html.concat(" ").concat(label_content || (object && object.class.human_attribute_name(name)) || name.to_s.humanize)
+ label_description = label_content || (object && object.class.human_attribute_name(name)) || name.to_s.humanize
label_name = name
# label's `for` attribute needs to match checkbox tag's id,
# IE sanitized value, IE
- # https://github.com/rails/rails/blob/c57e7239a8b82957bcb07534cb7c1a3dcef71864/actionview/lib/action_view/helpers/tags/base.rb#L116-L118
+ # https://github.com/rails/rails/blob/5-0-stable/actionview/lib/action_view/helpers/tags/base.rb#L123-L125
if options[:multiple]
label_name =
- "#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}"
+ "#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s}"
end
- disabled_class = " disabled" if options[:disabled]
- label_class = options[:label_class]
+ label_options = { class: label_class }
+ label_options[:for] = options[:id] if options[:id].present?
- if options[:inline]
- label_class = " #{label_class}" if label_class
- label(label_name, html, class: "form-check-inline#{disabled_class}#{label_class}")
- else
- content_tag(:div, class: "form-check#{disabled_class}") do
- label(label_name, html, class: ["form-check-label", label_class].compact.join(" "))
+ wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
+
+ content_tag(:div, class: wrapper_class.compact.join(" ")) do
+ html = if options[:skip_label]
+ checkbox_html
+ else
+ checkbox_html.concat(label(label_name, label_description, label_options))
end
+ html.concat(generate_error(name)) if options[:error_message]
+ html
end
end
@@ -147,20 +183,41 @@ def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_
def radio_button_with_bootstrap(name, value, *args)
options = args.extract_options!.symbolize_keys!
- args << options.except(:label, :label_class, :help, :inline)
+ radio_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label, :wrapper_class)
+ radio_classes = [options[:class]]
+ radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
+ radio_classes << "is-invalid" if has_error?(name)
+
+ label_classes = [options[:label_class]]
+ label_classes << hide_class if options[:hide_label]
+
+ if options[:custom]
+ radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(' ')
+ wrapper_class = ["custom-control", "custom-radio"]
+ wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
+ label_class = label_classes.prepend("custom-control-label").compact.join(" ")
+ else
+ radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(' ')
+ wrapper_class = ["form-check"]
+ wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
+ wrapper_class.append("disabled") if options[:disabled]
+ label_class = label_classes.prepend("form-check-label").compact.join(" ")
+ end
+ radio_html = radio_button_without_bootstrap(name, value, radio_options)
- html = radio_button_without_bootstrap(name, value, *args) + " " + options[:label]
+ label_options = { value: value, class: label_class }
+ label_options[:for] = options[:id] if options[:id].present?
- disabled_class = " disabled" if options[:disabled]
- label_class = options[:label_class]
+ wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
- if options[:inline]
- label_class = " #{label_class}" if label_class
- label(name, html, class: "radio-inline#{disabled_class}#{label_class}", value: value)
- else
- content_tag(:div, class: "radio#{disabled_class}") do
- label(name, html, value: value, class: label_class)
+ content_tag(:div, class: wrapper_class.compact.join(" ")) do
+ html = if options[:skip_label]
+ radio_html
+ else
+ radio_html.concat(label(name, options[:label], label_options))
end
+ html.concat(generate_error(name)) if options[:error_message]
+ html
end
end
@@ -184,30 +241,22 @@ def collection_radio_buttons_with_bootstrap(*args)
bootstrap_method_alias :collection_radio_buttons
- def check_boxes_collection(*args)
- warn "'BootstrapForm#check_boxes_collection' is deprecated, use 'BootstrapForm#collection_check_boxes' instead"
- collection_check_boxes(*args)
- end
-
- def radio_buttons_collection(*args)
- warn "'BootstrapForm#radio_buttons_collection' is deprecated, use 'BootstrapForm#collection_radio_buttons' instead"
- collection_radio_buttons(*args)
- end
-
def form_group(*args, &block)
options = args.extract_options!
name = args.first
options[:class] = ["form-group", options[:class]].compact.join(' ')
- options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal
- options[:class] << " #{error_class}" if has_error?(name)
+ options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal &&
+ !options[:class].split.include?("form-row")
+ options[:class] << " form-inline" if field_inline_override?(options[:layout])
options[:class] << " #{feedback_class}" if options[:icon]
- content_tag(:div, options.except(:id, :label, :help, :icon, :label_col, :control_col, :layout)) do
+ content_tag(:div, options.except(:append, :id, :label, :help, :icon, :input_group_class, :label_col, :control_col, :layout, :prepend)) do
label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label]
- control = capture(&block).to_s
- control.concat(generate_help(name, options[:help]).to_s)
- control.concat(generate_icon(options[:icon])) if options[:icon]
+ control = capture(&block)
+
+ help = options[:help]
+ help_text = generate_help(name, help).to_s
if get_group_layout(options[:layout]) == :horizontal
control_class = options[:control_col] || control_col
@@ -215,17 +264,21 @@ def form_group(*args, &block)
control_offset = offset_col(options[:label_col] || @label_col)
control_class = "#{control_class} #{control_offset}"
end
- control = content_tag(:div, control, class: control_class)
+ control = content_tag(:div, control + help_text, class: control_class)
+ concat(label).concat(control)
+ else
+ concat(label).concat(control).concat(help_text)
end
-
- concat(label).concat(control)
end
end
def fields_for_with_bootstrap(record_name, record_object = nil, fields_options = {}, &block)
- fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
+ if record_object.is_a?(Hash) && record_object.extractable_options?
+ fields_options = record_object
+ record_object = nil
+ end
fields_options[:layout] ||= options[:layout]
- fields_options[:label_col] = fields_options[:label_col].present? ? "#{fields_options[:label_col]} #{label_class}" : options[:label_col]
+ fields_options[:label_col] = fields_options[:label_col].present? ? "#{fields_options[:label_col]}" : options[:label_col]
fields_options[:control_col] ||= options[:control_col]
fields_options[:inline_errors] ||= options[:inline_errors]
fields_options[:label_errors] ||= options[:label_errors]
@@ -234,10 +287,34 @@ def fields_for_with_bootstrap(record_name, record_object = nil, fields_options =
bootstrap_method_alias :fields_for
+ # the Rails `fields` method passes its options
+ # to the builder, so there is no need to write a `bootstrap_form` helper
+ # for the `fields` method.
+
private
- def horizontal?
- layout == :horizontal
+ def layout_default?(field_layout = nil)
+ [:default, nil].include? layout_in_effect(field_layout)
+ end
+
+ def layout_horizontal?(field_layout = nil)
+ layout_in_effect(field_layout) == :horizontal
+ end
+
+ def layout_inline?(field_layout = nil)
+ layout_in_effect(field_layout) == :inline
+ end
+
+ def field_inline_override?(field_layout = nil)
+ field_layout == :inline && layout != :inline
+ end
+
+ # true and false should only come from check_box and radio_button,
+ # and those don't have a :horizontal layout
+ def layout_in_effect(field_layout)
+ field_layout = :inline if field_layout == true
+ field_layout = :default if field_layout == false
+ field_layout || layout
end
def get_group_layout(group_layout)
@@ -249,7 +326,7 @@ def default_label_col
end
def offset_col(label_col)
- label_col.sub(/^col-(\w+)-(\d)$/, 'col-\1-offset-\2')
+ label_col.gsub(/\bcol-(\w+)-(\d)\b/, 'offset-\1-\2')
end
def default_control_col
@@ -264,14 +341,6 @@ def control_class
"form-control"
end
- def label_class
- "form-control-label"
- end
-
- def error_class
- "has-danger"
- end
-
def feedback_class
"has-feedback"
end
@@ -309,18 +378,20 @@ def required_attribute?(obj, attribute)
def form_group_builder(method, options, html_options = nil)
options.symbolize_keys!
+
+ wrapper_class = options.delete(:wrapper_class)
+ wrapper_options = options.delete(:wrapper)
+
html_options.symbolize_keys! if html_options
# Add control_class; allow it to be overridden by :control_class option
css_options = html_options || options
control_classes = css_options.delete(:control_class) { control_class }
css_options[:class] = [control_classes, css_options[:class]].compact.join(" ")
- css_options[:class] << " form-control-danger" if has_error?(method)
+ css_options[:class] << " is-invalid" if has_error?(method)
options = convert_form_tag_options(method, options) if acts_like_form_tag
- wrapper_class = css_options.delete(:wrapper_class)
- wrapper_options = css_options.delete(:wrapper)
help = options.delete(:help)
icon = options.delete(:icon)
label_col = options.delete(:label_col)
@@ -347,17 +418,21 @@ def form_group_builder(method, options, html_options = nil)
options.delete(:label)
end
label_class ||= options.delete(:label_class)
- label_class = hide_class if options.delete(:hide_label)
+ label_class = hide_class if options.delete(:hide_label) || options[:label_as_placeholder]
if options[:label].is_a?(String)
label_text ||= options.delete(:label)
end
- form_group_options.merge!(label: {
+ form_group_options[:label] = {
text: label_text,
class: label_class,
skip_required: options.delete(:skip_required)
- })
+ }.merge(css_options[:id].present? ? { for: css_options[:id] } : {})
+
+ if options.delete(:label_as_placeholder)
+ css_options[:placeholder] = label_text || object.class.human_attribute_name(method)
+ end
end
form_group(method, form_group_options) do
@@ -366,46 +441,67 @@ def form_group_builder(method, options, html_options = nil)
end
def convert_form_tag_options(method, options = {})
- options[:name] ||= method
- options[:id] ||= method
+ unless @options[:skip_default_ids]
+ options[:name] ||= method
+ options[:id] ||= method
+ end
options
end
def generate_label(id, name, options, custom_label_col, group_layout)
+ # id is the caller's options[:id] at the only place this method is called.
+ # The options argument is a small subset of the options that might have
+ # been passed to generate_label's caller, and definitely doesn't include
+ # :id.
options[:for] = id if acts_like_form_tag
- classes = [options[:class], label_class]
- classes << (custom_label_col || label_col) if get_group_layout(group_layout) == :horizontal
+ classes = [options[:class]]
+
+ if layout_horizontal?(group_layout)
+ classes << "col-form-label"
+ classes << (custom_label_col || label_col)
+ elsif layout_inline?(group_layout)
+ classes << "mr-sm-2"
+ end
+
unless options.delete(:skip_required)
classes << "required" if required_attribute?(object, name)
end
- options[:class] = classes.compact.join(" ")
+ options[:class] = classes.compact.join(" ").strip
+ options.delete(:class) if options[:class].empty?
if label_errors && has_error?(name)
error_messages = get_error_messages(name)
label_text = (options[:text] || object.class.human_attribute_name(name)).to_s.concat(" #{error_messages}")
+ options[:class] = [options[:class], "text-danger"].compact.join(" ")
label(name, label_text, options.except(:text))
else
label(name, options[:text], options.except(:text))
end
+ end
+ def has_inline_error?(name)
+ has_error?(name) && inline_errors
end
- def generate_help(name, help_text)
- if has_error?(name) && inline_errors
+ def generate_error(name)
+ if has_inline_error?(name)
help_text = get_error_messages(name)
- help_klass = 'form-control-feedback'
+ help_klass = 'invalid-feedback'
+ help_tag = :div
+
+ content_tag(help_tag, help_text, class: help_klass)
end
- return if help_text == false
+ end
+
+ def generate_help(name, help_text)
+ return if help_text == false || has_inline_error?(name)
help_klass ||= 'form-text text-muted'
help_text ||= get_help_text_by_i18n_key(name)
+ help_tag ||= :small
- content_tag(:span, help_text, class: help_klass) if help_text.present?
- end
-
- def generate_icon(icon)
- content_tag(:span, "", class: "glyphicon glyphicon-#{icon} form-control-feedback")
+ content_tag(help_tag, help_text, class: help_klass) if help_text.present?
end
def get_error_messages(name)
@@ -413,10 +509,11 @@ def get_error_messages(name)
end
def inputs_collection(name, collection, value, text, options = {}, &block)
+ options[:inline] ||= layout_inline?(options[:layout])
form_group_builder(name, options) do
inputs = ""
- collection.each do |obj|
+ collection.each_with_index do |obj, i|
input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text))
input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
@@ -428,7 +525,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block)
end
input_options.delete(:class)
- inputs << block.call(name, input_value, input_options)
+ inputs << block.call(name, input_value, input_options.merge(error_message: i == collection.size - 1))
end
inputs.html_safe
@@ -439,15 +536,20 @@ def get_help_text_by_i18n_key(name)
if object
if object.class.respond_to?(:model_name)
- # ActiveModel::Naming 3.X.X does not support .name; it is supported as of 4.X.X
- partial_scope = object.class.model_name.respond_to?(:name) ? object.class.model_name.name : object.class.model_name
+ partial_scope = object.class.model_name.name
else
partial_scope = object.class.name
end
underscored_scope = "activerecord.help.#{partial_scope.underscore}"
downcased_scope = "activerecord.help.#{partial_scope.downcase}"
- help_text = I18n.t(name, scope: underscored_scope, default: '').presence
+ # First check for a subkey :html, as it is also accepted by i18n, and the simple check for name would return an hash instead of a string (both with .presence returning true!)
+ help_text = I18n.t("#{name}.html", scope: underscored_scope, default: '').html_safe.presence
+ help_text ||= if text = I18n.t("#{name}.html", scope: downcased_scope, default: '').html_safe.presence
+ warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
+ text
+ end
+ help_text ||= I18n.t(name, scope: underscored_scope, default: '').presence
help_text ||= if text = I18n.t(name, scope: downcased_scope, default: '').presence
warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
text
@@ -460,6 +562,5 @@ def get_help_text_by_i18n_key(name)
help_text
end
end
-
end
end
diff --git a/lib/bootstrap_form/helper.rb b/lib/bootstrap_form/helper.rb
index 6788588e8..15ed2502d 100644
--- a/lib/bootstrap_form/helper.rb
+++ b/lib/bootstrap_form/helper.rb
@@ -1,18 +1,10 @@
-require_relative 'helpers/nested_form'
-
module BootstrapForm
module Helper
- include ::BootstrapForm::Helpers::NestedForm
def bootstrap_form_for(object, options = {}, &block)
options.reverse_merge!({builder: BootstrapForm::FormBuilder})
- options[:html] ||= {}
- options[:html][:role] ||= 'form'
-
- if options[:layout] == :inline
- options[:html][:class] = [options[:html][:class], "form-inline"].compact.join(" ")
- end
+ options = process_options(options)
temporarily_disable_field_error_proc do
form_for(object, options, &block)
@@ -25,6 +17,31 @@ def bootstrap_form_tag(options = {}, &block)
bootstrap_form_for("", options, &block)
end
+ def bootstrap_form_with(options = {}, &block)
+ options.reverse_merge!(builder: BootstrapForm::FormBuilder)
+
+ options = process_options(options)
+
+ temporarily_disable_field_error_proc do
+ form_with(options, &block)
+ end
+ end
+
+ private
+
+ def process_options(options)
+ options[:html] ||= {}
+ options[:html][:role] ||= 'form'
+
+ if options[:layout] == :inline
+ options[:html][:class] = [options[:html][:class], 'form-inline'].compact.join(' ')
+ end
+
+ options
+ end
+
+ public
+
def temporarily_disable_field_error_proc
original_proc = ActionView::Base.field_error_proc
ActionView::Base.field_error_proc = proc { |input, instance| input }
diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb
index 342bb0abc..72ad15c39 100644
--- a/lib/bootstrap_form/helpers/bootstrap.rb
+++ b/lib/bootstrap_form/helpers/bootstrap.rb
@@ -1,14 +1,26 @@
module BootstrapForm
module Helpers
module Bootstrap
+
+ def button(value = nil, options = {}, &block)
+ setup_css_class 'btn btn-secondary', options
+ super
+ end
+
def submit(name = nil, options = {})
- options.reverse_merge! class: 'btn btn-secondary'
- super(name, options)
+ setup_css_class 'btn btn-secondary', options
+ super
end
- def primary(name = nil, options = {})
- options.reverse_merge! class: 'btn btn-primary'
- submit(name, options)
+ def primary(name = nil, options = {}, &block)
+ setup_css_class 'btn btn-primary', options
+
+ if options[:render_as_button] || block_given?
+ options.except! :render_as_button
+ button(name, options, &block)
+ else
+ submit(name, options)
+ end
end
def alert_message(title, options = {})
@@ -23,9 +35,11 @@ def alert_message(title, options = {})
end
def error_summary
- content_tag :ul, class: 'rails-bootstrap-forms-error-summary' do
- object.errors.full_messages.each do |error|
- concat content_tag(:li, error)
+ if object.errors.any?
+ content_tag :ul, class: 'rails-bootstrap-forms-error-summary' do
+ object.errors.full_messages.each do |error|
+ concat content_tag(:li, error)
+ end
end
end
end
@@ -44,19 +58,18 @@ def errors_on(name, options = {})
end
end
- def static_control(*args, &block)
+ def static_control(*args)
options = args.extract_options!
name = args.first
- html = if block_given?
- capture(&block)
- else
- object.send(name)
- end
+ static_options = options.merge({
+ readonly: true,
+ control_class: [options[:control_class], static_class].compact.join(" ")
+ })
- form_group_builder(name, options) do
- content_tag(:p, html, class: static_class)
- end
+ static_options[:value] = object.send(name) unless static_options.has_key?(:value)
+
+ text_field_with_bootstrap(name, static_options)
end
def custom_control(*args, &block)
@@ -66,26 +79,45 @@ def custom_control(*args, &block)
form_group_builder(name, options, &block)
end
- def prepend_and_append_input(options, &block)
+ def prepend_and_append_input(name, options, &block)
options = options.extract!(:prepend, :append, :input_group_class)
input_group_class = ["input-group", options[:input_group_class]].compact.join(' ')
- input = capture(&block)
+ input = capture(&block) || "".html_safe
input = content_tag(:div, input_group_content(options[:prepend]), class: 'input-group-prepend') + input if options[:prepend]
input << content_tag(:div, input_group_content(options[:append]), class: 'input-group-append') if options[:append]
+ input << generate_error(name)
input = content_tag(:div, input, class: input_group_class) unless options.empty?
input
end
+ def input_with_error(name, &block)
+ input = capture(&block)
+ input << generate_error(name)
+ end
+
def input_group_content(content)
return content if content.match(/btn/)
content_tag(:span, content, class: 'input-group-text')
end
def static_class
- "form-control-static"
+ "form-control-plaintext"
end
+
+
+ private
+
+ def setup_css_class(the_class, options = {})
+ unless options.has_key? :class
+ if extra_class = options.delete(:extra_class)
+ the_class = "#{the_class} #{extra_class}"
+ end
+ options[:class] = the_class
+ end
+ end
+
end
end
end
diff --git a/lib/bootstrap_form/helpers/nested_form.rb b/lib/bootstrap_form/helpers/nested_form.rb
deleted file mode 100644
index c2397cc12..000000000
--- a/lib/bootstrap_form/helpers/nested_form.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-begin
- require 'nested_form/builder_mixin'
-
- module BootstrapForm
- class NestedFormBuilder < ::BootstrapForm::FormBuilder
- include ::NestedForm::BuilderMixin
- end
- end
-
- module BootstrapForm
- module Helpers
- module NestedForm
- def bootstrap_nested_form_for(object, options = {}, &block)
- options.reverse_merge!({builder: BootstrapForm::NestedFormBuilder})
- bootstrap_form_for(object, options) do |f|
- capture(f, &block).to_s << after_nested_form_callbacks
- end
- end
- end
- end
- end
-
-rescue LoadError
- module BootstrapForm
- module Helpers
- module NestedForm
- def bootstrap_nested_form_for(object, options = {}, &block)
- raise 'nested_forms was not found. Is it in your Gemfile?'
- end
- end
- end
- end
-end
diff --git a/lib/bootstrap_form/version.rb b/lib/bootstrap_form/version.rb
index 603ffe175..773de1d06 100644
--- a/lib/bootstrap_form/version.rb
+++ b/lib/bootstrap_form/version.rb
@@ -1,3 +1,3 @@
module BootstrapForm
- VERSION = "4.0.0.dev".freeze
+ VERSION = "4.0.0.alpha1".freeze
end
diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb
index da2df251c..02b9ba6df 100644
--- a/test/bootstrap_checkbox_test.rb
+++ b/test/bootstrap_checkbox_test.rb
@@ -1,81 +1,281 @@
-require 'test_helper'
+require_relative "./test_helper"
class BootstrapCheckboxTest < ActionView::TestCase
include BootstrapForm::Helper
- def setup
- setup_test_fixture
- end
+ setup :setup_test_fixture
test "check_box is wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms')
end
+ test "check_box empty label" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ # is a zero-width space.
+ assert_equivalent_xml expected, @builder.check_box(:terms, label: "".html_safe)
+ end
+
test "disabled check_box has proper wrapper classes" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', disabled: true)
end
test "check_box label allows html" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label: %{I agree to the terms}.html_safe)
end
test "check_box accepts a block to define the label" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms) { "I agree to the terms" }
end
test "check_box accepts a custom label class" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label_class: 'btn')
end
+ test "check_box 'id' attribute is used to specify label 'for' attribute" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, id: 'custom_id')
+ end
+
test "check_box responds to checked_value and unchecked_value arguments" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms'}, 'yes', 'no')
end
test "inline checkboxes" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', inline: true)
end
+ test "inline checkboxes from form layout" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ actual = bootstrap_form_for(@user, layout: :inline) do |f|
+ f.check_box(:terms, label: 'I agree to the terms')
+ end
+ assert_equivalent_xml expected, actual
+ end
+
test "disabled inline check_box" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', inline: true, disabled: true)
end
test "inline checkboxes with custom label class" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.check_box(:terms, inline: true, label_class: 'btn')
end
test 'collection_check_boxes renders the form_group correctly' do
collection = [Address.new(id: 1, street: 'Foobar')]
- expected = %{
With a help!
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+ With a help!
+
+ HTML
assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street, label: 'This is a checkbox collection', help: 'With a help!')
end
test 'collection_check_boxes renders multiple checkboxes correctly' do
collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
- expected = %{
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street)
end
+ test 'collection_check_boxes renders multiple checkboxes contains unicode characters in IDs correctly' do
+ struct = Struct.new(:id, :name)
+ collection = [struct.new(1, 'Foo'), struct.new('二', 'Bar')]
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+
+
+
+
+
+ HTML
+
+ assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :name)
+ end
+
test 'collection_check_boxes renders inline checkboxes correctly' do
collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street, inline: true)
end
test 'collection_check_boxes renders with checked option correctly' do
collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
- expected = %{
+ HTML
assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, lambda { |a| "address_#{a.id}" }, :street, checked: ["address_1", "address_2"])
assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, lambda { |a| "address_#{a.id}" }, :street, checked: collection)
end
+ test "check_box is wrapped correctly with custom option set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', custom: true})
+ end
+ test "check_box is wrapped correctly with id option and custom option set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', id: "custom_id", custom: true})
+ end
+
+ test "check_box is wrapped correctly with custom and inline options set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', inline: true, custom: true})
+ end
+
+ test "check_box is wrapped correctly with custom and disabled options set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', disabled: true, custom: true})
+ end
+
+ test "check_box is wrapped correctly with custom, inline and disabled options set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', inline: true, disabled: true, custom: true})
+ end
+
+ test "check_box skip label" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', skip_label: true)
+ end
+
+ test "check_box hide label" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', hide_label: true)
+ end
+
+ test "check_box skip label with custom option set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', custom: true, skip_label: true})
+ end
+
+ test "check_box hide label with custom option set" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', custom: true, hide_label: true})
+ end
+
+ test 'collection_check_boxes renders error after last check box' do
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ @user.errors.add(:misc, "a box must be checked")
+
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ actual = bootstrap_form_for(@user) do |f|
+ f.collection_check_boxes(:misc, collection, :id, :street)
+ end
+
+ assert_equivalent_xml expected, actual
+ end
+
+ test 'collection_check_boxes renders multiple check boxes with error correctly' do
+ @user.errors.add(:misc, "error for test")
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ actual = bootstrap_form_for(@user) do |f|
+ f.collection_check_boxes(:misc, collection, :id, :street, checked: collection)
+ end
+ assert_equivalent_xml expected, actual
+ end
+
+ test 'check_box renders error when asked' do
+ @user.errors.add(:terms, "You must accept the terms.")
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ actual = bootstrap_form_for(@user) do |f|
+ f.check_box(:terms, label: 'I agree to the terms', error_message: true)
+ end
+ assert_equivalent_xml expected, actual
+ end
+
+ test "check_box with error is wrapped correctly with custom option set" do
+ @user.errors.add(:terms, "You must accept the terms.")
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ actual = bootstrap_form_for(@user) do |f|
+ f.check_box(:terms, {label: 'I agree to the terms', custom: true, error_message: true})
+ end
+ assert_equivalent_xml expected, actual
+ end
+
+ test "check box with custom wrapper class" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', wrapper_class: "custom-class")
+ end
+
+ test "inline check box with custom wrapper class" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', inline: true, wrapper_class: "custom-class")
+ end
+
+ test "custom check box with custom wrapper class" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', custom: true, wrapper_class: "custom-class"})
+ end
+
+ test "custom inline check box with custom wrapper class" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.check_box(:terms, {label: 'I agree to the terms', inline: true, custom: true, wrapper_class: "custom-class"})
+ end
end
diff --git a/test/bootstrap_fields_test.rb b/test/bootstrap_fields_test.rb
index b7d9445c9..26932f0c0 100644
--- a/test/bootstrap_fields_test.rb
+++ b/test/bootstrap_fields_test.rb
@@ -1,100 +1,280 @@
-require 'test_helper'
+require_relative "./test_helper"
class BootstrapFieldsTest < ActionView::TestCase
include BootstrapForm::Helper
- def setup
- setup_test_fixture
- end
+ setup :setup_test_fixture
test "color fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.color_field(:misc)
end
test "date fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.date_field(:misc)
end
test "date time fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.datetime_field(:misc)
end
test "date time local fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.datetime_local_field(:misc)
end
test "email fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.email_field(:misc)
end
test "file fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.file_field(:misc)
end
+ test "file field placeholder can be customized" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.file_field(:misc, placeholder: "Pick a file")
+ end
+
+ if ::Rails::VERSION::STRING > '5.1'
+ test "file field placeholder has appropriate `for` attribute when used in form_with" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, form_with_builder.file_field(:misc, id: "custom-id")
+ end
+ end
+
+ test "file fields are wrapped correctly with error" do
+ @user.errors.add(:misc, "error for test")
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.file_field(:misc) }
+ end
+
test "hidden fields are supported" do
expected = %{}
assert_equivalent_xml expected, @builder.hidden_field(:misc)
end
test "month local fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.month_field(:misc)
end
test "number fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.number_field(:misc)
end
test "password fields are wrapped correctly" do
- expected = %{
A good password should be at least six characters long
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+ A good password should be at least six characters long
+
+ HTML
assert_equivalent_xml expected, @builder.password_field(:password)
end
test "phone/telephone fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.phone_field(:misc)
assert_equivalent_xml expected, @builder.telephone_field(:misc)
end
test "range fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.range_field(:misc)
end
test "search fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.search_field(:misc)
end
test "text areas are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_area(:comments)
end
+ if ::Rails::VERSION::STRING > '5.1' && ::Rails::VERSION::STRING < '5.2'
+ test "text areas are wrapped correctly form_with Rails 5.1" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, form_with_builder.text_area(:comments)
+ end
+ end
+
+ if ::Rails::VERSION::STRING > '5.2'
+ test "text areas are wrapped correctly form_with Rails 5.2+" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, form_with_builder.text_area(:comments)
+ end
+ end
+
test "text fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email)
end
+ test "text fields are wrapped correctly when horizontal and form-row given" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @horizontal_builder.text_field(:email, wrapper_class: "form-row")
+ assert_equivalent_xml expected, @horizontal_builder.text_field(:email, wrapper: { class: "form-row" })
+ end
+
+ test "field 'id' attribute is used to specify label 'for' attribute" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.text_field(:email, id: :custom_id)
+ end
+
test "time fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.time_field(:misc)
end
test "url fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.url_field(:misc)
end
test "week fields are wrapped correctly" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.week_field(:misc)
end
@@ -107,7 +287,15 @@ def setup
end
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
@@ -120,7 +308,15 @@ def setup
end
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
@@ -133,20 +329,81 @@ def setup
end
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
test "fields_for correctly passes inline style from parent builder" do
@user.address = Address.new(street: '123 Main Street')
+ # NOTE: This test works with even if you use `fields_for_without_bootstrap`
output = bootstrap_form_for(@user, layout: :inline) do |f|
f.fields_for :address do |af|
af.text_field(:street)
end
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
+
+ if ::Rails::VERSION::STRING >= '5.1'
+ test "fields correctly uses options from parent builder" do
+ @user.address = Address.new(street: '123 Main Street')
+
+ bootstrap_form_with(model: @user,
+ control_col: "control-style",
+ inline_errors: false,
+ label_col: "label-style",
+ label_errors: true,
+ layout: :inline) do |f|
+ f.fields :address do |af|
+ af.text_field(:street)
+ assert_equal "control-style", af.control_col
+ assert_equal false, af.inline_errors
+ assert_equal "label-style", af.label_col
+ assert_equal true, af.label_errors
+ assert_equal :inline, af.layout
+ end
+ end
+ end
+ end
+
+ test "fields_for_without_bootstrap does not use options from parent builder" do
+ @user.address = Address.new(street: '123 Main Street')
+
+ bootstrap_form_for(@user,
+ control_col: "control-style",
+ inline_errors: false,
+ label_col: "label-style",
+ label_errors: true,
+ layout: :inline) do |f|
+ f.fields_for_without_bootstrap :address do |af|
+ af.text_field(:street)
+ assert_not_equal "control-style", af.control_col
+ assert_not_equal false, af.inline_errors
+ assert_not_equal "label-style", af.label_col
+ assert_not_equal true, af.label_errors
+ assert_not_equal :inline, af.layout
+ end
+ end
+ end
end
diff --git a/test/bootstrap_form_group_test.rb b/test/bootstrap_form_group_test.rb
index 5b2ac0acb..7d25beacf 100644
--- a/test/bootstrap_form_group_test.rb
+++ b/test/bootstrap_form_group_test.rb
@@ -1,64 +1,131 @@
-require 'test_helper'
+require_relative "./test_helper"
class BootstrapFormGroupTest < ActionView::TestCase
include BootstrapForm::Helper
- def setup
- setup_test_fixture
- end
+ setup :setup_test_fixture
test "changing the label text via the label option parameter" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, label: 'Email Address')
end
test "changing the label text via the html_options label hash" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, label: {text: 'Email Address'})
end
test "hiding a label" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, hide_label: true)
end
test "adding a custom label class via the label_class parameter" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, label_class: 'btn')
end
test "adding a custom label class via the html_options label hash" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, label: {class: 'btn'})
end
test "adding a custom label and changing the label text via the html_options label hash" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, label: {class: 'btn', text: "Email Address"})
end
test "skipping a label" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, skip_label: true)
end
test "preventing a label from having the required class" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, skip_required: true)
end
+ test "label as placeholder" do
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, @builder.text_field(:email, label_as_placeholder: true)
+ end
+
test "adding prepend text" do
- expected = %{
@
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ @
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, prepend: '@')
end
test "adding append text" do
- expected = %{
.00
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ .00
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, append: '.00')
end
test "append and prepend button" do
- prefix = %{
}
@@ -73,22 +140,101 @@ def setup
end
test "adding both prepend and append text" do
- expected = %{
$
.00
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ $
+
+
+
+ .00
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, prepend: '$', append: '.00')
end
+ test "adding both prepend and append text with validation error" do
+ @user.email = nil
+ assert @user.invalid?
+
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.text_field :email, prepend: '$', append: '.00' }
+ end
+
test "help messages for default forms" do
- expected = %{
This is required
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+ This is required
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, help: 'This is required')
end
test "help messages for horizontal forms" do
- expected = %{
This is required
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ This is required
+
+
+ HTML
assert_equivalent_xml expected, @horizontal_builder.text_field(:email, help: "This is required")
end
test "help messages to look up I18n automatically" do
- expected = %{
A good password should be at least six characters long
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+ A good password should be at least six characters long
+
+ HTML
+ assert_equivalent_xml expected, @builder.text_field(:password)
+ end
+
+ test "help messages to look up I18n automatically using HTML key" do
+ I18n.backend.store_translations(:en, activerecord: {
+ help: {
+ user: {
+ password: {
+ html: 'A good password should be at least six characters long'
+ }
+ }
+ }
+ })
+
+ expected = <<-HTML.strip_heredoc
+
+
+
+ A good password should be at least six characters long
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:password)
end
@@ -111,98 +257,222 @@ def setup
end
test "help messages to ignore translation when user disables help" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:password, help: false)
end
test "form_group creates a valid structure and allows arbitrary html to be added via a block" do
output = @horizontal_builder.form_group :nil, label: { text: 'Foo' } do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "form_group adds a spacer when no label exists for a horizontal form" do
output = @horizontal_builder.form_group do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "form_group renders the label correctly" do
output = @horizontal_builder.form_group :email, label: { text: 'Custom Control' } do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "form_group accepts class thorugh options hash" do
output = @horizontal_builder.form_group :email, class: "foo" do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "form_group accepts class thorugh options hash without needing a name" do
output = @horizontal_builder.form_group class: "foo" do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
+ assert_equivalent_xml expected, output
+ end
+
+ test "form_group horizontal lets caller override .row" do
+ output = @horizontal_builder.form_group class: "form-row" do
+ %{}.html_safe
+ end
+
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "form_group overrides the label's 'class' and 'for' attributes if others are passed" do
output = @horizontal_builder.form_group nil, label: { text: 'Custom Control', class: 'foo', for: 'bar' } do
- %{
Bar
}.html_safe
+ %{}.html_safe
end
- expected = %{
Bar
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
- test 'form_group renders the "error" class and message corrrectly when object is invalid' do
+ test 'upgrade doc for form_group renders the "error" class and message corrrectly when object is invalid' do
@user.email = nil
- @user.valid?
+ assert @user.invalid?
output = @builder.form_group :email do
- %{
Bar
}.html_safe
+ html = %{
Bar
}.html_safe
+ html.concat(content_tag(:div, @user.errors[:email].join(", "), class: "invalid-feedback", style: "display: block;")) unless @user.errors[:email].empty?
+ html
end
- expected = %{
Bar
can't be blank, is too short (minimum is 5 characters)
}
+ expected = <<-HTML.strip_heredoc
+
+
Bar
+
can't be blank, is too short (minimum is 5 characters)
+
+ HTML
+ assert_equivalent_xml expected, output
+ end
+
+ test 'upgrade doc for form_group renders check box corrrectly when object is invalid' do
+ @user.errors.add(:misc, "Must select one.")
+
+ output = bootstrap_form_for(@user) do |f|
+ f.form_group :email do
+ f.radio_button(:misc, "primary school")
+ .concat(f.radio_button(:misc, "high school"))
+ .concat(f.radio_button(:misc, "university", error_message: true))
+ end
+ end
+
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
test "adds class to wrapped form_group by a field" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.search_field(:misc, wrapper_class: 'none-margin')
end
test "adds class to wrapped form_group by a field with errors" do
@user.email = nil
- @user.valid?
-
- expected = %{
can't be blank, is too short (minimum is 5 characters)
can't be blank, is too short (minimum is 5 characters)
+
+ HTML
assert_equivalent_xml expected, @builder.email_field(:email, wrapper_class: 'none-margin')
end
test "adds class to wrapped form_group by a field with errors when bootstrap_form_for is used" do
@user.email = nil
- @user.valid?
+ assert @user.invalid?
output = bootstrap_form_for(@user) do |f|
f.text_field(:email, help: 'This is required', wrapper_class: 'none-margin')
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, output
end
@@ -211,7 +481,13 @@ def setup
@horizontal_builder.submit
end
- expected = %{
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
@@ -220,34 +496,55 @@ def setup
@horizontal_builder.submit
end
- expected = %{
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
- test "adding an icon to a field" do
- expected = %{
}
- assert_equivalent_xml expected, @builder.email_field(:misc, icon: 'ok')
- end
-
+ # TODO: What is this actually testing? Improve the test name
test "single form_group call in horizontal form should not be smash design" do
- output = ''
output = @horizontal_builder.form_group do
"Hallo"
end
output = output + @horizontal_builder.text_field(:email)
- expected = %{
Hallo
}
+ expected = <<-HTML.strip_heredoc
+
+
Hallo
+
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "adds data-attributes (or any other options) to wrapper" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.search_field(:misc, wrapper: { data: { foo: 'bar' } })
end
test "passing options to a form control get passed through" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.text_field(:email, autofocus: true)
end
@@ -256,22 +553,40 @@ def setup
nil
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+
+
+ HTML
assert_equivalent_xml expected, output
end
test "custom form group layout option" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email, layout: :inline }
end
test "non-default column span on form is reflected in form_group" do
non_default_horizontal_builder = BootstrapForm::FormBuilder.new(:user, @user, self, { layout: :horizontal, label_col: "col-sm-3", control_col: "col-sm-9" })
output = non_default_horizontal_builder.form_group do
- %{
}
assert_equivalent_xml expected, output
end
test ":input_group_class should apply to input-group" do
- expected = %{
}
+ expected = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+
+
+ HTML
assert_equivalent_xml expected, @builder.email_field(:email, append: @builder.primary('Subscribe'), input_group_class: 'input-group-lg')
end
end
diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb
index ff286ecc3..12596bcdd 100644
--- a/test/bootstrap_form_test.rb
+++ b/test/bootstrap_form_test.rb
@@ -1,224 +1,757 @@
-require 'test_helper'
+require_relative "./test_helper"
class BootstrapFormTest < ActionView::TestCase
include BootstrapForm::Helper
- def setup
- setup_test_fixture
- end
+ setup :setup_test_fixture
test "default-style forms" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| nil }
end
+ test "default-style form fields layout horizontal" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user) do |f|
+ f.email_field(:email, layout: :horizontal)
+ .concat(f.check_box(:terms, label: 'I agree to the terms'))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :horizontal))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]], layout: :horizontal))
+ end
+
+ assert_equivalent_xml expected, actual
+ # See the rendered output at: https://www.bootply.com/S2WFzEYChf
+ end
+
+ test "default-style form fields layout inline" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user) do |f|
+ f.email_field(:email, layout: :inline)
+ .concat(f.check_box(:terms, label: 'I agree to the terms', inline: true))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :inline))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]], layout: :inline))
+ end
+
+ assert_equivalent_xml expected, actual
+ # See the rendered output at: https://www.bootply.com/fH5sF4fcju
+ # Note that the baseline of the label text to the left of the two radio buttons
+ # isn't aligned with the text of the radio button labels.
+ # TODO: Align baseline better.
+ end
+
+ if ::Rails::VERSION::STRING >= '5.1'
+ # No need to test 5.2 separately for this case, since 5.2 does *not*
+ # generate a default ID for the form element.
+ test "default-style forms bootstrap_form_with Rails 5.1+" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ assert_equivalent_xml expected, bootstrap_form_with(model: @user) { |f| nil }
+ end
+ end
+
test "inline-style forms" do
- expected = %{}
- assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :inline) { |f| nil }
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user, layout: :inline) do |f|
+ f.email_field(:email)
+ .concat(f.check_box(:terms, label: 'I agree to the terms'))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]]))
+ end
+
+ assert_equivalent_xml expected, actual
end
test "horizontal-style forms" do
- expected = %{}
- assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email }
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user, layout: :horizontal) do |f|
+ f.email_field(:email)
+ .concat(f.check_box(:terms, label: 'I agree to the terms'))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]]))
+ end
+
+ assert_equivalent_xml expected, actual
+ end
+
+ test "horizontal-style form fields layout default" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user, layout: :horizontal) do |f|
+ f.email_field(:email, layout: :default)
+ .concat(f.check_box(:terms, label: 'I agree to the terms'))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :default))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]], layout: :default))
+ end
+
+ assert_equivalent_xml expected, actual
+ # See the rendered output at: https://www.bootply.com/4f23be1nLn
+ end
+
+ test "horizontal-style form fields layout inline" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+
+ collection = [Address.new(id: 1, street: 'Foo'), Address.new(id: 2, street: 'Bar')]
+ actual = bootstrap_form_for(@user, layout: :horizontal) do |f|
+ f.email_field(:email, layout: :inline)
+ .concat(f.check_box(:terms, label: 'I agree to the terms', inline: true))
+ .concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :inline))
+ .concat(f.select(:status, [['activated', 1], ['blocked', 2]], layout: :inline))
+ end
+
+ assert_equivalent_xml expected, actual
+ # See the rendered output here: https://www.bootply.com/Qby9FC9d3u#
end
test "existing styles aren't clobbered when specifying a form style" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal, html: { class: "my-style" }) { |f| f.email_field :email }
end
test "given role attribute should not be covered by default role attribute" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_for(@user, html: { role: 'not-a-form'}) {|f| nil}
end
test "bootstrap_form_tag acts like a form tag" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_tag(url: '/users') { |f| f.text_field :email, label: "Your Email" }
end
+ test "bootstrap_form_for does not clobber custom options" do
+ expected = <<-HTML.strip_heredoc
+
+ HTML
+ assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.text_field :email, name: 'NAME', id: "ID" }
+ end
+
test "bootstrap_form_tag does not clobber custom options" do
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_tag(url: '/users') { |f| f.text_field :email, name: 'NAME', id: "ID" }
end
test "bootstrap_form_tag allows an empty name for checkboxes" do
- checkbox = if ::Rails::VERSION::STRING >= '5.1'
- %{}
+ if ::Rails::VERSION::STRING >= '5.1'
+ id = 'misc'
+ name = 'misc'
else
- %{}
+ id = '_misc'
+ name = '[misc]'
end
- expected = %{}
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_tag(url: '/users') { |f| f.check_box :misc }
end
test "errors display correctly and inline_errors are turned off by default when label_errors is true" do
@user.email = nil
- @user.valid?
-
- expected = %{}
+ assert @user.invalid?
+
+ expected = <<-HTML.strip_heredoc
+
+ HTML
assert_equivalent_xml expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email }
end
test "errors display correctly and inline_errors can also be on when label_errors is true" do
@user.email = nil
- @user.valid?
-
- expected = %{}
+ assert @user.invalid?
+
+ expected = <<-HTML.strip_heredoc
+