bootstrap_form 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -0
  3. data/.rubocop.yml +3 -2
  4. data/.travis.yml +7 -1
  5. data/CHANGELOG.md +15 -1
  6. data/CONTRIBUTING.md +11 -0
  7. data/Dangerfile +4 -4
  8. data/Gemfile +7 -2
  9. data/OLD-README.md +795 -0
  10. data/README.md +150 -93
  11. data/Rakefile +2 -4
  12. data/bootstrap_form.gemspec +2 -1
  13. data/demo/.postcssrc.yml +3 -0
  14. data/demo/app/assets/config/manifest.js +2 -0
  15. data/demo/app/assets/stylesheets/actiontext.scss +38 -0
  16. data/demo/app/assets/stylesheets/application.scss +1 -0
  17. data/demo/app/helpers/bootstrap_helper.rb +16 -10
  18. data/demo/app/javascript/channels/consumer.js +6 -0
  19. data/demo/app/javascript/channels/index.js +5 -0
  20. data/demo/app/javascript/packs/application.js +11 -0
  21. data/demo/app/models/user.rb +2 -0
  22. data/demo/app/views/active_storage/blobs/_blob.html.erb +14 -0
  23. data/demo/app/views/bootstrap/form.html.erb +2 -1
  24. data/demo/app/views/layouts/application.html.erb +3 -0
  25. data/demo/bin/webpack +15 -0
  26. data/demo/bin/webpack-dev-server +15 -0
  27. data/demo/config/application.rb +2 -3
  28. data/demo/config/environments/development.rb +3 -1
  29. data/demo/config/environments/production.rb +48 -0
  30. data/demo/config/webpack/development.js +5 -0
  31. data/demo/config/webpack/environment.js +3 -0
  32. data/demo/config/webpack/production.js +5 -0
  33. data/demo/config/webpack/test.js +5 -0
  34. data/demo/config/webpacker.yml +92 -0
  35. data/demo/db/schema.rb +63 -18
  36. data/demo/package.json +13 -1
  37. data/demo/test/fixtures/action_text/rich_texts.yml +4 -0
  38. data/demo/yarn.lock +6257 -0
  39. data/lib/bootstrap_form.rb +34 -8
  40. data/lib/bootstrap_form/action_view_extensions/form_helper.rb +71 -0
  41. data/lib/bootstrap_form/components.rb +17 -0
  42. data/lib/bootstrap_form/components/hints.rb +60 -0
  43. data/lib/bootstrap_form/components/labels.rb +56 -0
  44. data/lib/bootstrap_form/components/layout.rb +39 -0
  45. data/lib/bootstrap_form/components/validation.rb +61 -0
  46. data/lib/bootstrap_form/engine.rb +10 -0
  47. data/lib/bootstrap_form/form_builder.rb +54 -524
  48. data/lib/bootstrap_form/form_group.rb +64 -0
  49. data/lib/bootstrap_form/form_group_builder.rb +103 -0
  50. data/lib/bootstrap_form/helpers.rb +9 -0
  51. data/lib/bootstrap_form/helpers/bootstrap.rb +39 -31
  52. data/lib/bootstrap_form/inputs.rb +40 -0
  53. data/lib/bootstrap_form/inputs/base.rb +40 -0
  54. data/lib/bootstrap_form/inputs/check_box.rb +89 -0
  55. data/lib/bootstrap_form/inputs/collection_check_boxes.rb +23 -0
  56. data/lib/bootstrap_form/inputs/collection_radio_buttons.rb +21 -0
  57. data/lib/bootstrap_form/inputs/collection_select.rb +25 -0
  58. data/lib/bootstrap_form/inputs/color_field.rb +14 -0
  59. data/lib/bootstrap_form/inputs/date_field.rb +14 -0
  60. data/lib/bootstrap_form/inputs/date_select.rb +14 -0
  61. data/lib/bootstrap_form/inputs/datetime_field.rb +14 -0
  62. data/lib/bootstrap_form/inputs/datetime_local_field.rb +14 -0
  63. data/lib/bootstrap_form/inputs/datetime_select.rb +14 -0
  64. data/lib/bootstrap_form/inputs/email_field.rb +14 -0
  65. data/lib/bootstrap_form/inputs/file_field.rb +35 -0
  66. data/lib/bootstrap_form/inputs/grouped_collection_select.rb +29 -0
  67. data/lib/bootstrap_form/inputs/inputs_collection.rb +44 -0
  68. data/lib/bootstrap_form/inputs/month_field.rb +14 -0
  69. data/lib/bootstrap_form/inputs/number_field.rb +14 -0
  70. data/lib/bootstrap_form/inputs/password_field.rb +14 -0
  71. data/lib/bootstrap_form/inputs/phone_field.rb +14 -0
  72. data/lib/bootstrap_form/inputs/radio_button.rb +77 -0
  73. data/lib/bootstrap_form/inputs/range_field.rb +14 -0
  74. data/lib/bootstrap_form/inputs/rich_text_area.rb +23 -0
  75. data/lib/bootstrap_form/inputs/search_field.rb +14 -0
  76. data/lib/bootstrap_form/inputs/select.rb +22 -0
  77. data/lib/bootstrap_form/inputs/telephone_field.rb +14 -0
  78. data/lib/bootstrap_form/inputs/text_area.rb +14 -0
  79. data/lib/bootstrap_form/inputs/text_field.rb +14 -0
  80. data/lib/bootstrap_form/inputs/time_field.rb +14 -0
  81. data/lib/bootstrap_form/inputs/time_select.rb +14 -0
  82. data/lib/bootstrap_form/inputs/time_zone_select.rb +22 -0
  83. data/lib/bootstrap_form/inputs/url_field.rb +14 -0
  84. data/lib/bootstrap_form/inputs/week_field.rb +14 -0
  85. data/lib/bootstrap_form/version.rb +1 -1
  86. metadata +79 -6
  87. data/.rubocop_todo.yml +0 -104
  88. data/lib/bootstrap_form/aliasing.rb +0 -35
  89. data/lib/bootstrap_form/helper.rb +0 -52
