russ_sarbora
ServiceNow Employee
ServiceNow Employee

I was working on a fix for a PRB the other day, and part of the solution involved adding client-side validation to a form field. The logic for validating the field value involved getting some data from the instance in order to verify the value was legal. No problem, I thought, I can use GlideAjax in a Client Script to lookup the data I need. This should be a piece of cake.

 

Things started out well enough. I created an onChange Client Script for my field, added an async GlideAjax call to pull my data, added the validation logic and used g_form.showErrorBox to display an error message on the field if the entered value was invalid. In twenty minutes I had it working. So far, so good. But then I tried saving the record with an invalid value in my field and...the form still submitted and saved. No good.

 

So next I started looking into how to stop the form submit. That's not terribly difficult to do, using an onSubmit Client Script. But, things went downhill pretty quickly at this point. First, I couldn't reuse the code I had just written for the onChange, because it was buried in the onChange handler. So I decided to refactor it into a function in an onLoad script that would be available from both the onChange and onSubmit. Next I realized I'd be repeating the Ajax call in both the onChange and the onSubmit, retrieving the same data, so I thought I'd have the onSubmit look for whether the error message was being displayed or not instead of re-running the full validation. Right about then is when it hit me that my simple, single-field validation had blown up into 3 Client Scripts involving probably 4 times as much "plumbing" code as it took for the actual validation logic. I was also up to about an hour of time so far, and I still didn't have it working to my satisfaction.

 

"This is crazy", I thought, "I must be missing something." So off to the Wiki and Community for some searching on form validation. What I wanted was a way to add validation to a field that would provide both immediate feedback whenever the user changed the field value, and also stop the form from submitting with an invalid value. It needed to be flexible enough to work with an asynchronous callback for the validation logic, and also eliminate expensive re-evaluations of that logic. I wanted it to be easy to add to the form, hopefully via a single Client Script. Most importantly, I wanted it to do all the plumbing for me, so that when adding a new validator all I have to code up is my validation logic.


If you've read this far, you can probably guess that I didn't find anything like that in the out-of-the-box product. I was a little chagrined, and definitely a lot disappointed. So I figured if I was going to have to work out how to do it for my validator, I would try to make it a generic, reusable solution for any validator. I ended up with something I'm calling the ValidatorFactory. It meets all the criteria I identified above, and I thought others might find it useful, so I thought I'd share it. The code is at the bottom of the this post.


As an example, imagine you want to add some client-side validation to the Password field on the sys_user table. You don't want to allow passwords less than 8 characters long. Note: I realize this is client-side validation, and that it would be best to also have some server-side validation to enforce this rule. Let's not get hung up on that issue, and just focus on the client-side for now.

 

To add this validator using the ValidatorFactory, you create an onLoad Client Script for the sys_user table. In the script block type:

function onLoad() {

  ValidatorFactory.newValidator({

            field: 'user_password',

            message: 'Password must be at least 8 characters',

            allowInvalidLegacy: false,

            onChange: true,

            test: function(pwd) {

                      return pwd.length>=8;

            }

  }).add();

}

 

This creates a new Validator for the user_password field, then adds it to the loaded form. Here's a quick explanation of the options:

field - name of the field to be validated

message - the message that will be displayed in the error box if the field is invalid

allowInvalidLegacy - If you have a lot of old records that are going to have invalid values in them, and don't want your users to have to update those values then you can set this option to true to have any existing value in the field be considered valid. On all new records, or if the field's value is changed in an existing record, the validator will apply and the value must be valid.

onChange - run the validator when the value in the field changes. If false, the validator is only run onSubmit

test - a function that performs the validation logic, it receives one parameter, the new value, and should return true if the value is valid, false if not.

 

That's it, we're done. Here's a screenshot of what an invalid password looks like.

invalidpassword.png

If you were to click that Submit button, the Password field's label would turn yellow and the form would not submit. If you enter a longer password, the error goes away and the submit button starts working. Not fancy, but its what I wanted, and I wrote zero lines of plumbing.

 

If you followed along and tried that on your instance, you're probably wondering why it didn't work. ValidatorFactory is not a part of the out-of-the-box product. You can either add it to the onLoad script we just created, in which case it will be available only on the sys_user table's forms. Or you can create a global UI Script, which will make it available on any form in the system. In either case, here's the code:

 

var ValidatorFactory = (function() {

      var pub = {};


      pub.newValidator = function(options) {

              var validator = {

                      isValid: true,

                      field: options.field,

                      message: options.message,

                      allowInvalidLegacy: options.allowInvalidLegacy,

                      onChange: options.onChange,

                      testFn: options.test

              };

         

              //implement the GlideValidator interface

              validator.validate = function(value) {

                      //value is really irrelevant, we've already checked it during the onChange

                      //instead, let's just see if our field has an attribute valid=false

                      if (!validator.isValid) {

                              return validator.message;

                      } else {

                              return true;

                      }

              };      

         

              validator.check = function(oldValue, newValue) {

                      if (typeof oldValue == 'undefined' && typeof newValue == 'undefined') {

                              oldValue=null;

                              newValue=g_form.getControl(validator.field).value;

                      }

                      if (validator.allowInvalidLegacy && newValue==oldValue) {

                              validator.updateField(true);

                      } else {

                              validator.updateField(validator.testFn.call(validator,newValue));

                      }

              };

         

              validator.updateField = function(isValid) {

                      validator.isValid=isValid;

                      if (isValid) {

                              g_form.hideErrorBox(validator.field);

                      } else {

                              g_form.showErrorBox(validator.field, validator.message);

                      }

              };

         

              validator.add = function() {

                      var fld = g_form.getControl(validator.field);

                      fld.setAttribute('specialtype', fld.id);

                      fld.validator = validator;

                      g_form.addValidator(fld.id, validator.validate);

                      if (validator.onChange) {

                              addOnChangeEvent([validator.field], g_form.getTableName(), validatorOnChange);

                      }

                      return validator;

              };

         

              function validatorOnChange(evt) {

                      var newValue = g_form.getValue(validator.field);

                      var oldValue = g_form.getValue('sys_original.'+validator.field);

                      g_form.getControl(validator.field).validator.check(oldValue,newValue);

              }

         

              return validator;

      };

 

      return pub;

})();

 

This has already gotten long enough, so I'm not going to explain how it works. If anyone is interested, let me know and I'll do a followup.

26 Comments