diff --git a/README.md b/README.md
index ccb568402..560127438 100644
--- a/README.md
+++ b/README.md
@@ -1134,7 +1134,7 @@ This generates:
-
+
@@ -1561,6 +1561,53 @@ This generates:
```
+### Required belongs_to associations
+
+Adding a form control for a `belongs_to` field will automatically pick up the associated presence validator.
+
+
+```erb
+<%= bootstrap_form_for(@address, url: '/address') do |f| %>
+ <%= f.collection_select :user_id, @users, :id, :email, include_blank: "Select a value" %>
+ <%= f.text_field :street %>
+ <%= f.text_field :city %>
+ <%= f.text_field :state %>
+ <%= f.text_field :zip_code %>
+ <%= f.submit "Save" %>
+<% end %>
+```
+
+Generated HTML:
+
+```html
+
+```
+
## Internationalization
bootstrap_form follows standard rails conventions so it's i18n-ready. See more
diff --git a/demo/Gemfile b/demo/Gemfile
index 25aa6a345..018a5b3a5 100644
--- a/demo/Gemfile
+++ b/demo/Gemfile
@@ -58,7 +58,7 @@ gem "sassc-rails"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
- gem "debug", platforms: %i[mri mingw x64_mingw]
+ # gem "debug", platforms: %i[mri mingw x64_mingw]
end
group :development do
diff --git a/demo/Gemfile.lock b/demo/Gemfile.lock
index aa42f32f2..78b082515 100644
--- a/demo/Gemfile.lock
+++ b/demo/Gemfile.lock
@@ -1,67 +1,67 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.0.2.4)
- actionpack (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ actioncable (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.2.4)
- actionpack (= 7.0.2.4)
- activejob (= 7.0.2.4)
- activerecord (= 7.0.2.4)
- activestorage (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ actionmailbox (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.2.4)
- actionpack (= 7.0.2.4)
- actionview (= 7.0.2.4)
- activejob (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ actionmailer (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ actionview (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
- actionpack (7.0.2.4)
- actionview (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ actionpack (7.0.3.1)
+ actionview (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.2.4)
- actionpack (= 7.0.2.4)
- activerecord (= 7.0.2.4)
- activestorage (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ actiontext (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.2.4)
- activesupport (= 7.0.2.4)
+ actionview (7.0.3.1)
+ activesupport (= 7.0.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (7.0.2.4)
- activesupport (= 7.0.2.4)
+ activejob (7.0.3.1)
+ activesupport (= 7.0.3.1)
globalid (>= 0.3.6)
- activemodel (7.0.2.4)
- activesupport (= 7.0.2.4)
- activerecord (7.0.2.4)
- activemodel (= 7.0.2.4)
- activesupport (= 7.0.2.4)
- activestorage (7.0.2.4)
- actionpack (= 7.0.2.4)
- activejob (= 7.0.2.4)
- activerecord (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ activemodel (7.0.3.1)
+ activesupport (= 7.0.3.1)
+ activerecord (7.0.3.1)
+ activemodel (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
+ activestorage (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (7.0.2.4)
+ activesupport (7.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -69,13 +69,13 @@ GEM
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
bindex (0.8.1)
- bootsnap (1.11.1)
+ bootsnap (1.13.0)
msgpack (~> 1.2)
- bootstrap_form (5.0.0)
+ bootstrap_form (5.1.0)
actionpack (>= 5.2)
activemodel (>= 5.2)
builder (3.2.4)
- capybara (3.36.0)
+ capybara (3.37.1)
addressable
matrix
mini_mime (>= 0.1.3)
@@ -84,7 +84,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
- capybara-screenshot-diff (1.6.1)
+ capybara-screenshot-diff (1.6.3)
actionpack (>= 4.2, < 8)
capybara (>= 2, < 4)
chunky_png (~> 1.3)
@@ -92,28 +92,22 @@ GEM
chunky_png (1.4.0)
concurrent-ruby (1.1.10)
crass (1.0.6)
- cssbundling-rails (1.1.0)
+ cssbundling-rails (1.1.1)
railties (>= 6.0.0)
- debug (1.5.0)
- irb (>= 1.3.6)
- reline (>= 0.2.7)
digest (3.1.0)
- erubi (1.10.0)
+ erubi (1.11.0)
ffi (1.15.5)
globalid (1.0.0)
activesupport (>= 5.0)
htmlbeautifier (1.4.2)
- i18n (1.10.0)
+ i18n (1.12.0)
concurrent-ruby (~> 1.0)
- io-console (0.5.11)
- irb (1.4.1)
- reline (>= 0.3.0)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
- jsbundling-rails (1.0.2)
+ jsbundling-rails (1.0.3)
railties (>= 6.0.0)
- loofah (2.16.0)
+ loofah (2.18.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -122,8 +116,8 @@ GEM
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
- minitest (5.15.0)
- msgpack (1.5.1)
+ minitest (5.16.2)
+ msgpack (1.5.4)
net-imap (0.2.3)
digest
net-protocol
@@ -139,47 +133,45 @@ GEM
net-protocol
timeout
nio4r (2.5.8)
- nokogiri (1.13.4-x86_64-darwin)
+ nokogiri (1.13.8-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.13.4-x86_64-linux)
+ nokogiri (1.13.8-x86_64-linux)
racc (~> 1.4)
public_suffix (4.0.7)
puma (5.6.4)
nio4r (~> 2.0)
racc (1.6.0)
- rack (2.2.3)
- rack-test (1.1.0)
- rack (>= 1.0, < 3)
- rails (7.0.2.4)
- actioncable (= 7.0.2.4)
- actionmailbox (= 7.0.2.4)
- actionmailer (= 7.0.2.4)
- actionpack (= 7.0.2.4)
- actiontext (= 7.0.2.4)
- actionview (= 7.0.2.4)
- activejob (= 7.0.2.4)
- activemodel (= 7.0.2.4)
- activerecord (= 7.0.2.4)
- activestorage (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ rack (2.2.4)
+ rack-test (2.0.2)
+ rack (>= 1.3)
+ rails (7.0.3.1)
+ actioncable (= 7.0.3.1)
+ actionmailbox (= 7.0.3.1)
+ actionmailer (= 7.0.3.1)
+ actionpack (= 7.0.3.1)
+ actiontext (= 7.0.3.1)
+ actionview (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activemodel (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
bundler (>= 1.15.0)
- railties (= 7.0.2.4)
+ railties (= 7.0.3.1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.4.2)
+ rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
- railties (7.0.2.4)
- actionpack (= 7.0.2.4)
- activesupport (= 7.0.2.4)
+ railties (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.0.6)
- regexp_parser (2.3.1)
- reline (0.3.1)
- io-console (~> 0.5)
+ regexp_parser (2.5.0)
rexml (3.2.5)
rubyzip (2.3.2)
sassc (2.4.0)
@@ -190,28 +182,30 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
- selenium-webdriver (4.1.0)
+ selenium-webdriver (4.4.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
- rubyzip (>= 1.2.2)
- sprockets (4.0.3)
+ rubyzip (>= 1.2.2, < 3.0)
+ websocket (~> 1.0)
+ sprockets (4.1.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
- sqlite3 (1.4.2)
- stimulus-rails (1.0.4)
+ sqlite3 (1.4.4)
+ stimulus-rails (1.1.0)
railties (>= 6.0.0)
- strscan (3.0.1)
+ strscan (3.0.4)
thor (1.2.1)
- tilt (2.0.10)
- timeout (0.2.0)
- turbo-rails (1.0.1)
+ tilt (2.0.11)
+ timeout (0.3.0)
+ turbo-rails (1.1.1)
actionpack (>= 6.0.0)
+ activejob (>= 6.0.0)
railties (>= 6.0.0)
- tzinfo (2.0.4)
+ tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
web-console (4.2.0)
actionview (>= 6.0.0)
@@ -222,12 +216,13 @@ GEM
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0)
+ websocket (1.2.9)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.5.4)
+ zeitwerk (2.6.0)
PLATFORMS
x86_64-darwin-21
@@ -238,7 +233,6 @@ DEPENDENCIES
bootstrap_form (~> 5.0)
capybara-screenshot-diff
cssbundling-rails
- debug
htmlbeautifier
jbuilder
jsbundling-rails
diff --git a/demo/app/assets/stylesheets/application.scss b/demo/app/assets/stylesheets/application.scss
index 6bcd0b60c..1a39738e5 100644
--- a/demo/app/assets/stylesheets/application.scss
+++ b/demo/app/assets/stylesheets/application.scss
@@ -1 +1,5 @@
// @import "actiontext";
+
+label.required:after {
+ content:" *";
+}
diff --git a/demo/app/controllers/bootstrap_controller.rb b/demo/app/controllers/bootstrap_controller.rb
index 40027dd5d..a4b3363f1 100644
--- a/demo/app/controllers/bootstrap_controller.rb
+++ b/demo/app/controllers/bootstrap_controller.rb
@@ -15,8 +15,9 @@ def fragment
private
def load_models
+ @address = Address.new(id: 1, street: "Foo")
@collection = [
- Address.new(id: 1, street: "Foo"),
+ @address,
Address.new(id: 2, street: "Bar")
]
@@ -25,5 +26,7 @@ def load_models
@user_with_error = User.new email: "steve.example.com"
@user_with_error.errors.add(:email)
@user_with_error.errors.add(:misc)
+
+ @users = [@user]
end
end
diff --git a/demo/doc/screenshots/bootstrap/index/00_horizontal_form.png b/demo/doc/screenshots/bootstrap/index/00_horizontal_form.png
index 5eb876a66..7c0c2eaa2 100644
Binary files a/demo/doc/screenshots/bootstrap/index/00_horizontal_form.png and b/demo/doc/screenshots/bootstrap/index/00_horizontal_form.png differ
diff --git a/demo/doc/screenshots/bootstrap/index/01_with_validation_error.png b/demo/doc/screenshots/bootstrap/index/01_with_validation_error.png
index 23d77366b..423ab9ccb 100644
Binary files a/demo/doc/screenshots/bootstrap/index/01_with_validation_error.png and b/demo/doc/screenshots/bootstrap/index/01_with_validation_error.png differ
diff --git a/demo/doc/screenshots/bootstrap/index/02_inline_form.png b/demo/doc/screenshots/bootstrap/index/02_inline_form.png
index 12627a482..ed249c51e 100644
Binary files a/demo/doc/screenshots/bootstrap/index/02_inline_form.png and b/demo/doc/screenshots/bootstrap/index/02_inline_form.png differ
diff --git a/demo/doc/screenshots/bootstrap/index/03_simple_action_text_example.png b/demo/doc/screenshots/bootstrap/index/03_simple_action_text_example.png
index 4e6c83bbb..d5c33087d 100644
Binary files a/demo/doc/screenshots/bootstrap/index/03_simple_action_text_example.png and b/demo/doc/screenshots/bootstrap/index/03_simple_action_text_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/index/04_floating_labels.png b/demo/doc/screenshots/bootstrap/index/04_floating_labels.png
index 01393fa5f..ae4b1a80a 100644
Binary files a/demo/doc/screenshots/bootstrap/index/04_floating_labels.png and b/demo/doc/screenshots/bootstrap/index/04_floating_labels.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/00_example.png b/demo/doc/screenshots/bootstrap/readme/00_example.png
index 2ca8207a9..f3e95c9d8 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/00_example.png and b/demo/doc/screenshots/bootstrap/readme/00_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/02_example.png b/demo/doc/screenshots/bootstrap/readme/02_example.png
index ad5104512..aa6c6cd33 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/02_example.png and b/demo/doc/screenshots/bootstrap/readme/02_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/05_example.png b/demo/doc/screenshots/bootstrap/readme/05_example.png
index 49fa4d5aa..d064e11a3 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/05_example.png and b/demo/doc/screenshots/bootstrap/readme/05_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/07_example.png b/demo/doc/screenshots/bootstrap/readme/07_example.png
index 5df6d7a85..d19de6887 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/07_example.png and b/demo/doc/screenshots/bootstrap/readme/07_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/12_example.png b/demo/doc/screenshots/bootstrap/readme/12_example.png
index d5e5f72fb..9dce52121 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/12_example.png and b/demo/doc/screenshots/bootstrap/readme/12_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/25_example.png b/demo/doc/screenshots/bootstrap/readme/25_example.png
index 5a635ea9a..4fe5a15b3 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/25_example.png and b/demo/doc/screenshots/bootstrap/readme/25_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/26_example.png b/demo/doc/screenshots/bootstrap/readme/26_example.png
index 6b7c0efa6..8ed59686a 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/26_example.png and b/demo/doc/screenshots/bootstrap/readme/26_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/38_example.png b/demo/doc/screenshots/bootstrap/readme/38_example.png
index 95731ce9c..330a281a4 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/38_example.png and b/demo/doc/screenshots/bootstrap/readme/38_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/39_example.png b/demo/doc/screenshots/bootstrap/readme/39_example.png
index 16d7dc527..67208b1d0 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/39_example.png and b/demo/doc/screenshots/bootstrap/readme/39_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/40_example.png b/demo/doc/screenshots/bootstrap/readme/40_example.png
index 11b2e1ae4..f3cc80e51 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/40_example.png and b/demo/doc/screenshots/bootstrap/readme/40_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/41_example.png b/demo/doc/screenshots/bootstrap/readme/41_example.png
index acaaba990..d3a81ace1 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/41_example.png and b/demo/doc/screenshots/bootstrap/readme/41_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/42_example.png b/demo/doc/screenshots/bootstrap/readme/42_example.png
index 2ca8207a9..f3e95c9d8 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/42_example.png and b/demo/doc/screenshots/bootstrap/readme/42_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/43_example.png b/demo/doc/screenshots/bootstrap/readme/43_example.png
index 3ab9473d7..d20c9d12e 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/43_example.png and b/demo/doc/screenshots/bootstrap/readme/43_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/50_example.png b/demo/doc/screenshots/bootstrap/readme/50_example.png
index 8ed1c6984..fb6d9ae26 100644
Binary files a/demo/doc/screenshots/bootstrap/readme/50_example.png and b/demo/doc/screenshots/bootstrap/readme/50_example.png differ
diff --git a/demo/doc/screenshots/bootstrap/readme/51_example.png b/demo/doc/screenshots/bootstrap/readme/51_example.png
new file mode 100644
index 000000000..798ac0331
Binary files /dev/null and b/demo/doc/screenshots/bootstrap/readme/51_example.png differ
diff --git a/lib/bootstrap_form/components/validation.rb b/lib/bootstrap_form/components/validation.rb
index 215d8b76f..221149b22 100644
--- a/lib/bootstrap_form/components/validation.rb
+++ b/lib/bootstrap_form/components/validation.rb
@@ -8,35 +8,45 @@ module Validation
private
def error?(name)
- object.respond_to?(:errors) && !(name.nil? || object.errors[name].empty?)
+ name && object.respond_to?(:errors) && (object.errors[name].any? || association_error?(name))
+ end
+
+ def association_error?(name)
+ object.class.reflections.any? do |association_name, a|
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
+ next unless a.foreign_key == name.to_s
+
+ object.errors[association_name].any?
+ end
end
def required_attribute?(obj, attribute)
return false unless obj && attribute
target = obj.instance_of?(Class) ? obj : obj.class
+ return false unless target.respond_to? :validators_on
- target_validators = if target.respond_to? :validators_on
- target.validators_on(attribute).map(&:class)
- else
- []
- end
-
- presence_validator?(target_validators)
+ presence_validator?(target_validators(target, attribute)) ||
+ required_association?(target, attribute)
end
- def presence_validator?(target_validators)
- has_presence_validator = target_validators.include?(
- ActiveModel::Validations::PresenceValidator
- )
-
- if defined? ActiveRecord::Validations::PresenceValidator
- has_presence_validator |= target_validators.include?(
- ActiveRecord::Validations::PresenceValidator
- )
+ def required_association?(target, attribute)
+ target.reflections.find do |name, a|
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
+ next unless a.foreign_key == attribute.to_s
+
+ presence_validator?(target_validators(target, name))
end
+ end
+
+ def target_validators(target, attribute)
+ target.validators_on(attribute).map(&:class)
+ end
- has_presence_validator
+ def presence_validator?(target_validators)
+ target_validators.include?(ActiveModel::Validations::PresenceValidator) ||
+ (defined?(ActiveRecord::Validations::PresenceValidator) &&
+ target_validators.include?(ActiveRecord::Validations::PresenceValidator))
end
def inline_error?(name)
@@ -54,7 +64,14 @@ def generate_error(name)
end
def get_error_messages(name)
- object.errors[name].join(", ")
+ messages = object.errors[name]
+ object.class.reflections.each do |association_name, a|
+ next unless a.is_a?(ActiveRecord::Reflection::BelongsToReflection)
+ next unless a.foreign_key == name.to_s
+
+ messages.concat object.errors[association_name]
+ end
+ messages.join(", ")
end
end
end