@@ -1,13 +1,39 @@
1
- require "bootstrap_form/form_builder"
2
- require "bootstrap_form/helper"
1
+ # NOTE: The rich_text_area and rich_text_area_tag helpers are defined in a file with a different
2
+ # name and not in the usual autoload-reachable way.
3
+ # The following line is definitely need to make `bootstrap_form` work.
4
+ if ::Rails::VERSION::STRING > "6"
5
+ require Gem::Specification.find_by_name("actiontext").gem_dir + # rubocop:disable Rails/DynamicFindBy
6
+ "/app/helpers/action_text/tag_helper"
7
+ end
8
+ require "action_view"
9
+ require "action_pack"
10
+ require "bootstrap_form/action_view_extensions/form_helper"
3
11
 
4
12
  module BootstrapForm
5
- module Rails
6
- class Engine < ::Rails::Engine
7
- end
13
+ extend ActiveSupport::Autoload
14
+
15
+ eager_autoload do
16
+ autoload :FormBuilder
17
+ autoload :FormGroupBuilder
18
+ autoload :FormGroup
19
+ autoload :Components
20
+ autoload :Inputs
21
+ autoload :Helpers
8
22
  end
9
- end
10
23
 
11
- ActiveSupport.on_load(:action_view) do
12
- include BootstrapForm::Helper
24
+ def self.eager_load!
25
+ super
26
+ BootstrapForm::Components.eager_load!
27
+ BootstrapForm::Helpers.eager_load!
28
+ BootstrapForm::Inputs.eager_load!
29
+ end
30
+
31
+ mattr_accessor :field_error_proc
32
+ # rubocop:disable Style/ClassVars
33
+ @@field_error_proc = proc do |html_tag, _instance_tag|
34
+ html_tag
35
+ end
36
+ # rubocop:enable Style/ClassVars
13
37
  end
