Service Portal Form Fields, Corrected

“If you take care of the small things, the big things take care of themselves. You can gain more control over your life by paying closer attention to the little things.”
Emily Dickinson

So, I have had this annoying little bug in my Service Portal Form Fields feature that just wasn’t quite annoying enough to compel me to fix it. Lately, however, I have been doing some testing with a form that included an email field, and I finally got irritated enough to hunt down the problem and put an end to it.

The issue came about when I added the reference type fields. Under the hood, a reference type field is just a form field wrapper around an snRecordPicker. Prior to adding the reference type to the list of supported types, I displayed validation messages only if the field had been $touched or the form had been $submitted. For a reference field, though, the field is never $touched (it’s not even visible on the screen), so I added $dirty as well. That solved my problem for reference fields, but it had the unfortunate side effect of displaying the validation messages while you were still filling out the field on all other types. For fields that are simply required, that’s not a problem (as soon as you start typing, you satisfy the criteria, so there is no validation error). On fields such as email addresses, though, the first character that you type is not a valid email address, so up pops the error message before you even finish entering the data. That’s just annoying!

Anyway, the solution is obviously to only include $dirty on reference fields and leave the others as they were. Unfortunately, that is a generic line that applies all form fields of any type, so I had to includes some conditional logic in there. Here is the offending line of code:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".snhTouched || " + fullName + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

Now, that is a huge, long line of code, so I really did not want to make it any longer by throwing some conditional logic in for the $dirty attribute. I ended up shortening it to this:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"" + errorShow + "\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

… and then building the value of the new errorShow variable ahead of that with this code:

var errorShow = "(" + fullName + ".$touched || " + fullName + ".snhTouched || ";
if (type == 'reference') {
	errorShow += fullName + ".$dirty || ";
}
errorShow += form + ".$submitted) && " + fullName + ".$invalid";

That was it. Problem solved. It turns out that it was a pretty simple fix, and something that should have been done a long, long time ago. Well, at least it’s done now. Here’s an Update Set for those of you who are into that kind of thing.

Fun with Webhooks, Part IX

“You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose.”
Theodor Geisel

We still need to test the My Webhooks portal page that we built last time, but before we do that, I wanted to first build out the page referenced in a couple of links on that page so that we could test everything together. My initial thought for that page was to build out a brand new portal widget for that purpose using our old friends SNH Panel and SNH Form Fields. Before I did that, though, it occurred to me that it might be faster to just use the stock form portal page, passing in the name of the table, and potentially a sys_id for existing records. There were a number of things that I did not like about that idea, but I thought that I could overcome those with some UI Policies and some default values for a couple of table fields. I played around with that a bit and found another thing that I didn’t really like, which was that saving a record left you still on the form page and did not bring you back to the My Webhooks page, which I thought was rather annoying. It seemed as though I might be able to mitigate that by adding my Dynamic Service Portal Breadcrumbs to the top of each page, but then I ran into another problem that I could not work around related to the Document ID field. At that point, I gave up and went back to my original plan, which is where I should have started in the first place.

So, here is the HTML for my new Webhook Registry portal widget:

