Take control over the wording of the validation message for category
authorCarl Worth <cworth@cworth.org>
Sun, 7 Jun 2020 22:47:36 +0000 (15:47 -0700)
committerCarl Worth <cworth@cworth.org>
Sun, 7 Jun 2020 23:25:58 +0000 (16:25 -0700)
The "pattern" attribute on the text field was really handy for
triggering HTML5 validation that the input matched a regular
expression. The only real downside is that it gives a truly generic
error message:

Please match the requested format.

That doesn't actually tell the user waht to fix, (in this case, to add
a number). I really wish there existed another attribute to simply
provide the error message that should be provided. If there is, I
couldn't find one.

I did have the title set here, which is close, but with two problems:

  1. The resulting error message isn't _just_ my text, it is:

Please match the requested format:
<My title text here>

     so I don't quite get as much control as I want.

  2. The title text shows up immediately when hovering over the input
     field, (even before the user has ever typed anything). But this
     is an error message that I only want the user to see if they've
     committed an error.

It took me quite a lot of struggling to come up with a solution for
this minor issue that works the way I want. Here it is:

  1. I remove the pattern attribute, (so I don't get any default
     validation of the input format). This is essential or else the
     "Please match the requested format" text will appear at least
     somewhere.

  2. In the subit handler, I validate the input against my regular
     expression and call setCustomValidity on the input field to set
     the text I want to appear.

  3. But _also_ immediately after doing this I _also_ call
     reportVisibility on the form element. Otherwise, the input field
     will still get styled as invalid as I want, but the error message
     won't actually get reported, (unless the user tries submitting
     _again_ while the input is still invalid).

  4. Next I also have to arrange to _clear_ this invalid state. So for
     this I add a new onChange handler (so it will be called for every
     keystroke). For this, a lot of tutorials just call
     setCustomValidity with an empty string here unconditionally. The
     downside of that is that the field will get styled as valid as
     soon as the user makes any change. Instead, I check the regular
     expression here so that the field is only styled as valid once
     the mistake is corrected. (This is consistent with the behavior
     of the default HTML5 validation with the "pattern" attribute.)

So in the end, this gives the behavior that I want. It's a little
wordy, especially here in my explanation!, but also in the code:
Particularly that two different handlers are required: One to set the
error state and one to clear it. As I implemented things here the
regular expression is even duplicated in those two cases, (but that's
a defect that could be addressed---the pattern could be stashed in a
common place if I cared to do it).

Note that it would be possible to set the error state in an 'else'
clause within my onChange handler, (and that would eliminate the
duplication of the regular expression pattern). The reason I don't do
that here is that it would cause the field to be styled with the
angry-red "invalid" styling as soon as the user started typing, rather
than waiting for form submission before validation happens. So that
could be annoying to users (it would drive me crazy) and it would also
be inconsistent with how HTML5 validation happens with the "pattern"
attribute.

empathy/empathy.jsx

index 84a35a5eab96f71fd37336ce6f4bcef37312a85b..1ee8000ab09376e7557b2dcdcdee3d844bde1963 100644 (file)
@@ -127,9 +127,18 @@ class CategoryRequest extends React.Component {
     super(props);
     this.category = React.createRef();
 
+    this.handle_change = this.handle_change.bind(this);
     this.handle_submit = this.handle_submit.bind(this);
   }
 
+  handle_change(event) {
+    const category_input = this.category.current;
+    const category = category_input.value;
+
+    if (/[0-9]/.test(category))
+      category_input.setCustomValidity("");
+  }
+
   handle_submit(event) {
     const category_input = this.category.current;
     const category = category_input.value;
@@ -137,6 +146,12 @@ class CategoryRequest extends React.Component {
     /* Prevent the default page-changing form-submission behavior. */
     event.preventDefault();
 
+    if (! /[0-9]/.test(category)) {
+      category_input.setCustomValidity("Category must include a number");
+      event.currentTarget.reportValidity();
+      return;
+    }
+
     console.log("Do something here with category: " + category);
   }
 
@@ -155,8 +170,8 @@ class CategoryRequest extends React.Component {
               type="text"
               id="category"
               placeholder="6 things at the beach"
-              required pattern=".*[0-9]+.*"
-              title="Category must contain a number"
+              required
+              onChange={this.handle_change}
               ref={this.category}
             />
           </div>