38
+
39
+ require "bootstrap_form/engine" if defined?(Rails)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module ActionViewExtensions
5
+ # This module creates BootstrapForm wrappers around the default form_with
6
+ # and form_for methods
7
+ #
8
+ # Example:
9
+ #
10
+ # bootstrap_form_for @user do |f|
11
+ # f.text_field :name
12
+ # end
13
+ #
14
+ # Example:
15
+ #
16
+ # bootstrap_form_with model: @user do |f|
17
+ # f.text_field :name
18
+ # end
19
+ module FormHelper
20
+ def bootstrap_form_for(record, options={}, &block)
21
+ options.reverse_merge!(builder: BootstrapForm::FormBuilder)
22
+
23
+ options = process_options(options)
24
+
25
+ with_bootstrap_form_field_error_proc do
26
+ form_for(record, options, &block)
27
+ end
28
+ end
29
+
30
+ def bootstrap_form_with(options={}, &block)
31
+ options.reverse_merge!(builder: BootstrapForm::FormBuilder)
32
+
33
+ options = process_options(options)
34
+
35
+ with_bootstrap_form_field_error_proc do
36
+ form_with(options, &block)
37
+ end
38
+ end
39
+
40
+ def bootstrap_form_tag(options={}, &block)
41
+ options[:acts_like_form_tag] = true
42
+
43
+ bootstrap_form_for("", options, &block)
44
+ end
45
+
46
+ private
47
+
48
+ def process_options(options)
49
+ options[:html] ||= {}
50
+ options[:html][:role] ||= "form"
51
+
52
+ options[:layout] == :inline &&
53
+ options[:html][:class] = [options[:html][:class], "form-inline"].compact.join(" ")
54
+
55
+ options
56
+ end
57
+
58
+ def with_bootstrap_form_field_error_proc
59
+ original_proc = ActionView::Base.field_error_proc
60
+ ActionView::Base.field_error_proc = BootstrapForm.field_error_proc
61
+ yield
62
+ ensure
63
+ ActionView::Base.field_error_proc = original_proc
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ ActiveSupport.on_load(:action_view) do
70
+ include BootstrapForm::ActionViewExtensions::FormHelper
71
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module Components
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Hints
8
+ autoload :Labels
9
+ autoload :Layout
10
+ autoload :Validation
11
+
12
+ include Hints
13
+ include Labels
14
+ include Layout
15
+ include Validation
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module Components
5
+ module Hints
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def generate_help(name, help_text)
11
+ return if help_text == false || inline_error?(name)
12
+
13
+ help_klass ||= "form-text text-muted"
14
+ help_text ||= get_help_text_by_i18n_key(name)
15
+ help_tag ||= :small
16
+
17
+ content_tag(help_tag, help_text, class: help_klass) if help_text.present?
18
+ end
19
+
20
+ def get_help_text_by_i18n_key(name)
21
+ return unless object
22
+
23
+ partial_scope = if object.class.respond_to?(:model_name)
24
+ object.class.model_name.name
25
+ else
26
+ object.class.name
27
+ end
28
+
29
+ # First check for a subkey :html, as it is also accepted by i18n, and the
30
+ # simple check for name would return an hash instead of a string (both
31
+ # with .presence returning true!)
32
+ help_text = nil
33
+ ["#{name}.html", name, "#{name}_html"].each do |scope|
34
+ break if help_text
35
+
36
+ help_text = scoped_help_text(scope, partial_scope)
37
+ end
38
+ help_text
39
+ end
40
+
41
+ def scoped_help_text(name, partial_scope)
42
+ underscored_scope = "activerecord.help.#{partial_scope.underscore}"
43
+ downcased_scope = "activerecord.help.#{partial_scope.downcase}"
44
+
45
+ help_text = translated_help_text(name, underscored_scope).presence
46
+
47
+ help_text ||= if (text = translated_help_text(name, downcased_scope).presence)
48
+ warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
49
+ text
50
+ end
51
+
52
+ help_text
53
+ end
54
+
55
+ def translated_help_text(name, scope)
56
+ ActiveSupport::SafeBuffer.new I18n.t(name, scope: scope, default: "")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module Components
5
+ module Labels
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def generate_label(id, name, options, custom_label_col, group_layout)
11
+ return if options.blank?
12
+
13
+ # id is the caller's options[:id] at the only place this method is called.
14
+ # The options argument is a small subset of the options that might have
15
+ # been passed to generate_label's caller, and definitely doesn't include
16
+ # :id.
17
+ options[:for] = id if acts_like_form_tag
18
+
19
+ options[:class] = label_classes(name, options, custom_label_col, group_layout)
20
+ options.delete(:class) if options[:class].none?
21
+
22
+ label(name, label_text(name, options), options.except(:text))
23
+ end
24
+
25
+ def label_classes(name, options, custom_label_col, group_layout)
26
+ classes = [options[:class], label_layout_classes(custom_label_col, group_layout)]
27
+
28
+ case options.delete(:required)
29
+ when true
30
+ classes << "required"
31
+ when nil, :default
32
+ classes << "required" if required_attribute?(object, name)
33
+ end
34
+
35
+ classes << "text-danger" if label_errors && error?(name)
36
+ classes.flatten.compact
37
+ end
38
+
39
+ def label_layout_classes(custom_label_col, group_layout)
40
+ if layout_horizontal?(group_layout)
41
+ ["col-form-label", (custom_label_col || label_col)]
42
+ elsif layout_inline?(group_layout)
43
+ ["mr-sm-2"]
44
+ end
45
+ end
46
+
47
+ def label_text(name, options)
48
+ if label_errors && error?(name)
49
+ (options[:text] || object.class.human_attribute_name(name)).to_s.concat(" #{get_error_messages(name)}")
50
+ else
51
+ options[:text]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module Components
5
+ module Layout
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def layout_default?(field_layout=nil)
11
+ [:default, nil].include? layout_in_effect(field_layout)
12
+ end
13
+
14
+ def layout_horizontal?(field_layout=nil)
15
+ layout_in_effect(field_layout) == :horizontal
16
+ end
17
+
18
+ def layout_inline?(field_layout=nil)
19
+ layout_in_effect(field_layout) == :inline
20
+ end
21
+
22
+ def field_inline_override?(field_layout=nil)
23
+ field_layout == :inline && layout != :inline
24
+ end
25
+
26
+ # true and false should only come from check_box and radio_button,
27
+ # and those don't have a :horizontal layout
28
+ def layout_in_effect(field_layout)
29
+ field_layout = :inline if field_layout == true
30
+ field_layout = :default if field_layout == false
31
+ field_layout || layout
32
+ end
33
+
34
+ def get_group_layout(group_layout)
35
+ group_layout || layout
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BootstrapForm
4
+ module Components
5
+ module Validation
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def error?(name)
11
+ object.respond_to?(:errors) && !(name.nil? || object.errors[name].empty?)
12
+ end
13
+
14
+ def required_attribute?(obj, attribute)
15
+ return false unless obj && attribute
16
+
17
+ target = obj.class == Class ? obj : obj.class
18
+
19
+ target_validators = if target.respond_to? :validators_on
20
+ target.validators_on(attribute).map(&:class)
21
+ else
22
+ []
23
+ end
24
+
25
+ presence_validator?(target_validators)
26
+ end
27
+
28
+ def presence_validator?(target_validators)
29
+ has_presence_validator = target_validators.include?(
30
+ ActiveModel::Validations::PresenceValidator
31
+ )
32
+
33
+ if defined? ActiveRecord::Validations::PresenceValidator
34
+ has_presence_validator |= target_validators.include?(
35
+ ActiveRecord::Validations::PresenceValidator
36
+ )
37
+ end
38
+
39
+ has_presence_validator
40
+ end
41
+
42
+ def inline_error?(name)
43
+ error?(name) && inline_errors
44
+ end
45
+
46
+ def generate_error(name)
47
+ return unless inline_error?(name)
48
+
49
+ help_text = get_error_messages(name)
50
+ help_klass = "invalid-feedback"
51
+ help_tag = :div
52
+
53
+ content_tag(help_tag, help_text, class: help_klass)
54
+ end
55
+
56
+ def get_error_messages(name)
57
+ object.errors[name].join(", ")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module BootstrapForm
6
+ class Engine < Rails::Engine
7
+ config.eager_load_namespaces << BootstrapForm
8
+ config.autoload_paths << File.expand_path("lib", __dir__)
9
+ end
10
+ end
@@ -1,20 +1,47 @@
1
- require_relative "aliasing"
2
- require_relative "helpers/bootstrap"
1
+ # require 'bootstrap_form/aliasing'
3
2
 