<snh-panel class="panel panel-primary" title="'${Webhook Registry}'">
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.number"
          snh-name="number"
          snh-label="Number"
          snh-type="text"
          readonly="readonly"/>
        <snh-form-field
          snh-model="c.data.type"
          snh-name="type"
          snh-label="Type"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"label":"Single Item","value":"single"},{"label":"Caller / Requester","value":"requester"},{"label":"Assignment Group","value":"group"},{"label":"Assignee","value":"assignee"}]'/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.owner"
          snh-name="owner"
          snh-label="Owner"
          snh-type="text"
          readonly="readonly"/>
        <snh-form-field
          snh-model="c.data.active"
          snh-name="active"
          snh-label="Active"
          snh-type="checkbox"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.url"
          snh-name="url"
          snh-label="URL"
          snh-type="url"
          snh-required="true"/>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.document_id"
          snh-name="document_id"
          snh-label="Item"
          snh-type="reference"
          snh-required="c.data.type=='single'"
          table="'incident'"
          display-field="'number'"
          search-fields="'number'"
          value-field="'sys_id'"
          ng-show="c.data.type=='single'"/>
        <snh-form-field
          snh-model="c.data.person"
          snh-name="person"
          snh-label="Person"
          snh-type="reference"
          snh-required="c.data.type=='assignee' || c.data.type=='requester'"
          table="'sys_user'"
          default-query="'active=true'"
          display-field="'name'"
          search-fields="'name'"
          value-field="'sys_id'"
          ng-show="c.data.type=='assignee' || c.data.type=='requester'"/>
        <snh-form-field
          snh-model="c.data.group"
          snh-name="group"
          snh-label="Group"
          snh-type="reference"
          snh-required="c.data.type=='group'"
          table="'sys_user_group'"
          default-query="'active=true'"
          display-field="'name'"
          search-fields="'name'"
          value-field="'sys_id'"
          ng-show="c.data.type=='group'"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.authentication"
          snh-name="authentication"
          snh-label="Authentication"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"label":"None","value":"none"},{"label":"Basic","value":"basic"}]'/>
        <snh-form-field
          snh-model="c.data.username"
          snh-name="username"
          snh-label="Username"
          snh-type="text"
          snh-required="c.data.authentication=='basic'"
          ng-show="c.data.authentication=='basic'"/>
        <snh-form-field
          snh-model="c.data.password"
          snh-name="password"
          snh-label="Password"
          snh-type="password"
          snh-required="c.data.authentication=='basic'"
          ng-show="c.data.authentication=='basic'"/>
      </div>
    </div>
  </form>

  <div style="width: 100%; padding: 5px 50px; text-align: center;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to abandon this update and return to your webhooks">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your input">Save</button>
  </div>
</snh-panel>

There’s nothing too magical there; just a bunch of SNH Form Fields wrapped inside of an SNH Panel. To mirror the UI Policies on the ServiceNow side of things, I used ng-show attributes to hide unneeded fields, and when those fields where required, I used the exact same criteria for the snh-required attribute, which kept it from telling me to complete fields that I couldn’t even see. With just that alone, I could throw the widget onto a page and bring it up, just to see what it looked like.

The new Webhook Registry widget layout

Not too bad, all things considered. Of course, this is just the layout. We still have to put the code underneath this presentation layer. We will definitely need some server side code to read and update the database, but I like to do the easy things first, so let’s start on the client side and throw in the code for the Cancel button. That just takes you right back the My Webhooks page, so that should be pretty simple, although we should bake in a little confirmation pop-up, just to be sure that the operator really does want to abandon their work. We can do that with a plain Javascript confirm, but I like the spModal version much better.

$scope.cancel = function() {
	spModal.confirm('Abandond your changes and return to your Webhooks?').then(function(confirmed) {
		if (confirmed) {
			$location.search('id=my_webhooks');
		}
	});
};

Technically, I should have checked to make sure that something was at least altered before I popped up that confirmation, and if not, just whisked you straight away to the My Webhooks page without asking. I may actually do that at some point, but this works for now. Unlike the Cancel button, the Save button will require some server side code, but we can still code out the client side while we are here and then tackle that next. Here is the code for the Save button.

$scope.save = function() {
	if ($scope.form1.$valid) {
		c.server.update().then(function(response) {
			$location.search('id=my_webhooks');
		});
	} else {
		$scope.form1.$setSubmitted(true);
	}
};

Here we check to make sure that there are no validation errors on the form before invoking the server side code, and then we return to the My Webhooks page once the server process has completed. If there are validation errors, then we set the form status to submitted to reveal all of the field errors to the user. SNH Form Fields hide validation errors until you touch the field or the form has been submitted, so setting the form to submitted here reveals any validation errors present for fields that have not yet been touched.

