bootstrap_form 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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