Validation message from server
For each field, encode an ActiveModel::Error
-generated validation
message that corresponds to a ValidityState key into the field’s
[data-validation-messages]
attribute. When handling invalid
events,
retrieve the corresponding error message from that Object and render it
when available.
When a key in a field’s ValidityState is absent from the
data-validation-messages
, fallback to the Browser-provided default.
To force this situation, declare an arbitrary length: { minimum: 7 }
validation on the Authentication#password
attribute.
Mapping ActiveModel::Errors to Validation messages
The application/validation_messages
JSON partial is an incomplete
mapping. There are some attribute and validation-specific subtleties
whose solutions are not yet aren’t immediately obvious. For example,
length
validations encode a %{count}
interpolation
placeholder . Some validation concepts are flat-out
missing counterparts.
There is a possibility to implement a lightweight I18n
gem counterpart
on the client side to replace interpolations with the correct values at
validation-time, but at this point in time, that is an exercise best
left up to the reader.
Collapse app/javascript/initializers/validations.ts
Expand app/javascript/initializers/validations.ts
app/javascript/initializers/validations.ts
diff --git a/app/javascript/initializers/validations.ts b/app/javascript/initializers/validations.ts
index 0008f1f..48ff64e 100644
--- a/app/javascript/initializers/validations.ts
+++ b/app/javascript/initializers/validations.ts
@@ -33,7 +33,7 @@ function reportValidity(input: FieldElement): void {
if (input.form?.hasAttribute("novalidate")) return
const id = input.getAttribute("data-validation-message")
- const validationMessage = input.validationMessage
+ const validationMessage = getValidationMessage(input)
const element = document.getElementById(id) || createValidationMessageFragment(input.form)
if (element) {
@@ -62,6 +62,22 @@ function createValidationMessageFragment(form) {
}
}
+function getValidationMessage(input) {
+ const validationMessages = Object.entries(readValidationMessages(input))
+
+ const [ _, validationMessage ] = validationMessages.find(([ key ]) => input.validity[key]) || [ null, null ]
+
+ return validationMessage || input.validationMessage
+}
+
+function readValidationMessages(input) {
+ try {
+ return JSON.parse(input.getAttribute("data-validation-messages"))
+ } catch(_) {
+ return {}
+ }
+}
+
function isFieldElement(element: any): element is FieldElement {
return [
HTMLButtonElement,
Collapse app/models/authentication.rb
Expand app/models/authentication.rb
app/models/authentication.rb
diff --git a/app/models/authentication.rb b/app/models/authentication.rb
index 6fb6e80..a876249 100644
--- a/app/models/authentication.rb
+++ b/app/models/authentication.rb
@@ -8,7 +8,7 @@ class Authentication
attribute :session
validates :username, presence: true
- validates :password, presence: true
+ validates :password, presence: true, length: { minimum: 7 }
validate { errors.add(:base, :invalid) unless user.present? }
def self.find(session)
Collapse app/views/application/_validation_messages.json.jbuilder
Expand app/views/application/_validation_messages.json.jbuilder
app/views/application/_validation_messages.json.jbuilder
diff --git a/app/views/application/_validation_messages.json.jbuilder b/app/views/application/_validation_messages.json.jbuilder
new file mode 100644
index 0000000..c605573
--- /dev/null
+++ b/app/views/application/_validation_messages.json.jbuilder
@@ -0,0 +1,2 @@
+json.badInput object.errors.generate_message(method_name, :invalid)
+json.valueMissing object.errors.generate_message(method_name, :blank)
Collapse lib/rails_ext/action_view.rb
Expand lib/rails_ext/action_view.rb
lib/rails_ext/action_view.rb
diff --git a/lib/rails_ext/action_view.rb b/lib/rails_ext/action_view.rb
index aa3a192..6bd0227 100644
--- a/lib/rails_ext/action_view.rb
+++ b/lib/rails_ext/action_view.rb
@@ -23,6 +23,7 @@ module ValidationMessageExtension
index = @options.fetch("index", @auto_index)
@options["data-validation-message"] ||= tag_id(index) + "_validation_message"
+ @options["data-validation-messages"] ||= @template_object.render(formats: :json, partial: "validation_messages", locals: instance_values.symbolize_keys)
super
end
@@ -53,8 +54,8 @@ ActiveSupport.on_load :action_view do
end
[RadioButton, CheckBox].each do |kls|
- prepend CheckableAriaTagsExtension
- prepend ValidationMessageExtension
+ kls.prepend CheckableAriaTagsExtension
+ kls.prepend ValidationMessageExtension
end
end
end
Collapse test/controllers/authentications_controller_test.rb
Expand test/controllers/authentications_controller_test.rb
test/controllers/authentications_controller_test.rb
diff --git a/test/controllers/authentications_controller_test.rb b/test/controllers/authentications_controller_test.rb
index 04e9c2d..773c833 100644
--- a/test/controllers/authentications_controller_test.rb
+++ b/test/controllers/authentications_controller_test.rb
@@ -40,7 +40,7 @@ class AuthenticationsControllerTest < ActionDispatch::IntegrationTest
assert_nil session[:user_id]
assert_select "input#authentication_username ~ #authentication_username_validation_message", text: "can't be blank"
assert_select %(input[type="text"][aria-invalid="true"][aria-describedby~="authentication_username_validation_message"])
- assert_select "input#authentication_password ~ #authentication_password_validation_message", text: "can't be blank"
+ assert_select "input#authentication_password ~ #authentication_password_validation_message", text: "can't be blank and is too short (minimum is 7 characters)"
assert_select %(input[type="password"][aria-invalid="true"][aria-describedby~="authentication_password_validation_message"])
end
Collapse test/system/authentications_test.rb
Expand test/system/authentications_test.rb
test/system/authentications_test.rb
diff --git a/test/system/authentications_test.rb b/test/system/authentications_test.rb
index 7cd5d98..a6ff627 100644
--- a/test/system/authentications_test.rb
+++ b/test/system/authentications_test.rb
@@ -5,8 +5,8 @@ class AuthenticationsTest < ApplicationSystemTestCase
visit new_authentication_path
click_on submit(:authentication)
- assert_field label(:authentication, :username), validation_error: "Please fill out this field."
- assert_field label(:authentication, :password), validation_error: "Please fill out this field."
+ assert_field label(:authentication, :username), validation_error: "can't be blank"
+ assert_field label(:authentication, :password), validation_error: "can't be blank"
end
test "invalid inputs re-validate on blur renders errors" do
@@ -16,7 +16,15 @@ class AuthenticationsTest < ApplicationSystemTestCase
fill_in label(:authentication, :password), with: ""
assert_field label(:authentication, :username), valid: true
- assert_field label(:authentication, :password), validation_error: "Please fill out this field."
- assert_no_field label(:authentication, :username), validation_error: "Please fill out this field."
+ assert_field label(:authentication, :password), validation_error: "can't be blank"
+ assert_no_field label(:authentication, :username), validation_error: "can't be blank"
+ end
+
+ test "invalid authentication falls back to in-Browser validation messages" do
+ visit new_authentication_path
+ fill_in label(:authentication, :password), with: "junk"
+ click_on submit(:authentication)
+
+ assert_field label(:authentication, :password), validation_error: "Please lengthen this text to 7 characters or more (you are currently using 4 characters)."
end
end