On the server side, we essentially have two events to handle: 1) widget initialization and 2) handling a Save action. The Save action involves input from the client side, so we know that if there is input present, we are doing a Save; otherwise, we are initializing the widget. At initialization, we need to look for a sys_id parameter in the URL, which tells us that we are updating an existing record. If there isn’t one, then we are adding a new record. For existing records, we need to go get the data and for new records, we need to initialize certain fields. Here is all that code:

data.sysId = $sp.getParameter("sys_id");
if (data.sysId) {
	whrGR.get(data.sysId);
	data.number = whrGR.getDisplayValue('number');
	data.type = whrGR.getValue('type');
	data.url = whrGR.getValue('url');
	data.document_id = {value: whrGR.getValue('document_id'), displayValue: whrGR.getDisplayValue('document_id')};
	data.group = {value: whrGR.getValue('group'), displayValue: whrGR.getDisplayValue('group')};
	data.person = {value: whrGR.getValue('person'), displayValue: whrGR.getDisplayValue('person')};
	data.owner = whrGR.getDisplayValue('owner');
	data.active = whrGR.getValue('active')=='1'?true:false;
	data.authentication =  whrGR.getValue('authentication');
	data.username = whrGR.getValue('username');
	data.password = whrGR.getValue('password');
} else {
	data.owner = gs.getUserDisplayName();
	data.active = true;
	data.document_id = {};
	data.group = {};
	data.person = {};
}

Similarly, when we do a Save, we need to know whether we are doing an update or an insert, which we can again tell by the presence of a sys_id. If we are updating, we need to go out and get the current record, and then in all cases, we need to move the data from the screen to the record and then save it. Here is all of that code:

if (input.sysId) {
	whrGR.get(input.sysId);
}
whrGR.type = input.type;
whrGR.url = input.url;
whrGR.document_id = input.document_id.value;
whrGR.group = input.group.value;
whrGR.person = input.person.value;
whrGR.setValue('active', input.active?'1':'0');
whrGR.authentication = input.authentication;
whrGR.username = input.username;
whrGR.password = input.password;
if (input.sysId) {
	whrGR.update();
} else {
	whrGR.insert();
}

That’s pretty much it for the widget. Just to make sure that it works, we can pull up an existing record and see what shows up on the screen.

Webhook Registry widget with existing record

Once I pulled the record up, I switched the Authentication to Basic and then hit the Save button, just to see if the form validation was working. So far, so good, but there is obviously a lot more testing to do, including the integration with the My Webhooks page. Still, things are looking pretty good at this point.

I’m not quite ready to put out an Update Set at the moment, as there is still quite a bit of testing that I would like to do first. Hopefully, though, I won’t find anything too major and I can drop the whole package next time out.

Fun with Outbound REST Events, Part V

“The best preparation for good work tomorrow is to do good work today.”
Elbert Hubbard

At the end of the last installment in this series, I mentioned two possible options for the next item on which to focus our energies. At the time, I wasn’t really sure which direction would be the preferable choice, but now that it is time to fish or cut bait, a decision needs to be made. Given that the entire purpose of this exercise is to demonstrate the use of Event Management practices on internal ServiceNow processes, I believe it would be good to go ahead and wrap up our example Use Case at this point so that we can devote the remainder of our time exclusively on the Event Management aspects. All that is really left to do in order to to complete our address verification scenario is to add the form validation to the User Profile form. That process will leverage the new Script Include and Outbound REST Message that we have already completed.

There are two different ways that we can go about this: we can use an onSubmit Business Rule on the server side, or we can use an onSubmit Client Script on the client side. The Business Rule route is actually a little easier, as you have access to both the current and previous versions of the GlideRecord, and you can call the Script Include directly, without the need for GlideAjax. My problem with that, though, from a User Experience perspective, is that you have to submit the entire form to the server for processing, which then gets reloaded if you have validation issues. My preference is to validate the form right where it sits on the client side, before the form ever gets submitted to the server. For that, we need to build a Client Script.