4
3
  module BootstrapForm
5
- # TODO: Refactor this class and remove the rubocop:disable
6
- class FormBuilder < ActionView::Helpers::FormBuilder # rubocop:disable Metrics/ClassLength
7
- extend BootstrapForm::Aliasing
8
- include BootstrapForm::Helpers::Bootstrap
9
-
10
- attr_reader :layout, :label_col, :control_col, :has_error, :inline_errors, :label_errors, :acts_like_form_tag
4
+ class FormBuilder < ActionView::Helpers::FormBuilder
5
+ attr_reader :layout, :label_col, :control_col, :has_error, :inline_errors,
6
+ :label_errors, :acts_like_form_tag
11
7
 
12
- FIELD_HELPERS = %w[color_field date_field datetime_field datetime_local_field
13
- email_field month_field number_field password_field phone_field
14
- range_field search_field telephone_field text_area text_field time_field
15
- url_field week_field].freeze
8
+ include BootstrapForm::Helpers::Bootstrap
16
9
 
17
- DATE_SELECT_HELPERS = %w[date_select time_select datetime_select].freeze
10
+ include BootstrapForm::FormGroupBuilder
11
+ include BootstrapForm::FormGroup
12
+ include BootstrapForm::Components
13
+
14
+ include BootstrapForm::Inputs::Base
15
+ include BootstrapForm::Inputs::CheckBox
16
+ include BootstrapForm::Inputs::CollectionCheckBoxes
17
+ include BootstrapForm::Inputs::CollectionRadioButtons
18
+ include BootstrapForm::Inputs::CollectionSelect
19
+ include BootstrapForm::Inputs::ColorField
20
+ include BootstrapForm::Inputs::DateField
21
+ include BootstrapForm::Inputs::DateSelect
22
+ include BootstrapForm::Inputs::DatetimeField
23
+ include BootstrapForm::Inputs::DatetimeLocalField
24
+ include BootstrapForm::Inputs::DatetimeSelect
25
+ include BootstrapForm::Inputs::EmailField
26
+ include BootstrapForm::Inputs::FileField
27
+ include BootstrapForm::Inputs::GroupedCollectionSelect
28
+ include BootstrapForm::Inputs::MonthField
29
+ include BootstrapForm::Inputs::NumberField
30
+ include BootstrapForm::Inputs::PasswordField
31
+ include BootstrapForm::Inputs::PhoneField
32
+ include BootstrapForm::Inputs::RadioButton
33
+ include BootstrapForm::Inputs::RangeField
34
+ include BootstrapForm::Inputs::RichTextArea if Rails::VERSION::MAJOR >= 6
35
+ include BootstrapForm::Inputs::SearchField
36
+ include BootstrapForm::Inputs::Select
37
+ include BootstrapForm::Inputs::TelephoneField
38
+ include BootstrapForm::Inputs::TextArea
39
+ include BootstrapForm::Inputs::TextField
40
+ include BootstrapForm::Inputs::TimeField
41
+ include BootstrapForm::Inputs::TimeSelect
42
+ include BootstrapForm::Inputs::TimeZoneSelect
43
+ include BootstrapForm::Inputs::UrlField
44
+ include BootstrapForm::Inputs::WeekField
18
45
 
19
46
  delegate :content_tag, :capture, :concat, to: :@template
20
47
 
@@ -23,278 +50,24 @@ module BootstrapForm
23
50
  @label_col = options[:label_col] || default_label_col
24
51
  @control_col = options[:control_col] || default_control_col
25
52
  @label_errors = options[:label_errors] || false
53
+
26
54
  @inline_errors = if options[:inline_errors].nil?
27
55
  @label_errors != true
28
56
  else
29
57
  options[:inline_errors] != false
30
58
  end
31
59
  @acts_like_form_tag = options[:acts_like_form_tag]
32
-
33
60
  super
34
61
  end
35
62
 
