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. + +![Example 51](demo/doc/screenshots/bootstrap/readme/51_example.png "Example 51") +```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