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 +
+ +
+ + +
+
+ + + A good password should be at least six characters long +
+
+ + + +
+ +
+``` + +`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
``` @@ -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 @@ +

Horizontal Form

+ +<%= form_with_source do %> + <%= bootstrap_form_for @user, layout: :horizontal do |form| %> + <%= form.email_field :email, placeholder: "Enter Email", label: "Email address", help: "We'll never share your email with anyone else" %> + <%= form.password_field :password, placeholder: "Password" %> + <%= form.select :status, [['activated', 1], ['blocked', 2]], prompt: "Please Select" %> + <%= form.text_area :misc %> + <%= form.check_box :terms, label: "Agree to Terms" %> + <%= form.collection_check_boxes :misc, @collection, :id, :street %> + <%= form.collection_radio_buttons :misc, @collection, :id, :street %> + <%= form.file_field :misc %> + <%= form.datetime_select :misc, include_blank: true %> + + <%= form.submit %> + <% end %> +<% end %> + +

With Validation Error

+ +<%= form_with_source do %> + <%= bootstrap_form_for @user_with_error, layout: :horizontal do |form| %> + <%= form.alert_message "This is an alert" %> + <%= form.error_summary %> + <%= form.email_field :email, placeholder: "Enter Email", label: "Email address", help: "We'll never share your email with anyone else" %> + <%= form.collection_check_boxes :misc, @collection, :id, :street %> + <%= form.submit %> + <% end %> +<% end %> + +

Inline Form

+ +<%= form_with_source do %> + <%= bootstrap_form_for @user, layout: :inline do |form| %> + <%= form.email_field :email, placeholder: "Enter Email", label: "Email address", help: "We'll never share your email with anyone else" %> + <%= form.password_field :password, placeholder: "Password" %> + <%= form.check_box :terms, label: "Agree to Terms" %> + <%= form.collection_check_boxes :misc, @collection, :id, :street %> + <%= form.submit %> + <% end %> +<% end %> + +

Simple

+ +<%= form_with_source do %> + <%= bootstrap_form_for @user, url: "/" do |form| %> + <%= form.email_field :email, placeholder: "Enter Email", label: "Email address", help: "We'll never share your email with anyone else" %> + <%= form.password_field :password, placeholder: "Password" %> + <%= form.check_box :terms, label: "Agree to Terms" %> + <%= form.collection_check_boxes :misc, @collection, :id, :street %> + <%= form.submit %> + <% end %> +<% end %> diff --git a/demo/app/views/layouts/application.html.erb b/demo/app/views/layouts/application.html.erb new file mode 100644 index 000000000..d1cef12b5 --- /dev/null +++ b/demo/app/views/layouts/application.html.erb @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + Hello, world! + <%= csrf_meta_tags %> + + + +
+ <%= yield %> +
+ + + + + + + + + + + + + + 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 = %{
} + expected = <<-HTML.strip_heredoc + +
+ +
+ + +
+
+ + +
+
+ HTML assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street, checked: 1) assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street, checked: collection.first) @@ -83,7 +283,20 @@ def setup test 'collection_check_boxes renders with multiple checked options 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, checked: [1, 2]) assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :id, :street, checked: collection) @@ -91,42 +304,136 @@ def setup test 'collection_check_boxes sanitizes values when generating label `for`' do collection = [Address.new(id: 1, street: 'Foo St')] - expected = %{
} - + expected = <<-HTML.strip_heredoc + +
+ +
+ + +
+
+ HTML assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, :street, :street) end test 'collection_check_boxes renders multiple checkboxes with labels defined by Proc :text_method 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, Proc.new { |a| a.street.reverse }) end test 'collection_check_boxes renders multiple checkboxes with values defined by Proc :value_method 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, Proc.new { |a| "address_#{a.id}" }, :street) end test 'collection_check_boxes renders multiple checkboxes with labels defined by lambda :text_method 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, lambda { |a| a.street.reverse }) end test 'collection_check_boxes renders multiple checkboxes with values defined by lambda :value_method 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, lambda { |a| "address_#{a.id}" }, :street) end test 'collection_check_boxes renders with checked option correctly with Proc :value_method' 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, Proc.new { |a| "address_#{a.id}" }, :street, checked: "address_1") assert_equivalent_xml expected, @builder.collection_check_boxes(:misc, collection, Proc.new { |a| "address_#{a.id}" }, :street, checked: collection.first) @@ -134,11 +441,269 @@ def setup test 'collection_check_boxes renders with multiple checked options correctly with lambda :value_method' 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, 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 +
+ + +
+ +
+ + +
+
+ + +
a box must be checked
+
+
+
+ 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 +
+ + +
+ +
+ + +
+
+ + +
error for test
+
+
+
+ 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 +
+ +
+ + + +
You must accept the terms.
+
+
+ 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 +
+ +
+ + + +
You must accept the terms.
+
+
+ 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 +
+ +
+ +
+ + +
error for test
+
+
+
+ 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 = %{
} + prefix = %{
} field = %{} button_prepend = %{} button_append = %{} @@ -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 +
+ +
+ +
+
+ $
+
+ +
+ .00 +
+
can't be blank, is too short (minimum is 5 characters) +
+
+
+ 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

} + 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 +
+ +
+
+ + +
+
+ + +
+
+ + +
Must select one.
+
+
+
+ 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 = %{
} + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
+
+ +
+
+ +
+
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 +
+ +
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ 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 - %{

Bar

}.html_safe + %{}.html_safe end - expected = %{

Bar

} + expected = <<-HTML.strip_heredoc +
+
+ +
+
+ HTML assert_equivalent_xml expected, output end @@ -279,12 +594,22 @@ def setup frozen_horizontal_builder = BootstrapForm::FormBuilder.new(:user, @user, self, { layout: :horizontal, label_col: "col-sm-3".freeze, control_col: "col-sm-9".freeze }) output = frozen_horizontal_builder.form_group { 'test' } - expected = %{
test
} + expected = %{
test
} 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 = %{
#{checkbox}
} + 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 = %{
can't be blank, is too short (minimum is 5 characters)
} + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
+ +
+ + +
can't be blank, is too short (minimum is 5 characters) +
+ + HTML assert_equivalent_xml expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } end test "label error messages use humanized attribute names" do - I18n.backend.store_translations(:en, {activerecord: {attributes: {user: {email: 'Your e-mail address'}}}}) - - @user.email = nil - @user.valid? - - expected = %{
can't be blank, is too short (minimum is 5 characters)
} - assert_equivalent_xml expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } - - I18n.backend.store_translations(:en, {activerecord: {attributes: {user: {email: nil}}}}) + begin + I18n.backend.store_translations(:en, {activerecord: {attributes: {user: {email: 'Your e-mail address'}}}}) + + @user.email = nil + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
+ +
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } + + ensure + I18n.backend.store_translations(:en, {activerecord: {attributes: {user: {email: nil}}}}) + end end test "alert message is wrapped correctly" do @user.email = nil - @user.valid? - expected = %{

Please fix the following errors:

  • Email can't be blank
  • Email is too short (minimum is 5 characters)
  • Terms must be accepted
} + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+ HTML assert_equivalent_xml expected, @builder.alert_message('Please fix the following errors:') end test "changing the class name for the alert message" do @user.email = nil - @user.valid? - expected = %{

Please fix the following errors:

  • Email can't be blank
  • Email is too short (minimum is 5 characters)
  • Terms must be accepted
} + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+ HTML assert_equivalent_xml expected, @builder.alert_message('Please fix the following errors:', class: 'my-css-class') end test "alert_message contains the error summary when inline_errors are turned off" do @user.email = nil - @user.valid? + assert @user.invalid? output = bootstrap_form_for(@user, inline_errors: false) do |f| f.alert_message('Please fix the following errors:') end - expected = %{

Please fix the following errors:

  • Email can't be blank
  • Email is too short (minimum is 5 characters)
  • Terms must be accepted
} + expected = <<-HTML.strip_heredoc +
+ +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+
+ HTML assert_equivalent_xml expected, output end test "alert_message allows the error_summary to be turned off" do @user.email = nil - @user.valid? + assert @user.invalid? output = bootstrap_form_for(@user, inline_errors: false) do |f| f.alert_message('Please fix the following errors:', error_summary: false) end - expected = %{

Please fix the following errors:

} + expected = <<-HTML.strip_heredoc +
+ +
+

Please fix the following errors:

+
+
+ HTML assert_equivalent_xml expected, output end test "alert_message allows the error_summary to be turned on with inline_errors also turned on" do @user.email = nil - @user.valid? + assert @user.invalid? output = bootstrap_form_for(@user, inline_errors: true) do |f| f.alert_message('Please fix the following errors:', error_summary: true) end - expected = %{

Please fix the following errors:

  • Email can't be blank
  • Email is too short (minimum is 5 characters)
  • Terms must be accepted
} + expected = <<-HTML.strip_heredoc +
+ +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+
+ HTML assert_equivalent_xml expected, output end test "error_summary returns an unordered list of errors" do @user.email = nil - @user.valid? - - expected = %{
  • Email can't be blank
  • Email is too short (minimum is 5 characters)
  • Terms must be accepted
} + assert @user.invalid? + + expected = <<-HTML.strip_heredoc +
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+ HTML assert_equivalent_xml expected, @builder.error_summary end + test "error_summary returns nothing if no errors" do + @user.terms = true + assert @user.valid? + + assert_equal nil, @builder.error_summary + end + test 'errors_on renders the errors for a specific attribute when invalid' do @user.email = nil - @user.valid? + assert @user.invalid? - expected = %{
Email can't be blank, Email is too short (minimum is 5 characters)
} + expected = <<-HTML.strip_heredoc +
Email can't be blank, Email is too short (minimum is 5 characters)
+ HTML assert_equivalent_xml expected, @builder.errors_on(:email) end test "custom label width for horizontal forms" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ +
+ +
+ +
+
+
+ HTML assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email, label_col: 'col-sm-1' } end test "offset for form group without label respects label width for horizontal forms" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ +
+
+ +
+
+
+ HTML assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal, label_col: 'col-md-2', control_col: 'col-md-10') { |f| f.form_group { f.submit } } end + test "offset for form group without label respects multiple label widths for horizontal forms" do + expected = <<-HTML.strip_heredoc +
+ +
+
+ +
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal, label_col: 'col-sm-4 col-md-2', control_col: 'col-sm-8 col-md-10') { |f| f.form_group { f.submit } } + end + test "custom input width for horizontal forms" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ +
+ +
+ +
+
+
+ HTML assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email, control_col: 'col-sm-5' } end test "the field contains the error and is not wrapped in div.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') end - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ +
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ HTML assert_equivalent_xml expected, output end test "the field is wrapped with div.field_with_errors when form_for is used" do @user.email = nil - @user.valid? + assert @user.invalid? output = form_for(@user, builder: BootstrapForm::FormBuilder) do |f| f.text_field(:email, help: 'This is required') end - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ +
+
+ +
+
+ +
+
can't be blank, is too short (minimum is 5 characters) +
+ + HTML assert_equivalent_xml expected, output end test "help is preserved when inline_errors: false is passed to bootstrap_form_for" do @user.email = nil - @user.valid? + assert @user.invalid? output = bootstrap_form_for(@user, inline_errors: false) do |f| f.text_field(:email, help: 'This is required') end - expected = %{
This is required
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + This is required +
+
+ HTML assert_equivalent_xml expected, output end test "help translations do not escape HTML when _html is appended to the name" do - I18n.backend.store_translations(:en, {activerecord: {help: {user: {email_html: "This is useful help"}}}}) - - output = bootstrap_form_for(@user) do |f| - f.text_field(:email) + begin + I18n.backend.store_translations(:en, {activerecord: {help: {user: {email_html: "This is useful help"}}}}) + + output = bootstrap_form_for(@user) do |f| + f.text_field(:email) + end + + expected = <<-HTML.strip_heredoc +
+ +
+ + + This is useful help +
+
+ HTML + assert_equivalent_xml expected, output + ensure + I18n.backend.store_translations(:en, {activerecord: {help: {user: {email_html: nil}}}}) end - - expected = %{
This is useful help
} - assert_equivalent_xml expected, output - - I18n.backend.store_translations(:en, {activerecord: {help: {user: {email_html: nil}}}}) end test "allows the form object to be nil" do builder = BootstrapForm::FormBuilder.new :other_model, nil, self, {} - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, builder.text_field(:email) end test 'errors_on hide attribute name in message' do @user.email = nil - @user.valid? + assert @user.invalid? expected = %{
can't be blank, is too short (minimum is 5 characters)
} diff --git a/test/bootstrap_other_components_test.rb b/test/bootstrap_other_components_test.rb index 0933c56f2..0257c6076 100644 --- a/test/bootstrap_other_components_test.rb +++ b/test/bootstrap_other_components_test.rb @@ -1,34 +1,91 @@ -require 'test_helper' +require_relative "./test_helper" class BootstrapOtherComponentsTest < ActionView::TestCase include BootstrapForm::Helper - def setup - setup_test_fixture - end + setup :setup_test_fixture test "static control" do output = @horizontal_builder.static_control :email - expected = %{

steve@example.com

} + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML assert_equivalent_xml expected, output end - test "static control doesn't require an actual attribute" do - output = @horizontal_builder.static_control nil, label: "My Label" do - "this is a test" - end + test "static control can have custom_id" do + output = @horizontal_builder.static_control :email, id: 'custom_id' + + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML + assert_equivalent_xml expected, output + end - expected = %{

this is a test

} + test "static control doesn't require an actual attribute" do + output = @horizontal_builder.static_control nil, label: "My Label", value: "this is a test" + + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML assert_equivalent_xml expected, output end test "static control doesn't require a name" do - output = @horizontal_builder.static_control label: "Custom Label" do - "Custom Control" - end + output = @horizontal_builder.static_control label: "Custom Label", value: "Custom Control" + + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML + assert_equivalent_xml expected, output + end + + test "static control support a nil value" do + output = @horizontal_builder.static_control label: "Custom Label", value: nil + + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML + assert_equivalent_xml expected, output + end - expected = %{

Custom Control

} + test "static control won't overwrite a control_class that is passed by the user" do + output = @horizontal_builder.static_control :email, control_class: "test_class" + + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ HTML assert_equivalent_xml expected, output end @@ -37,7 +94,12 @@ def setup "this is a test" end - expected = %{
this is a test
} + expected = <<-HTML.strip_heredoc +
+ +
this is a test
+
+ HTML assert_equivalent_xml expected, output end @@ -46,7 +108,12 @@ def setup "this is a test" end - expected = %{
this is a test
} + expected = <<-HTML.strip_heredoc +
+ +
this is a test
+
+ HTML assert_equivalent_xml expected, output end @@ -55,10 +122,27 @@ def setup "Custom Control" end - expected = %{
Custom Control
} + expected = <<-HTML.strip_heredoc +
+ +
Custom Control
+
+ HTML assert_equivalent_xml expected, output end + test "regular button uses proper css classes" do + expected = %{} + assert_equivalent_xml expected, + @builder.button("I'm HTML! in a button!".html_safe) + end + + test "regular button can have extra css classes" do + expected = %{} + assert_equivalent_xml expected, + @builder.button("I'm HTML! in a button!".html_safe, extra_class: 'test-button') + end + test "submit button defaults to rails action name" do expected = %{} assert_equivalent_xml expected, @builder.submit @@ -69,6 +153,11 @@ def setup assert_equivalent_xml expected, @builder.submit("Submit Form") end + test "submit button can have extra css classes" do + expected = %{} + assert_equivalent_xml expected, @builder.submit("Submit Form", extra_class: 'test-button') + end + test "override submit button classes" do expected = %{} assert_equivalent_xml expected, @builder.submit("Submit Form", class: "btn btn-primary") @@ -79,6 +168,26 @@ def setup assert_equivalent_xml expected, @builder.primary("Submit Form") end + test "primary button can have extra css classes" do + expected = %{} + assert_equivalent_xml expected, @builder.primary("Submit Form", extra_class: 'test-button') + end + + test "primary button can render as HTML button" do + expected = %{} + assert_equivalent_xml expected, + @builder.primary("I'm HTML! Submit Form".html_safe, + render_as_button: true) + end + + test "primary button with content block renders as HTML button" do + output = @builder.primary do + "I'm HTML! Submit Form".html_safe + end + expected = %{} + assert_equivalent_xml expected, output + end + test "override primary button classes" do expected = %{} assert_equivalent_xml expected, @builder.primary("Submit Form", class: "btn btn-primary disabled") diff --git a/test/bootstrap_radio_button_test.rb b/test/bootstrap_radio_button_test.rb index 2ba720da1..c7f1ad20c 100644 --- a/test/bootstrap_radio_button_test.rb +++ b/test/bootstrap_radio_button_test.rb @@ -1,124 +1,531 @@ -require 'test_helper' +require_relative "./test_helper" class BootstrapRadioButtonTest < ActionView::TestCase include BootstrapForm::Helper - def setup - setup_test_fixture - end + setup :setup_test_fixture test "radio_button is wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button') end + test "radio_button no label" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + # ​ is a zero-width space. + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: '​'.html_safe) + end + + test "radio_button with error is wrapped correctly" do + @user.errors.add(:misc, "error for test") + expected = <<-HTML.strip_heredoc +
+ +
+ + +
error for test
+
+
+ HTML + actual = bootstrap_form_for(@user) do |f| + f.radio_button(:misc, '1', label: 'This is a radio button', error_message: true) + end + assert_equivalent_xml expected, actual + end + test "radio_button disabled label is set correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', disabled: true) end test "radio_button label class is set correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', label_class: 'btn') end + test "radio_button 'id' attribute is used to specify label 'for' attribute" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', id: 'custom_id') + end + test "radio_button inline label is set correctly" do - expected = %{} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', inline: true) end + test "radio_button inline label is set correctly from form level" do + expected = <<-HTML.strip_heredoc +
+ +
+ + +
+
+ HTML + actual = bootstrap_form_for(@user, layout: :inline) do |f| + f.radio_button(:misc, '1', label: 'This is a radio button') + end + assert_equivalent_xml expected, actual + end + test "radio_button disabled inline label is set correctly" do - expected = %{} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', inline: true, disabled: true) end test "radio_button inline label class is set correctly" do - expected = %{} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', inline: true, label_class: 'btn') end test 'collection_radio_buttons 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_radio_buttons(:misc, collection, :id, :street, label: 'This is a radio button collection', help: 'With a help!') end test 'collection_radio_buttons renders multiple radios 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_radio_buttons(:misc, collection, :id, :street) end + test 'collection_radio_buttons renders multiple radios 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 +
+ +
+ +
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_radio_buttons(:misc, collection, :id, :street) + end + assert_equivalent_xml expected, actual + end + test 'collection_radio_buttons renders inline radios 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_radio_buttons(:misc, collection, :id, :street, inline: true) end test 'collection_radio_buttons renders with checked option 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_radio_buttons(:misc, collection, :id, :street, checked: 1) end test 'collection_radio_buttons renders label defined by Proc 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_radio_buttons(:misc, collection, :id, Proc.new { |a| a.street.reverse }, label: 'This is a radio button collection', help: 'With a help!') end test 'collection_radio_buttons renders value defined by Proc 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_radio_buttons(:misc, collection, Proc.new { |a| "address_#{a.id}" }, :street, label: 'This is a radio button collection', help: 'With a help!') end test 'collection_radio_buttons renders multiple radios with label defined by Proc 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_radio_buttons(:misc, collection, :id, Proc.new { |a| a.street.reverse }) end test 'collection_radio_buttons renders multiple radios with value defined by Proc 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_radio_buttons(:misc, collection, Proc.new { |a| "address_#{a.id}" }, :street) end test 'collection_radio_buttons renders label defined by lambda 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_radio_buttons(:misc, collection, :id, lambda { |a| a.street.reverse }, label: 'This is a radio button collection', help: 'With a help!') end test 'collection_radio_buttons renders value defined by lambda 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_radio_buttons(:misc, collection, lambda { |a| "address_#{a.id}" }, :street, label: 'This is a radio button collection', help: 'With a help!') end test 'collection_radio_buttons renders multiple radios with label defined by lambda 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_radio_buttons(:misc, collection, :id, lambda { |a| a.street.reverse }) end test 'collection_radio_buttons renders multiple radios with value defined by lambda 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_radio_buttons(:misc, collection, lambda { |a| "address_#{a.id}" }, :street) end + test "radio_button is wrapped correctly with custom option set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', custom: true}) + end + + test "radio_button is wrapped correctly with id option and custom option set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', id: "custom_id", custom: true}) + end + + test "radio_button with error is wrapped correctly with custom option set" do + @user.errors.add(:misc, "error for test") + expected = <<-HTML.strip_heredoc +
+ +
+ + +
error for test
+
+
+ HTML + actual = bootstrap_form_for(@user) do |f| + f.radio_button(:misc, '1', {label: 'This is a radio button', custom: true, error_message: true}) + end + assert_equivalent_xml expected, actual + end + + test "radio_button is wrapped correctly with custom and inline options set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', inline: true, custom: true}) + end + + test "radio_button is wrapped correctly with custom and disabled options set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', disabled: true, custom: true}) + end + test "radio_button is wrapped correctly with custom, inline and disabled options set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', inline: true, disabled: true, custom: true}) + end + + test "radio button skip label" do + expected = <<-HTML.strip_heredoc +
+ +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', skip_label: true) + end + test "radio button hide label" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', hide_label: true) + end + + + test "radio button skip label with custom option set" do + expected = <<-HTML.strip_heredoc +
+ +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', custom: true, skip_label: true}) + end + + test "radio button hide label with custom option set" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', custom: true, hide_label: true}) + end + + test "radio button with custom wrapper class" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', wrapper_class: "custom-class") + end + + test "inline radio button with custom wrapper class" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', label: 'This is a radio button', inline: true, wrapper_class: "custom-class") + end + + test "custom radio button with custom wrapper class" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', custom: true, wrapper_class: "custom-class"}) + end + + test "custom inline radio button with custom wrapper class" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.radio_button(:misc, '1', {label: 'This is a radio button', inline: true, custom: true, wrapper_class: "custom-class"}) + end end diff --git a/test/bootstrap_selects_test.rb b/test/bootstrap_selects_test.rb index 929130a82..d11c9efa2 100644 --- a/test/bootstrap_selects_test.rb +++ b/test/bootstrap_selects_test.rb @@ -1,41 +1,127 @@ -require 'test_helper' +require_relative "./test_helper" class BootstrapSelectsTest < ActionView::TestCase include BootstrapForm::Helper - def setup - setup_test_fixture + setup :setup_test_fixture + + # Helper to generate options + def options_range(start: 1, stop: 31, selected: nil, months: false) + (start..stop).map do |n| + attr = (n == selected) ? 'selected="selected"' : "" + label = months ? Date::MONTHNAMES[n] : n + "" + end.join("\n") end test "time zone selects are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.time_zone_select(:misc) end + test "time zone selects are wrapped correctly with wrapper" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.time_zone_select(:misc, nil, wrapper: { class: "none-margin" }) + end + + test "time zone selects are wrapped correctly with error" do + @user.errors.add(:misc, "error for test") + expected = <<-HTML.strip_heredoc +
+ +
+ + +
error for test
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.time_zone_select(:misc) } + end + test "selects are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]]) end test "bootstrap_specific options are handled correctly" do - expected = %{
Help!
} + expected = <<-HTML.strip_heredoc +
+ + + Help! +
+ HTML assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]], label: "My Status Label", help: "Help!" ) end test "selects with options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]], prompt: "Please Select") end test "selects with both options and html_options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]], { prompt: "Please Select" }, class: "my-select") end + test "select 'id' attribute is used to specify label 'for' attribute" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]], { prompt: "Please Select" }, id: "custom_id") + end + test 'selects with addons are wrapped correctly' do expected = <<-HTML.strip_heredoc
- +
Before
} - select = @builder.select(:status) do - content_tag(:option) { 'Option 1' } + - content_tag(:option) { 'Option 2' } - end - assert_equivalent_xml expected, select + test "selects with block use block as content" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + select = @builder.select(:status) do + content_tag(:option) { 'Option 1' } + + content_tag(:option) { 'Option 2' } end + assert_equivalent_xml expected, select end test "selects render labels properly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.select(:status, [['activated', 1], ['blocked', 2]], label: "User Status") end test "collection_selects are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.collection_select(:status, [], :id, :name) end + test "collection_selects are wrapped correctly with wrapper" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.collection_select(:status, [], :id, :name, wrapper: { class: "none-margin" }) + end + + test "collection_selects are wrapped correctly with error" do + @user.errors.add(:status, "error for test") + expected = <<-HTML.strip_heredoc +
+ +
+ + +
error for test
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.collection_select(:status, [], :id, :name) } + end + test "collection_selects with options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.collection_select(:status, [], :id, :name, prompt: "Please Select") end test "collection_selects with options and html_options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.collection_select(:status, [], :id, :name, { prompt: "Please Select" }, class: "my-select") end test "grouped_collection_selects are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.grouped_collection_select(:status, [], :last, :first, :to_s, :to_s) end + test "grouped_collection_selects are wrapped correctly with wrapper" do + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML + assert_equivalent_xml expected, @builder.grouped_collection_select(:status, [], :last, :first, :to_s, :to_s, wrapper_class: "none-margin") + end + + test "grouped_collection_selects are wrapped correctly with error" do + @user.errors.add(:status, "error for test") + expected = <<-HTML.strip_heredoc +
+ +
+ + +
error for test
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.grouped_collection_select(:status, [], :last, :first, :to_s, :to_s) } + end + test "grouped_collection_selects with options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.grouped_collection_select(:status, [], :last, :first, :to_s, :to_s, prompt: "Please Select") end test "grouped_collection_selects with options and html_options are wrapped correctly" do - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.grouped_collection_select(:status, [], :last, :first, :to_s, :to_s, { prompt: "Please Select" }, class: "my-select") end test "date selects are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3)) do - expected = %{
\n\n\n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + +
+
+ HTML assert_equivalent_xml expected, @builder.date_select(:misc) end end + test "date selects are wrapped correctly with wrapper class" do + Timecop.freeze(Time.utc(2012, 2, 3)) do + expected = <<-HTML.strip_heredoc +
+ +
+ + + +
+
+ HTML + assert_equivalent_xml expected, @builder.date_select(:misc, wrapper_class: "none-margin") + end + end + + test "date selects inline when layout is horizontal" do + Timecop.freeze(Time.utc(2012, 2, 3)) do + expected = <<-HTML.strip_heredoc +
+ +
+ +
+
+ + + +
+
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user, layout: :horizontal) { |f| f.date_select(:misc) } + end + end + + test "date selects are wrapped correctly with error" do + @user.errors.add(:misc, "error for test") + Timecop.freeze(Time.utc(2012, 2, 3)) do + expected = <<-HTML.strip_heredoc +
+ +
+ +
+ + + +
error for test
+
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.date_select(:misc) } + end + end + test "date selects with options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3)) do - expected = %{
\n\n\n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + +
+
+ HTML + assert_equivalent_xml expected, @builder.date_select(:misc, include_blank: true) end end test "date selects with options and html_options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3)) do - expected = %{
\n\n\n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + +
+
+ HTML assert_equivalent_xml expected, @builder.date_select(:misc, { include_blank: true }, class: "my-date-select") end end test "time selects are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n\n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.time_select(:misc) end end + test "time selects are wrapped correctly with error" do + @user.errors.add(:misc, "error for test") + Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do + expected = <<-HTML.strip_heredoc +
+ +
+ +
+ + + + + : + +
error for test
+
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.time_select(:misc) } + end + end + test "time selects with options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n\n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.time_select(:misc, include_blank: true) end end test "time selects with options and html_options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n\n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.time_select(:misc, { include_blank: true }, class: "my-time-select") end end test "datetime selects are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n — \n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + — + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.datetime_select(:misc) end end + test "datetime selects are wrapped correctly with error" do + @user.errors.add(:misc, "error for test") + Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do + expected = <<-HTML.strip_heredoc +
+ +
+ +
+ + + + — + + : + +
error for test
+
+
+
+ HTML + assert_equivalent_xml expected, bootstrap_form_for(@user) { |f| f.datetime_select(:misc) } + end + end + test "datetime selects with options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n — \n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + — + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.datetime_select(:misc, include_blank: true) end end test "datetime selects with options and html_options are wrapped correctly" do Timecop.freeze(Time.utc(2012, 2, 3, 12, 0, 0)) do - expected = %{
\n\n\n — \n : \n
} + expected = <<-HTML.strip_heredoc +
+ +
+ + + + — + + : + +
+
+ HTML assert_equivalent_xml expected, @builder.datetime_select(:misc, { include_blank: true }, class: "my-datetime-select") end end diff --git a/test/gemfiles/5.0.gemfile b/test/gemfiles/5.0.gemfile index 4408d2821..99678fadb 100644 --- a/test/gemfiles/5.0.gemfile +++ b/test/gemfiles/5.0.gemfile @@ -5,10 +5,10 @@ gemspec path: "../../" gem "rails", "~> 5.0.0" group :test do + gem "minitest", "~> 5.10.3" gem "diffy" gem "equivalent-xml" gem "mocha" gem "sqlite3" gem "timecop", "~> 0.7.1" end - diff --git a/test/special_form_class_models_test.rb b/test/special_form_class_models_test.rb index cc88f836a..d5a04aec7 100644 --- a/test/special_form_class_models_test.rb +++ b/test/special_form_class_models_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require_relative "./test_helper" class SpecialFormClassModelsTest < ActionView::TestCase include BootstrapForm::Helper @@ -14,7 +14,12 @@ def user_klass.model_name @horizontal_builder = BootstrapForm::FormBuilder.new(:user, @user, self, {layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-10"}) I18n.backend.store_translations(:en, {activerecord: {help: {user: {password: "A good password should be at least six characters long"}}}}) - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.date_field(:misc) end @@ -24,7 +29,12 @@ def user_klass.model_name @horizontal_builder = BootstrapForm::FormBuilder.new(:user, @user, self, {layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-10"}) I18n.backend.store_translations(:en, {activerecord: {help: {user: {password: "A good password should be at least six characters long"}}}}) - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.date_field(:misc) end @@ -36,7 +46,12 @@ def user_klass.model_name @horizontal_builder = BootstrapForm::FormBuilder.new(:user, @user, self, {layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-10"}) I18n.backend.store_translations(:en, {activerecord: {help: {faux_user: {password: "A good password should be at least six characters long"}}}}) - expected = %{
} + expected = <<-HTML.strip_heredoc +
+ + +
+ HTML assert_equivalent_xml expected, @builder.date_field(:misc) end diff --git a/test/test_helper.rb b/test/test_helper.rb index f24b41149..ae85b32a9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,13 +1,13 @@ +ENV['RAILS_ENV'] ||= 'test' + require 'timecop' require 'diffy' require 'nokogiri' require 'equivalent-xml' -require 'mocha/mini_test' - -ENV["RAILS_ENV"] = "test" -require_relative "./dummy/config/environment.rb" +require_relative "../demo/config/environment.rb" require "rails/test_help" +require 'mocha/minitest' Rails.backtrace_cleaner.remove_silencers! @@ -38,6 +38,13 @@ def setup_test_fixture }) end + # Originally only used in one test file but placed here in case it's needed in others in the future. + def form_with_builder + builder = nil + bootstrap_form_with(model: @user) { |f| builder = f } + builder + end + def sort_attributes doc doc.dup.traverse do |node| if node.is_a?(Nokogiri::XML::Element) @@ -51,9 +58,10 @@ def sort_attributes doc end end + # Expected and actual are wrapped in a root tag to ensure proper XML structure def assert_equivalent_xml(expected, actual) - expected_xml = Nokogiri::XML(expected) - actual_xml = Nokogiri::XML(actual) + expected_xml = Nokogiri::XML("\n#{expected}\n") { |config| config.default_xml.noblanks } + actual_xml = Nokogiri::XML("\n#{actual}\n") { |config| config.default_xml.noblanks } ignored_attributes = %w(style data-disable-with) equivalent = EquivalentXml.equivalent?(expected_xml, actual_xml, { @@ -82,8 +90,8 @@ def assert_equivalent_xml(expected, actual) assert equivalent, lambda { # using a lambda because diffing is expensive Diffy::Diff.new( - sort_attributes(expected_xml.root), - sort_attributes(actual_xml.root) + sort_attributes(expected_xml.root).to_xml(indent: 2), + sort_attributes(actual_xml.root).to_xml(indent: 2) ).to_s(:color) } end