36
- FIELD_HELPERS.each do |method_name|
37
- with_method_name = "#{method_name}_with_bootstrap"
38
- without_method_name = "#{method_name}_without_bootstrap"
39
-
40
- define_method(with_method_name) do |name, options={}|
41
- form_group_builder(name, options) do
42
- prepend_and_append_input(name, options) do
43
- send(without_method_name, name, options)
44
- end
45
- end
46
- end
47
-
48
- bootstrap_method_alias method_name
49
- end
50
-
51
- DATE_SELECT_HELPERS.each do |method_name|
52
- with_method_name = "#{method_name}_with_bootstrap"
53
- without_method_name = "#{method_name}_without_bootstrap"
54
-
55
- define_method(with_method_name) do |name, options={}, html_options={}|
56
- form_group_builder(name, options, html_options) do
57
- html_class = control_specific_class(method_name)
58
- html_class = "#{html_class} form-inline" if @layout == :horizontal && options[:skip_inline].blank?
59
- content_tag(:div, class: html_class) do
60
- input_with_error(name) do
61
- send(without_method_name, name, options, html_options)
62
- end
63
- end
64
- end
65
- end
66
-
67
- bootstrap_method_alias method_name
68
- end
69
-
70
- def file_field_with_bootstrap(name, options={})
71
- options = options.reverse_merge(control_class: "custom-file-input")
72
- form_group_builder(name, options) do
73
- content_tag(:div, class: "custom-file") do
74
- input_with_error(name) do
75
- placeholder = options.delete(:placeholder) || "Choose file"
76
- placeholder_opts = { class: "custom-file-label" }
77
- placeholder_opts[:for] = options[:id] if options[:id].present?
78
-
79
- input = file_field_without_bootstrap(name, options)
80
- placeholder_label = label(name, placeholder, placeholder_opts)
81
- concat(input)
82
- concat(placeholder_label)
83
- end
84
- end
85
- end
86
- end
87
-
88
- bootstrap_method_alias :file_field
89
-
90
- def select_with_bootstrap(method, choices=nil, options={}, html_options={}, &block)
91
- form_group_builder(method, options, html_options) do
92
- prepend_and_append_input(method, options) do
93
- select_without_bootstrap(method, choices, options, html_options, &block)
94
- end
95
- end
96
- end
97
-
98
- bootstrap_method_alias :select
99
-
100
- def collection_select_with_bootstrap(method, collection, value_method, text_method, options={}, html_options={})
101
- form_group_builder(method, options, html_options) do
102
- input_with_error(method) do
103
- collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
104
- end
105
- end
106
- end
107
-
108
- bootstrap_method_alias :collection_select
109
-
110
- def grouped_collection_select_with_bootstrap(method, collection, group_method,
111
- group_label_method, option_key_method,
112
- option_value_method, options={}, html_options={})
113
- form_group_builder(method, options, html_options) do
114
- input_with_error(method) do
115
- grouped_collection_select_without_bootstrap(method, collection, group_method,
116
- group_label_method, option_key_method,
117
- option_value_method, options, html_options)
118
- end
119
- end
120
- end
121
-
122
- bootstrap_method_alias :grouped_collection_select
123
-
124
- def time_zone_select_with_bootstrap(method, priority_zones=nil, options={}, html_options={})
125
- form_group_builder(method, options, html_options) do
126
- input_with_error(method) do
127
- time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
128
- end
129
- end
130
- end
131
-
132
- bootstrap_method_alias :time_zone_select
133
-
134
- def check_box_with_bootstrap(name, options={}, checked_value="1", unchecked_value="0", &block)
135
- options = options.symbolize_keys!
136
- check_box_options = options.except(:label, :label_class, :error_message, :help,
137
- :inline, :custom, :hide_label, :skip_label, :wrapper_class)
138
- check_box_classes = [check_box_options[:class]]
139
- check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
140
- check_box_classes << "is-invalid" if has_error?(name)
141
-
142
- label_classes = [options[:label_class]]
143
- label_classes << hide_class if options[:hide_label]
144
-
145
- if options[:custom]
146
- check_box_options[:class] = (["custom-control-input"] + check_box_classes).compact.join(" ")
147
- wrapper_class = ["custom-control", "custom-checkbox"]
148
- wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
149
- label_class = label_classes.prepend("custom-control-label").compact.join(" ")
150
- else
151
- check_box_options[:class] = (["form-check-input"] + check_box_classes).compact.join(" ")
152
- wrapper_class = ["form-check"]
153
- wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
154
- label_class = label_classes.prepend("form-check-label").compact.join(" ")
155
- end
156
-
157
- checkbox_html = check_box_without_bootstrap(name, check_box_options, checked_value, unchecked_value)
158
- label_content = block_given? ? capture(&block) : options[:label]
159
- label_description = label_content || (object && object.class.human_attribute_name(name)) || name.to_s.humanize
160
-
161
- label_name = name
162
- # label's `for` attribute needs to match checkbox tag's id,
163
- # IE sanitized value, IE
164
- # https://github.com/rails/rails/blob/5-0-stable/actionview/lib/action_view/helpers/tags/base.rb#L123-L125
165
- if options[:multiple]
166
- label_name =
167
- "#{name}_#{checked_value.to_s.gsub(/\s/, '_').gsub(/[^-[[:word:]]]/, '').mb_chars.downcase}"
168
- end
169
-
170
- label_options = { class: label_class }
171
- label_options[:for] = options[:id] if options[:id].present?
172
-
173
- wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
174
-
175
- content_tag(:div, class: wrapper_class.compact.join(" ")) do
176
- html = if options[:skip_label]
177
- checkbox_html
178
- else
179
- checkbox_html.concat(label(label_name, label_description, label_options))
180
- end
181
- html.concat(generate_error(name)) if options[:error_message]
182
- html
183
- end
184
- end
185
-
186
- bootstrap_method_alias :check_box
187
-
188
- def radio_button_with_bootstrap(name, value, *args)
189
- options = args.extract_options!.symbolize_keys!
190
- radio_options = options.except(:label, :label_class, :error_message, :help,
191
- :inline, :custom, :hide_label, :skip_label,
192
- :wrapper_class)
193
- radio_classes = [options[:class]]
194
- radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
195
- radio_classes << "is-invalid" if has_error?(name)
196
-
197
- label_classes = [options[:label_class]]
198
- label_classes << hide_class if options[:hide_label]
199
-
200
- if options[:custom]
201
- radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(" ")
202
- wrapper_class = ["custom-control", "custom-radio"]
203
- wrapper_class.append("custom-control-inline") if layout_inline?(options[:inline])
204
- label_class = label_classes.prepend("custom-control-label").compact.join(" ")
205
- else
206
- radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(" ")
207
- wrapper_class = ["form-check"]
208
- wrapper_class.append("form-check-inline") if layout_inline?(options[:inline])
209
- wrapper_class.append("disabled") if options[:disabled]
210
- label_class = label_classes.prepend("form-check-label").compact.join(" ")
211
- end
212
- radio_html = radio_button_without_bootstrap(name, value, radio_options)
213
-
214
- label_options = { value: value, class: label_class }
215
- label_options[:for] = options[:id] if options[:id].present?
216
-
217
- wrapper_class.append(options[:wrapper_class]) if options[:wrapper_class]
218
-
219
- content_tag(:div, class: wrapper_class.compact.join(" ")) do
220
- html = if options[:skip_label]
221
- radio_html
222
- else
223
- radio_html.concat(label(name, options[:label], label_options))
224
- end
225
- html.concat(generate_error(name)) if options[:error_message]
226
- html
227
- end
228
- end
229
-
230
- bootstrap_method_alias :radio_button
231
-
232
- def collection_check_boxes_with_bootstrap(*args)
233
- html = inputs_collection(*args) do |name, value, options|
234
- options[:multiple] = true
235
- check_box(name, options, value, nil)
236
- end
237
- hidden_field(args.first, value: "", multiple: true).concat(html)
238
- end
239
-
240
- bootstrap_method_alias :collection_check_boxes
241
-
242
- def collection_radio_buttons_with_bootstrap(*args)
243
- inputs_collection(*args) do |name, value, options|
244
- radio_button(name, value, options)
245
- end
246
- end
247
-
248
- bootstrap_method_alias :collection_radio_buttons
249
-
250
- def form_group(*args, &block)
251
- options = args.extract_options!
252
- name = args.first
253
-
254
- options[:class] = ["form-group", options[:class]].compact.join(" ")
255
- options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal &&
256
- !options[:class].split.include?("form-row")
257
- options[:class] << " form-inline" if field_inline_override?(options[:layout])
258
- options[:class] << " #{feedback_class}" if options[:icon]
259
-
260
- content_tag(:div, options.except(:append, :id, :label, :help, :icon,
261
- :input_group_class, :label_col, :control_col,
262
- :add_control_col_class, :layout, :prepend)) do
263
- label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label]
264
- control = capture(&block)
265
-
266
- help = options[:help]
267
- help_text = generate_help(name, help).to_s
268
-
269
- if get_group_layout(options[:layout]) == :horizontal
270
- control_class = options[:control_col] || control_col
271
- control_class = [control_class, options[:add_control_col_class]].compact.join(" ") if options[:add_control_col_class]
272
- unless options[:label]
273
- control_offset = offset_col(options[:label_col] || @label_col)
274
- control_class = "#{control_class} #{control_offset}"
275
- end
276
- control = content_tag(:div, control + help_text, class: control_class)
277
- concat(label).concat(control)
278
- else
279
- concat(label).concat(control).concat(help_text)
280
- end
281
- end
282
- end
283
-
284
63
  def fields_for_with_bootstrap(record_name, record_object=nil, fields_options={}, &block)
