Service Portal Form Fields, Part IV

“All progress is precarious, and the solution of one problem brings us face to face with another problem.”
Martin Luther King, Jr.

Some things I really like, and others not so much. I like the standard look and feel that you get when you use the same template for everything, and I really like the way that the required marker works without even leaving the field, but I’m not quite sold on the way that the field validation works just yet. I think that could use a little work somewhere, although I am not exactly sure where.

This is the line that triggers the validation:

scope.$watch(fullName + '.$valid', function (isValid) {

Basically, we are watching the $valid property of the field, and then updating the messages based on the $errors that are present for that particular field. Unfortunately, if you have a field with multiple validations, such as a required field with a minimum length, the field is not $valid when it is empty, and it is also not $valid when it is too short. When you start typing in the field, the required field error message does not get replaced with the field too short error message because the state never changed from not being $valid. This leaves a misleading error message underneath that field, which I don’t really care for. I may have to adjust my approach on that to work around that problem. But that’s not really today’s issue, so I’m going to have to set that aside for now.

What I really want to do is test what I have built so far. To expand my testing, I added a few more supported types, but I haven’t really tried them all just yet. Right now, my static list looks like this:

var SUPPORTED_TYPE = ['date', 'datetime-local', 'email', 'month', 'number', 'password', 'tel', 'text', 'textarea', 'time', 'url', 'week'];

Although I haven’t tried testing them all, I did expand my test page just a little to try a few more things out. I added some additional attributes to make sure that those got passed in OK, I added some custom error messages to verify that that process was working as it should, and then added an email field to see how that new type rendered out as well. Here is the latest version of that test page:

<snh-panel title="'${Form Field Test}'">
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.firstName"
          snh-name="firstName"
          snh-label="First Name"
          snh-type="text"
          snh-required="true"
          snh-messages='{"required":"You must enter a First Name"}'/>
        <snh-form-field snh-form="form1"
          snh-model="c.data.middleName"
          snh-name="middleName"
          snh-label="Middle Name"
          snh-type="text"
          snh-required="false" />
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.lastName"
          snh-name="lastName"
          snh-label="Last Name"
          snh-type="text"
          snh-required="true"
          minlength="5"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.textarea"
          snh-name="textarea"
          snh-label="Text Area"
          snh-type="textarea"
          maxlength="20"
          snh-help="This is where the field-level help is displayed."/>
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.email"
          snh-name="email"
          snh-label="Email Address"
          snh-type="email"
          snh-required="true"
          snh-messages='{"required":"You must enter an Email address", "email":"You must enter a valid Email address"}'/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.comments"
          snh-name="comments"
          snh-label="Comments"
          snh-type="textarea"/>
      </div>
    </div>
  </form>
</snh-panel>

… and here is how it renders out, after I triggered a few of the validation errors:

Form Field Validation Test

For the most part, I am still very pleased with the results so far, but I am definitely going to have to make some adjustments in the validation message area to clean up the way that works. There is still much to be done, but I am getting closer to having a somewhat finished product.

Service Portal Form Fields, Part III

“For the great doesn’t happen through impulse alone, and is a succession of little things that are brought together.”
Vincent van Gogh

It’s only just the very beginnings, but I am really starting to like how things are turning out, at least so far. There is a lot left to do, and so much so that it is difficult to decide on what should be tackled next, but at least things are starting to take shape, and you can begin to see where things might lead. If you were paying close attention, you will notice that my initial HTML structure left out a couple of key elements from the example that we lifted from the Incident form. One is the hidden INPUT element that contains the original value and the other is the DIV element that contains all of the “extras” — that collection of little icons to the right of the INPUT element that do things like pop up the details for a reference field or provide some other functionality related to the value of the field. The first I skipped because I wasn’t exactly sure what value that provided in the AngularJS world, and the second I skipped because I’m not exactly sure how I would support that feature at this point. I may go back to one or both of those at some point, but for now, I’ve left those out of this little experiment.

So, the question remains: where do we go from here? Although I’m seriously tempted to start adding support for additional input types beyond text and textarea, I think I will defer that for now and start digging into field validation. I’ve been doing a little reading on HTML5 form validation and AngularJS form validation, and it appears that you have to make a conscious decision to go one way or another. Not being that familiar with either one, it will be a learning experience for me either way, so I thought briefly about just flipping a coin just to get the deciding part over with. After further review, though, I decided to go the AngularJS route, mainly because I need to learn a lot more about AngularJS anyway.

As usual, I hunt down the easy parts first and get those out of the way. There are a handful of static values that I want to use for reference purposes, so I came up with those first:

var SUPPORTED_TYPE = ['text', 'textarea'];
var RESERVED_ATTR = ['ng-model', 'class', 'id', 'name', 'required'];
var STD_MESSAGE = {
	email: "Please enter a valid email address",
	max: "Please enter a smaller number",
	maxlength: "Please enter fewer characters",
	min: "Please enter a larger number",
	minlength: "Please enter more characters",
	number: "Please enter a valid number",
	pattern: "Please enter a valid value",
	required: "This information is required",
	url: "Please enter a valid URL",
	date: "Please enter a valid date",
	datetimelocal: "Please enter a valid local date/time",
	time: "Please enter a valid time",
	week: "Please enter a valid week",
	month: "Please enter a valid month"
};

The first is an array of supported types, which right now just stands at text and textarea. I plan to expand on that in the future, and this array will help make those additions a little easier. The second is an array of reserved attributes, and the purpose of that one is to allow me to copy all of the attributes from the original snh-form-field element over to the generated input element, except those that are on that list (well, those that are on that list, plus any that start with snh- or $). That way, developers can specify any additional attributes and they will be carried forward as long as they are not already used in the processing of the directive. The last is a list of standard validation messages, which I also might expand on at some point in the future. I found that initial list of potential keys out on the Interwebs somewhere, and it looked like a good place to start, so I went ahead and grabbed it and tried to come with an appropriate message for each one.

.I already included the code that uses that first one in my last installment, but let’s reproduce it here, just to show how that is supposed to work:

var type = attrs.snhType || 'text';
type = type.toLowerCase();
if (SUPPORTED_TYPE.indexOf(type) == -1) {
	type = 'text';
}

Initially, we get the type from the snh-type attribute, or if you don’t provide one, we default it to text. Then, we convert it to lower case and check it against our array of supported types. If we don’t find it on the list, then again, we default it over to text. This ensures that we are always working with a valid, supported type.

The second array contains any attributes that might be used in our template, and we maintain that list so that we don’t pass along any conflicting values for any of those. When we copy over the remaining attributes, which we do in a function so that it can be called from multiple places, we use the list in much the same was as we did this first one, but this time we are not looking for the ones to keep, but rather the ones to avoid.

function passThroughAttributes(attrs) {
	var tags = '';
	for (var name in attrs) {
		if (name.indexOf('snh') != 0 && name.indexOf('$') != 0 && RESERVED_ATTR.indexOf(name) == -1) {
			tags += ' ' + name + '="' + attrs[name] + '"';
		}
	}
	return tags;
}

Basically, if it doesn’t start with snh and it doesn’t start with $ and it isn’t found on the list, then we go ahead and pass it through to the generated INPUT field, whatever that happens to be.

The last item is not an array, but a simple object, or data map, of the standard messages to use for the known list of possible validation error key values. I created an attribute called snh-messages so that developers could override these messages, but the basic idea here that there is a default message for everything, and if that’s good enough for your purposes, then there is nothing more for you to have to do.

for (var key in elem.$error) {
	elem.validationErrors += separator;
	if (overrides[key]) {
		elem.validationErrors += overrides[key];
	} else if (STD_MESSAGE[key]) {
		elem.validationErrors += STD_MESSAGE[key];
	} else {
		elem.validationErrors += 'Undefined field validation error: ' + key;
	}
	separator = '<br/>';
}

The above code loops through all of the validation errors, and if an override value was provided, that is the message that will be used. If not, and there is a standard message available, then that is the message that will be used. If there is no override and no standard message, then we report the fact that we do not have a message to go along with that particular error, and include the error key.

So, the full thing looks like this at this point:

function() {
	var SUPPORTED_TYPE = ['text','textarea'];
	var RESERVED_ATTR = ['ng-model', 'class', 'id', 'name', 'required'];
	var STD_MESSAGE = {
		email: "Please enter a valid email address",
		max: "Please enter a smaller number",
		maxlength: "Please enter fewer characters",
		min: "Please enter a larger number",
		minlength: "Please enter more characters",
		number: "Please enter a valid number",
		pattern: "Please enter a valid value",
		required: "This information is required",
		url: "Please enter a valid URL",
		date: "Please enter a valid date",
		datetimelocal: "Please enter a valid local date/time",
		time: "Please enter a valid time",
		week: "Please enter a valid week",
		month: "Please enter a valid month"
	};
	return {
		restrict: 'E',
		replace: true,
		require: ['^form'],
		template: function(element, attrs) {
			var htmlText = '';
			var form = attrs.snhForm;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			var type = attrs.snhType || 'text';
			type = type.toLowerCase();
			if (SUPPORTED_TYPE.indexOf(type) == -1) {
				type = 'text';
			}
			var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
			var model = attrs.snhModel;
			var value = '';
			htmlText += "    <div id=\"element." + name + "\" class=\"form-group\">\n";
			htmlText += "      <div id=\"label." + name + "\" class=\"snh-label\" nowrap=\"true\">\n";
			htmlText += "        <label for=\"" + name + "\" class=\"col-xs-12 col-md-4 col-lg-6 control-label\">\n";
			htmlText += "          <span id=\"status." + name + "\"";
			if (required) {
				htmlText += " ng-class=\"" + model + ">''?'snh-required-filled':'snh-required'\"";
			}
			htmlText += "></span>\n";
			htmlText += "          <span title=\"" + attrs.snhLabel + "\" data-original-title=\"" + attrs.snhLabel + "\">" + attrs.snhLabel + "</span>\n";
			htmlText += "        </label>\n";
			htmlText += "      </div>\n";
			if (attrs.snhHelp) {
				htmlText += "    <div id=\"help." + name + "\" class=\"snh-help\">" + attrs.snhHelp + "</div>\n";
			}
			if (type == 'textarea') {
				htmlText += "      <textarea class=\"form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
			} else {
				htmlText += "      <input class=\"form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
			}
			htmlText += "      <div id=\"message." + name + "\" ng-show=\"" + fullName + ".$touched && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
			htmlText += "     </div>\n";
			return htmlText;

			function passThroughAttributes(attrs) {
				var tags = '';
				for (var name in attrs) {
					if (name.indexOf('snh') != 0 && name.indexOf('$') != 0 && RESERVED_ATTR.indexOf(name) == -1) {
						tags += ' ' + name + '="' + attrs[name] + '"';
					}
				}

				return tags;
			}
		},
		link: function(scope, element, attrs, ctls) {
			var form = ctls[0].$name;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			var overrides = {};
			if (attrs.snhMessages) {
				overrides = JSON.parse(attrs.snhMessages);
			}
			scope.$watch(fullName + '.$valid', function (isValid) {
				var elem = scope.$eval(fullName);
				elem.validationErrors = '';
				var separator = '';
				if (!isValid) {
					for (var key in elem.$error) {
						elem.validationErrors += separator;
						if (overrides[key]) {
							elem.validationErrors += overrides[key];
						} else if (STD_MESSAGE[key]) {
							elem.validationErrors += STD_MESSAGE[key];
						} else {
							elem.validationErrors += 'Undefined field validation error: ' + key;
						}
						separator = '<br/>';
					}
				}
			});
		}
	};
}

One thing that I had to do temporarily was to pass the name of the form to the template as an attribute. I don’t think that I should have to do that, and I really don’t like it, but I spent way too much time trying to figure out how to pick that up from the element itself with no success, so I put that aside for now and just passed it in as another attribute. I don’t like that, though, and I really don’t think that that should be necessary, but for now, I have put that aside for another day and intend to go back to it at some point and make that right.

There is still a lot more to do, but it is starting to come together. Next time, I think I will do a little testing to make sure all of this works, and then maybe explore supporting a few more different type values.

Service Portal Form Fields, Part II

“The journey of a thousand miles begins with a single step.”
— Lao Tzu (Tao te Ching)

Last week, I had this idea, and today I think I have a fairly good plan. I am going to make a copy of my old sn-panel hack and make myself another Angular Provider for a generic form field and all of the associated trimmings. My strategy, as usual, is to start out small see what I can get to work, and then build it up piece by piece until it finally does everything that I want it to do. I’ll use the tag snh-form-field to invoke the provider, and then I will create my own snh-* attributes and snh-* styles, just to be sure that I don’t conflict with anything already existing in the platform. Ultimately, I want to be able to support any kind of valid HTML5 input, but to start with, I’m going to limit the initial version to just text and textarea fields. At this point, I don’t want to drive things off of the dictionary; I see that as something that would live outside of this process. Everything will be driven off of the attributes specified in the tag, and if I ever do add something driven off of the dictionary, it will simply leverage this work and use the dictionary to determine what the attribute values should be.

To begin, let’s start out with the basic template. This is pretty much the heart and soul of the entire operation, so might as well start out with the basic HTML framework to be used for all types of input. Here is my first attempt at the essential code needed to produce the simplest of structures to include all of the standard elements:

var htmlText = '';
var name = attrs.snhName;
var model = attrs.snhModel;
var type = attrs.snhType || 'text';
type = type.toLowerCase();
if (SUPPORTED_TYPE.indexOf(type) == -1) {
	type = 'text';
}
var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
htmlText += "    <div id=\"element." + name + "\" class=\"form-group\">\n";
htmlText += "      <div data-type=\"label\" id=\"label." + name + "\" nowrap=\"true\">\n";
htmlText += "        <label for=\"" + name + "\" class=\"col-xs-12 col-md-4 col-lg-6 control-label\">\n";
htmlText += "          <span id=\"status." + name + "\"";
if (required) {
	htmlText += " ng-class=\"" + model + ">''?'snh-required-filled':'snh-required'\"";
}
htmlText += "></span>\n";
htmlText += "          <span title=\"" + attrs.snhLabel + "\" data-original-title=\"" + attrs.snhLabel + "\">" + attrs.snhLabel + "</span>\n";
htmlText += "        </label>\n";
htmlText += "      </div>\n";
if (attrs.snhHelp) {
	htmlText += "    <div id=\"help." + name + "\" class=\"snh-help\">" + attrs.snhHelp + "</div>\n";
}
if (type == 'textarea') {
	htmlText += "      <textarea class=\"form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + (required?' required':'') + "></textarea>\n";
} else {
	htmlText += "      <input class=\"form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + (required?' required':'') + "/>\n";
}
htmlText += "      <div id=\"message." + name + "\" class=\"snh-error-msg\" ng-show=\"" + model + "ErrorMsg>''\">{{" + model + "ErrorMsg}}</div>\n";
htmlText += "     </div>\n";
return htmlText;

The first thing that you see is that we pull some commonly used variables out the associated attributes, mainly for the sake of clarity and brevity. The first real line of HTML is the outer enclosing DIV of everything else, which we name in accordance with the standard as “element.” plus the “name” of the field. The next line is the outer enclosing DIV of field label, which we name in accordance with the standard as “label.” plus the “name” of the field. Next is the LABEL tag, which doesn’t have a name, followed by the SPAN for the required marker, which we name in accordance with the standard as “status.” plus the “name” of the field. After that comes the SPAN for the field label, which also doesn’t have a name, but uses the snh-label attribute for the title as well as the value. Following the label SPAN is the closing LABEL tag and then the closing DIV tag for the field label elements, completing the field label block.

Just below the field label there is an optional DIV for field-level help. You don’t see that all that much, but it is an option on ServiceNow forms, so I wanted to be sure to include it. After that comes the INPUT element itself, the formatting of which is entirely dependent on the type of input. For now, just to have something to start with, I am only supporting two different types, text and textarea. We’ll add more later, but this will be enough to get us started. After the INPUT element, we have the DIV for validation errors, and then we wrap everything up with the closing DIV tag for the outer enclosure. That completes the basic HTML structure for a form field.

To try it out, we’ll need a simple widget with a few form fields defined. Just to see how things come out, let’s do a few text fields and a few textarea fields, make some required and some not required. And just for fun, let’s wrap the whole thing in the old snh-panel.

<snh-panel rect="rect" title="'${Form Field Test}'">
  <form role="form" id="form1" ng-init="checkInputs()">
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field snh-model="c.data.firstName" snh-name="firstName" snh-label="First Name" snh-type="text" snh-required="true" />
        <snh-form-field snh-model="c.data.middleName" snh-name="middleName" snh-label="Middle Name" snh-type="text" snh-required="false" />
        <snh-form-field snh-model="c.data.lastName" snh-name="lastName" snh-label="Last Name" snh-type="text" snh-required="true" />
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field snh-model="c.data.textarea" snh-name="textarea" snh-label="Text Area" snh-type="textarea"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field snh-model="c.data.comments" snh-name="comments" snh-label="Comments" snh-type="textarea"/>
      </div>
    </div>
  </form>
</snh-panel>

Of course, we have to build a Portal Page so that we have somewhere to put our widget, but that’s all basic ServiceNow stuff that we don’t need to go into here. Let’s just assume that we did all that and get right to the good stuff, which is to see how it all comes out when you bring up in the browser.

First Form Field Test

Not bad! The required marker actually works pretty cool … the minute that you type a single letter into the field, it goes from red to grey. I’m used to that changing once you leave the field, but this does it right then and there while you are entering the data. The form doesn’t validate just yet, but hey — it’s a start. I think I am going to really like this once it has all been put together. Next time, we will add a little more and see just how much further we can get …

Service Portal Form Fields

“Everything begins with an idea.”
Earl Nightingale

Even though I tend to prefer the Service Portal environment over the original ServiceNow UI, the one thing that you just can’t beat on the original side of the house is the built-in support for form fields. As soon as you create a Table in ServiceNow, a corresponding form is also created, built using a generic infrastructure that is driven off of the embedded data dictionary. Every field on the form is formatted in a consistent way using consistent components supporting consistent features. So far, I have not seen anything comparable to that in the Service Portal environment.

On a standard ServiceNow UI form, every field on the page will have the same set of standard elements based on the same basic template, customized to the specifications of the specific field as contained in the data dictionary. For example, here is the HTML from the Incident ID field on the Incident form:

   <div id="element.incident.number" class="form-group " style="">
      <div class="" data-type="label" choice="0" type="string" id="label.incident.number" nowrap="true"><label onclick="return labelClicked(this);" for="incident.number" dir="ltr" class=" col-xs-12 col-md-3 col-lg-4 control-label"><span id="status.incident.number" data-dynamic-title="" mandatory="false" oclass="" class=" label_description" aria-label=""></span><span title="" class="label-text" data-html="false" data-original-title="">Number</span></label></div>
      <div class="col-xs-10 col-sm-9 col-md-6 col-lg-5 form-field input_controls">
         <div ng-non-bindable="" class="hidden"><input id="sys_original.incident.number" name="sys_original.incident.number" value="INC0010037" type="hidden"></div>
         <input id="incident.number" name="incident.number" aria-required="false" onchange="onChange('incident.number');" maxlength="40" value="INC0010037" style="; " autocomplete="off" ng-non-bindable="" class="form-control" spellcheck="false" type="text">
      </div>
      <div class="col-xs-2 col-sm-3 col-lg-2 form-field-addons"></div>
   </div>

Looking through the code, you can spot a number of unique standard elements, each with its own specific purpose, and many with a standard ID:

  • element.incident.number
  • label.incident.number
  • status.incident.number
  • sys_original.incident.number
  • incident.number

So, in theory, it seems as if you could use the above block of HTML as a guide and set up some kind of AngularJS component to replicate that entire concept over on the Service Portal side of the house. Unfortunately, I’m not all that familiar with AngularJS, but it sounds like an interesting challenge. Who knows … maybe someone has already put this together. Surely someone smarter than I am has already thought of doing this, and maybe has already figured all of it out. Even if that’s true, it would still be fun to try on my own. You know what I always ask myself: how hard could it be?

Portal Widgets on UI Pages

“Our life is frittered away by detail. Simplify, simplify.”
Henry David Thoreau

There are two distinctly different environments on ServiceNow, the original ServiceNow UI based on Lists, Forms, and UI Pages, and the more recent Service Portal environment based on Widgets and Portal Pages. The foundation technology for UI Pages is Apache Jelly. The foundation technology for Service Portal Widgets is AngularJS. From a technology perspective, the two really aren’t all that compatible; when you are working with the legacy UI, you work in one technology and when you are working with the Service Portal, you work in the other.

Personally, I like AngularJS better than Jelly, although I have to admit that I am no expert in either one. Still, given the choice, my preferences always seem to tilt more towards the AngularJS side of the scale. So when I had to build a modal pop-up for a UI Page, my inclination was to find a way to do it using a Service Portal Widget. To be completely honest, I wanted to find a way to be able to always use a widget for any modal pop-up on the ServiceNow UI side of things. That way, I could hone my expertise on just one approach, and be able to use it on both sides of the house.

The solution actually turned out to be quite simple: I created a very simple UI Page that contained just a single element, an iframe. I passed just one parameter to the page, and that was the URL that was to be the source for the iframe. Now, that URL could contain multiple URL parameters to be passed to the interior widget, but from the perspective of the UI Page itself, all you are passing in is the lone URL, which is then used to establish the value of the src attribute of the iframe tag. There are two simple parts to the page, the HTML and the associated script. Here is the HTML:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<iframe id="portal_page_frame" style="height: 400px; width: 100%; border: none;"></iframe>
</j:jelly>

… and here is the script:

var gdw = GlideDialogWindow.get();
var url = gdw.getPreference('url');
document.getElementById('portal_page_frame').src = url;

The script simply gets a handle on the GlideDialogWindow so that it can snag the URL “preference”, and then uses that value to establish the source of the iframe via the iframe’s src attribute. To pop open a Service Portal Widget using this lightweight portal_page_container, you would use something like the following:

var dialog = new GlideDialogWindow("portal_page_container");
dialog.setPreference('url', '/path/to/my/widget');
dialog.render();

That’s it. Simple. Easy. Quick. Just the way I like it! If you want a copy to play with, you can get it here.

Update: There is an even better way to do this, which you can find here.