In fact, for our purpose, we will need two Client Scripts, one an onLoad script and the other an onSubmit script. The reason that we have to have an onLoad script is because you do not have access to the previous field values on the client side like you do with a server-side Business Rule. We will need to snag those values and stuff them somewhere for safekeeping as soon as the form loads. The ServiceNow platform provides a place for just that sort of thing called the g_scratchpad. You can pretty much throw whatever you want in there and it will be available for use until the form is reloaded. The entire onLoad script is just a few short lines of code.

function onLoad() {
	g_scratchpad.originalStreet = g_form.getValue('street');
	g_scratchpad.originalCity = g_form.getValue('city');
	g_scratchpad.originalState = g_form.getValue('state');
	g_scratchpad.originalZip = g_form.getValue('zip');
}

That’s all there is to that. With those initial values safely tucked away, we can then pull them back out later on and refer to them in our onSubmit script to see if anything has changed. Before we start in on our onSubmit script, however, we need to go back to our Script Include and add just a bit of code. When we first built out Script Include, we did not set it up to be client callable, but we are going to need to do that now so that we can access it via GlideAjax. It’s a simple checkbox on the Script Include form, which triggers a slight change in the prototype for the script. That change is handled automatically for any new script, but since we have been working on ours for a while, checking the box may not actually alter any modified code. In that case, you just need to modify the prototype line yourself to look like this:

AddressValidationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {

Also, you will need to go down to the very bottom of the script and insert a closing paren in between the final curly brace and the terminating semi-colon. Once that’s done, we can add a new function that we can call from the client side.

validateAddressViaClient: function() {
	var street = this.getParameter('sysparm_street');
	var city = this.getParameter('sysparm_city');
	var state = this.getParameter('sysparm_state');
	var zip = this.getParameter('sysparm_zip');
	return JSON.stringify(this.validateAddress(street, city, state, zip));
},

This function just grabs all of the parameters passed via GlideAjax and passes them to our existing function, and then turns the response object into a string that can be returned in the XML Ajax response. Now we are all set up to receive calls from the client side.

Being a validation script, our onSubmit script will need to prevent the submission of the form in the event that any validation errors are detected. Without an Ajax call back to the server, that’s easily accomplished by returning false from the onSubmit function. However, making an asynchronous Ajax call means you are leaving your function without the answer in hand, so you don’t know whether or not any issues were detected until the response comes back, which will be in a completely different thread. Although you could utilize the getXMLWait() method to simply wait for the answer, that approach is quite frowned upon in Client Scripts for a variety of reasons, so we do not want to go down that road. Instead, we will use a modified version of this technique.

The approach is to cancel submission of the form before making the Ajax call, and then when the response comes in, submit the form a second time if all is well. Of course, submitting the form again will launch the onSubmit script again, so we need to make it smart enough to know that this is the second submit and to not start the whole process all over again. To accomplish that, we use yet another g_scratchpad property to let the script know that validation has already taken place. Here is the main onSubmit script:

function onSubmit() {
	var submitForm = true;

	var actionName = g_form.getActionName();
	if (!g_scratchpad.isFormValid) {
		submitForm = checkAddress();
	}

	return submitForm;
}

On the first submit, we default the submitForm variable to true, capture the name of the button that was pushed to submit the form so that we can use it when we submit again later, and then check to see if validation has already taken place and passed by testing the isFormValid scratchpad property. In the case of the first submit, isFormValid has not been set to true, so we check the address to see if it needs validated. If it does, then the submitForm variable will be set to false, and the form will not be submitted.

Assuming that it does need to be validated, the checkAddress function will make the Ajax call and cancel the original form submission by returning false. When the Ajax response comes in, if the address is valid, the response function will then submit the form a second time using the saved actionName after setting the isFormValid scratchpad property to true. When the onSubmit function then starts again due to the second submit, it will bypass the address check and simply allow the form to submit due to the isFormValid scratchpad property being set to true.

It’s all a little convoluted, but it works. Here is the checkAddress function:

function checkAddress() {
	var submitForm = true;

	var street = g_form.getValue('street');
	var city = g_form.getValue('city');
	var state = g_form.getValue('state');
	var zip = g_form.getValue('zip');
	if (street || city || state || zip) {
		if (street != g_scratchpad.originalStreet || city != g_scratchpad.originalCity || state != g_scratchpad.originalState || zip != g_scratchpad.originalZip) {
			GlideUI.get().clearOutputMessages();
			var ga = new GlideAjax("AddressValidationUtils");
			ga.addParam('sysparm_name', 'validateAddressViaClient');
			ga.addParam('sysparm_street', street);
			ga.addParam('sysparm_city', city);
			ga.addParam('sysparm_state', state);
			ga.addParam('sysparm_zip', zip);
			ga.getXMLAnswer(processXMLAnswer);
			submitForm = false;
		}
	}

	return submitForm;
}

Just like in the main onSubmit function, we first default the submitForm variable to true, and then we grab all of values for the four address components off of the g_form object. The first thing that we check is if there is even any data in any of the four fields. If they are all empty, then there is nothing to validate. If there is some data there, then the next thing that we check is if it is any different than the values that we squirreled away in our onLoad script. If there are no changes, then again there is no need for validation. But if there is data there and it has changed in any way, now we are going to be making that GlideAjax call. Most of that is pretty standard GlideAjax stuff, but we also clear out any error messages on the screen from previous attempts and set the submitForm variable to false to kill the original form submission.

To process the Ajax response, we have yet another function, processXMLAnswer:

function processXMLAnswer(answer) {
	var response = JSON.parse(answer);
	if (response.result == 'invalid') {
		g_form.addErrorMessage('Unable to verify address entered using the US Address validation service');
		g_form.showFieldMsg('street', 'Unverifiable address', 'error');
		g_form.showFieldMsg('city', 'Unverifiable address', 'error');
		g_form.showFieldMsg('state', 'Unverifiable address', 'error');
		g_form.showFieldMsg('zip', 'Unverifiable address', 'error');
	} else {
		if (response.result == 'valid') {
			g_form.setValue('street', response.street);
			g_form.setValue('city', response.city);
			g_form.setValue('state', response.state);
			g_form.setValue('zip', response.zip);
		}
		g_scratchpad.isFormValid = true;
		g_form.submit(actionName);
	}
}

The response comes back in the form of a string, so the first thing that we have to do is convert it back to an object. If the result property of that object is ‘invalid’, then we leave the form unsubmitted and throw a few error messages up on the screen; otherwise, we are going to submit the form a second time. But before we do that, we check to see if the result property is ‘valid’, in which case we overlay the user’s input with the clean address values returned by the service. After that , we set our isFormValid scratchpad property to true and resubmit the form using the saved actionName.

It’s all a little complicated, having to pass through the onSubmit script twice due to the double submit, but it all makes sense if you really think about it. Of course, we won’t really know if it works until we try it, so let’s pull up a user’s profile and give it a shot.

For our first test, let’s see if we can get a validation error. We can use the test address that we have been using up to this point, but let’s change the state from FL to TX and let’s drop the zip code entirely. Also, let’s put everything in lower case, just for fun. OK, let’s see what happens.

Test using invalid address

Having both form-level and field-level error messages is probably a little overkill, but everything seems to have worked. Now, let’s change the state back to FL and see if that is enough to consider it a valid address.

User profile after address validation

Not only did it pass validation, it also corrected our capitalization and added the missing zip code. That’s actually pretty slick. This wasn’t really the point of this series, but I like this address validation feature. I was originally just looking for something that might have a failure to showcase the Event Management stuff, but this turned out to be a pretty handy little addition that could be useful in a number of other places, such as the Building and Location forms.

This now completes the example working feature that has the potential for failure. From this point on, we can focus on what we came here for, which is to log and process Events that originate in ServiceNow. Next time out, we will start in on the logging.