285
- if record_object.is_a?(Hash) && record_object.extractable_options?
286
- fields_options = record_object
64
+ fields_options = fields_for_options(record_object, fields_options)
65
+ record_object.is_a?(Hash) && record_object.extractable_options? &&
287
66
  record_object = nil
288
- end
289
- fields_options[:layout] ||= options[:layout]
290
- fields_options[:label_col] = fields_options[:label_col].present? ? (fields_options[:label_col]).to_s : options[:label_col]
291
- fields_options[:control_col] ||= options[:control_col]
292
- fields_options[:inline_errors] ||= options[:inline_errors]
293
- fields_options[:label_errors] ||= options[:label_errors]
294
67
  fields_for_without_bootstrap(record_name, record_object, fields_options, &block)
295
68
  end
296
69
 
297
- bootstrap_method_alias :fields_for
70
+ bootstrap_alias :fields_for
298
71
 
299
72
  # the Rails `fields` method passes its options
300
73
  # to the builder, so there is no need to write a `bootstrap_form` helper
@@ -302,32 +75,15 @@ module BootstrapForm
302
75
 
303
76
  private
304
77
 
305
- def layout_default?(field_layout=nil)
306
- [:default, nil].include? layout_in_effect(field_layout)
307
- end
308
-
309
- def layout_horizontal?(field_layout=nil)
310
- layout_in_effect(field_layout) == :horizontal
311
- end
312
-
313
- def layout_inline?(field_layout=nil)
314
- layout_in_effect(field_layout) == :inline
315
- end
316
-
317
- def field_inline_override?(field_layout=nil)
318
- field_layout == :inline && layout != :inline
319
- end
320
-
321
- # true and false should only come from check_box and radio_button,
322
- # and those don't have a :horizontal layout
323
- def layout_in_effect(field_layout)
324
- field_layout = :inline if field_layout == true
325
- field_layout = :default if field_layout == false
326
- field_layout || layout
327
- end
328
-
329
- def get_group_layout(group_layout)
330
- group_layout || layout
78
+ def fields_for_options(record_object, fields_options)
79
+ field_options = fields_options
80
+ record_object.is_a?(Hash) && record_object.extractable_options? &&
81
+ field_options = record_object
82
+ %i[layout control_col inline_errors label_errors].each do |option|
83
+ field_options[option] ||= options[option]
84
+ end
85
+ field_options[:label_col] = field_options[:label_col].present? ? (field_options[:label_col]).to_s : options[:label_col]
86
+ field_options
331
87
  end
332
88
 
333
89
  def default_label_col
@@ -355,233 +111,7 @@ module BootstrapForm
355
111
  end
356
112
 
357
113
  def control_specific_class(method)
358
- "rails-bootstrap-forms-#{method.tr('_', '-')}"
359
- end
360
-
361
- def has_error?(name)
362
- object.respond_to?(:errors) && !(name.nil? || object.errors[name].empty?)
363
- end
364
-
365
- def required_attribute?(obj, attribute)
366
- return false unless obj && attribute
367
-
368
- target = obj.class == Class ? obj : obj.class
369
-
370
- target_validators = if target.respond_to? :validators_on
371
- target.validators_on(attribute).map(&:class)
372
- else
373
- []
374
- end
375
-
376
- has_presence_validator = target_validators.include?(
377
- ActiveModel::Validations::PresenceValidator
378
- )
379
-
380
- if defined? ActiveRecord::Validations::PresenceValidator
381
- has_presence_validator |= target_validators.include?(
382
- ActiveRecord::Validations::PresenceValidator
383
- )
384
- end
385
-
386
- has_presence_validator
387
- end
388
-
389
- # TODO: Refactor this method and remove the rubocop:disable
390
- def form_group_builder(method, options, html_options=nil) # rubocop:disable Metrics/MethodLength
391
- options.symbolize_keys!
392
-
393
- wrapper_class = options.delete(:wrapper_class)
394
- wrapper_options = options.delete(:wrapper)
395
-
396
- html_options.symbolize_keys! if html_options
397
-
398
- # Add control_class; allow it to be overridden by :control_class option
399
- css_options = html_options || options
400
- control_classes = css_options.delete(:control_class) { control_class }
401
- css_options[:class] = [control_classes, css_options[:class]].compact.join(" ")
402
- css_options[:class] << " is-invalid" if has_error?(method)
403
-
404
- options = convert_form_tag_options(method, options) if acts_like_form_tag
405
-
406
- help = options.delete(:help)
407
- icon = options.delete(:icon)
408
- label_col = options.delete(:label_col)
409
- control_col = options.delete(:control_col)
410
- add_control_col_class = options.delete(:add_control_col_class)
411
- layout = get_group_layout(options.delete(:layout))
412
- form_group_options = {
413
- id: options[:id],
414
- help: help,
415
- icon: icon,
416
- label_col: label_col,
417
- control_col: control_col,
418
- add_control_col_class: add_control_col_class,
419
- layout: layout,
420
- class: wrapper_class
421
- }
422
-
423
- form_group_options.merge!(wrapper_options) if wrapper_options.is_a?(Hash)
424
-
425
- unless options.delete(:skip_label)
426
- if options[:label].is_a?(Hash)
427
- label_text = options[:label].delete(:text)
428
- label_class = options[:label].delete(:class)
429
- options.delete(:label)
430
- end
431
- label_class ||= options.delete(:label_class)
432
- label_class = hide_class if options.delete(:hide_label) || options[:label_as_placeholder]
433
-
434
- label_text ||= options.delete(:label) if options[:label].is_a?(String)
435
-
436
- if options.key?(:skip_required)
437
- warn "`:skip_required` is deprecated, use `:required: false` instead"
438
- options[:required] = options.delete(:skip_required) ? false : :default
439
- end
440
-
441
- form_group_options[:label] = {
442
- text: label_text,
443
- class: label_class,
444
- required: options[:required]
445
- }.merge(css_options[:id].present? ? { for: css_options[:id] } : {})
446
-
447
- css_options[:placeholder] = label_text || object.class.human_attribute_name(method) if options.delete(:label_as_placeholder)
448
- end
449
-
450
- if wrapper_options == false
451
- yield
452
- else
453
- form_group(method, form_group_options) do
454
- yield
455
- end
456
- end
457
- end
458
-
459
- def convert_form_tag_options(method, options={})
460
- unless @options[:skip_default_ids]
461
- options[:name] ||= method
462
- options[:id] ||= method
463
- end
464
- options
465
- end
466
-
467
- def generate_label(id, name, options, custom_label_col, group_layout)
468
- # id is the caller's options[:id] at the only place this method is called.
469
- # The options argument is a small subset of the options that might have
470
- # been passed to generate_label's caller, and definitely doesn't include
471
- # :id.
472
- options[:for] = id if acts_like_form_tag
473
- classes = [options[:class]]
474
-
475
- if layout_horizontal?(group_layout)
476
- classes << "col-form-label"
477
- classes << (custom_label_col || label_col)
478
- elsif layout_inline?(group_layout)
479
- classes << "mr-sm-2"
480
- end
481
-
482
- case options.delete(:required)
483
- when true
484
- classes << "required"
485
- when nil, :default
486
- classes << "required" if required_attribute?(object, name)
487
- end
488
-
489
- options[:class] = classes.compact.join(" ").strip
490
- options.delete(:class) if options[:class].empty?
491
-
492
- if label_errors && has_error?(name)
493
- error_messages = get_error_messages(name)
494
- label_text = (options[:text] || object.class.human_attribute_name(name)).to_s.concat(" #{error_messages}")
495
- options[:class] = [options[:class], "text-danger"].compact.join(" ")
496
- label(name, label_text, options.except(:text))
497
- else
498
- label(name, options[:text], options.except(:text))
499
- end
500
- end
501
-
502
- def has_inline_error?(name)
503
- has_error?(name) && inline_errors
504
- end
505
-
506
- def generate_error(name)
507
- if has_inline_error?(name)
508
- help_text = get_error_messages(name)
509
- help_klass = "invalid-feedback"
510
- help_tag = :div
511
-
512
- content_tag(help_tag, help_text, class: help_klass)
513
- end
514
- end
515
-
516
- def generate_help(name, help_text)
517
- return if help_text == false || has_inline_error?(name)
518
-
519
- help_klass ||= "form-text text-muted"
520
- help_text ||= get_help_text_by_i18n_key(name)
521
- help_tag ||= :small
522
-
523
- content_tag(help_tag, help_text, class: help_klass) if help_text.present?
524
- end
525
-
526
- def get_error_messages(name)
527
- object.errors[name].join(", ")
528
- end
529
-
530
- def inputs_collection(name, collection, value, text, options={})
531
- options[:inline] ||= layout_inline?(options[:layout])
532
- form_group_builder(name, options) do
533
- inputs = ""
534
-
535
- collection.each_with_index do |obj, i|
536
- input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text))
537
-
538
- input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
539
- if (checked = input_options[:checked])
540
- input_options[:checked] = checked == input_value ||
541
- Array(checked).try(:include?, input_value) ||
542
- checked == obj ||
543
- Array(checked).try(:include?, obj)
544
- end
545
-
546
- input_options.delete(:class)
547
- inputs << yield(name, input_value, input_options.merge(error_message: i == collection.size - 1))
548
- end
549
-
550
- inputs.html_safe
551
- end
552
- end
553
-
554
- def get_help_text_by_i18n_key(name)
555
- if object
556
-
557
- partial_scope = if object.class.respond_to?(:model_name)
558
- object.class.model_name.name
559
- else
560
- object.class.name
561
- end
562
-
563
- underscored_scope = "activerecord.help.#{partial_scope.underscore}"
564
- downcased_scope = "activerecord.help.#{partial_scope.downcase}"
565
- # First check for a subkey :html, as it is also accepted by i18n, and the
566
- # simple check for name would return an hash instead of a string (both
567
- # with .presence returning true!)
568
- help_text = I18n.t("#{name}.html", scope: underscored_scope, default: "").html_safe.presence
569
- help_text ||= if (text = I18n.t("#{name}.html", scope: downcased_scope, default: "").html_safe.presence)
570
- warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
571
- text
572
- end
573
- help_text ||= I18n.t(name, scope: underscored_scope, default: "").presence
574
- help_text ||= if (text = I18n.t(name, scope: downcased_scope, default: "").presence)
575
- warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
576
- text
577
- end
578
- help_text ||= I18n.t("#{name}_html", scope: underscored_scope, default: "").html_safe.presence
579
- help_text ||= if (text = I18n.t("#{name}_html", scope: downcased_scope, default: "").html_safe.presence)
580
- warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
581
- text
582
- end
583
- help_text
584
- end
114
+ "rails-bootstrap-forms-#{method.to_s.tr('_', '-')}"
585
115
  end
586
116
  end
587
117
  end