Fun with the Service Portal Data List Widget

“I can’t do it never yet accomplished anything. I will try has performed miracles.”
George Burnham

Straight out of the box, the ServiceNow Service Portal comes bundled with a trio of widgets for displaying rows of a table: a primary data table widget and two additional wrapper widgets that provide different means to pass configuration parameters to the primary widget.

Service Portal data table widgets

One of the wrapper widgets is the Data Table from URL Definition widget, which was almost exactly what I was looking for. The problem, of course, was the almost. I needed something that was exactly what I was looking for. So close, but no cigar. The problem was that it took most, but not all, of the parameters that I wanted pass via the URL. You can pass in the name of the table, the name of the view, a query filter, and a number of other, related options, but you cannot pass in a list of columns that you want to have displayed in the list. There is a property called fields, which is set up for that very purpose, but its value is hard-coded rather than being pulled from the URL.

Well, that won’t work!

Here is the line in question:

data.fields = $sp.getListColumns(data.table, data.view);

Here is what I would like see on that line:

data.fields = $sp.getParameter('fields') || $sp.getListColumns(data.table, data.view);

That really shouldn’t hurt anything at all. All that would do would be to take a quick peek at the URL, and if someone provided a list of fields, then it would use that list; otherwise, it would revert to what it is currently doing right now. It would simply add what I wanted without taking away anything that it is already set up to do. Unfortunately, this particular widget is one of those provided in read-only mode and you are not allowed to modify it, even if you are an admin. Well, isn’t that restrictive!

The recommended course of action in these cases is to make a clone or copy of the protected widget under a new name and then modify that one, leaving the original intact. I thought about doing just that, but I’m not really one to blindly follow the recommended course of action at every turn. I just wanted to make this one small change to this one and leave it at that. Fortunately, there is a way to do just that. First, you have to export the widget to XML.

Exporting the widget to XML

Next, make whatever modifications that you want to make to the exported XML, being careful not to disturb anything else, and the save the updated XML. Now, go back to the list of widgets and use the hamburger menu on one of the list columns to select Import XML.

Importing the widget XML back into ServiceNow

Browse for your XML file, upload it, and now the modified widget is back where it belongs with your modification in place. Voila! Easy, peasy. Now, I can get back to doing what I wanted to do with this widget in the first place.

Service Portal Form Fields, Part IX

“I do one thing, I do it very well. And then I move on.”
Charles Emerson Winchester III

Generally speaking, I try not to tinker. Once an idea has been turned into an actual functioning reality, I like to set that aside and move on to other ideas. But there’s always that one little thing that you realize that you could have done better, or just differently, and there is this constantly nagging temptation to go back and fine tune things just to make them a wee bit better. I prefer not to give in to that temptation and just press forward towards other aspirations, but occasionally there are some things that you just can’t let go. So here we are …

I started out thinking about drafting some actual instructions, and identifying which attributes are mandatory and which are optional. Then I realized that there really isn’t any code that requires you to include any attribute at all. Things just won’t work if you don’t get it right. I don’t really like that, so I added a few lines to put out a configuration error instead of making a failed attempt to render HTML in the event that you left out a critical attribute. That turned out to be a relatively simple thing to do:

if (!name) {
	htmlText = configurationError('Attribute snh-name is required');
} else if (!model) {
	htmlText = configurationError('Attribute snh-model is required');
} else {
	...

Of course, once you crack open the code, you’ve now opened the door to squeezing in all of those other little tweaks and tidbits that you thought about doing earlier but never got around to actually putting into place. I don’t like having to specify things if it isn’t absolutely necessary, which is why I kept looking for a way to avoid having to specify the form name. It is also the reason why I default the the type value to text. The label is another thing I didn’t think you should have to specify, as you could use the name for that purpose, if no label was provided. I never put that in there, though, so since I was already opening up the code for another version, I went ahead and threw that in there as well.

var label = attrs.snhLabel;
if (!label) {
	label = nameToLabel(name);
}

Something else that I had been contemplating was throwing in an SMS link icon for cell phone numbers in addition to the link icon that I had put in to place a call. A couple of teeny little wrinkles popped up in that exercise, but nothing too awfully exciting really. For one, I didn’t want that showing up for land lines, so I ended up basing its presence on the label, and only had it show up if the label contained the word cell or mobile. The other thing that cropped up was that AngularJS converted the sms: protocol to unsafe:sms: for some reason, and it took a little while to figure out how to stop that from happening. Apparently, you have to add this line of script outside of your widgets and providers:

angular.module('sn.$sp').config(['$compileProvider',function( $compileProvider ){ 
        $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|sms|tel):/) ;
}]);

I’m not all that clear on how all of that works, but it does work, so I’m good. All together, it’s not all that much of an improvement, but it’s different, so I guess we’ll call this one v1.2. Now I just need to put it all together into another Update Set for anyone who might want play around with it on their own.

Service Portal Form Fields, Part VIII

“I regret only one thing, which is that the days are so short and that they pass so quickly. One never notices what has been done; one can only see what remains to be done, and if one didn’t like the work it would be very discouraging.”
Maria Sklodowska

Well, I finally resolved the two major annoyances of my little form field project. One was having to include the form name as an attribute when I felt for sure that there must be a way to derive that without having to pass it in explicitly, and the other was related to the validation message behavior. When my error message routine was based on watching $valid, the message did not change even though the reason that it was not $valid changed. Empty required fields would show the message about the field being required, but when you started typing and the value was not yet a valid value, that original message would remain rather than switching to something more appropriate such as explaining to you that the value that you were entering was not a valid value. I tried switching to watching $error instead of $valid, but that didn’t work at all. So, I went out and scoured the Interwebs for an answer to my dilemma.

As it turns out, watching $error really was the right thing to do, but in order for that to work, I had to add true as a third argument to the scope.$watch function call (the first argument is the element to watch and the second is the function to run when something changes). I’m not sure what that third argument is or does, but I now know that if you add it, then it works, so I guess that’s all I really need to know.

As for the form name, after many, many, many different experiments, I finally stumbled across a sequence of random letters, numbers, and symbols that consistently returned the name of the form:

var form = element.parents("form").toArray()[0].name;

Of course, now that it is laid out right there in front of you in plain sight, you can tell right away how intuitively obvious that should have been from the start. Why I didn’t just know that right off of the bat will remain an eternal mystery, but the good news is that we have the secret now, and I can finally remove all of those unneeded snh-form=”form1″ attributes from all of my various test cases. I always felt as if that shouldn’t have been necessary, but I could never quite come up with an approach that would return the name of the form in every instance. Fortunately, I am rather relentless with these kinds of things and I just kept trying things until I finally stumbled upon something that actually worked.

Those were the two major items on my list of stuff that I thought needed to be addressed before we could really call this good enough. I also did a little bit of clean-up in a few other areas as well, just tidying a few little odds and ends up here and there where I thought things could use a little improvement. Here is the full text of the current version of the script that performs all of the magic:

function() {
	var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'inlineradio', 'month', 'number', 'password', 'radio', 'reference', 'tel', 'text', 'textarea', 'time', 'url', 'week'];
	var RESERVED_ATTR = ['ngHide', 'ngModel', 'ngShow', 'class', 'field', 'id', 'name', 'required'];
	var SPECIAL_TYPE = {
		email: {
			title: 'Send an email to {{MODEL}}',
			href: 'mailto:{{MODEL}}',
			icon: 'mail'
		},
		tel: {
			title: 'Call {{MODEL}}',
			href: 'tel:{{MODEL}}',
			icon: 'phone'
		},
		url: {
			title: 'Open {{MODEL}} in a new browser window',
			href: '{{MODEL}}" target="_blank',
			icon: 'pop-out'
		}
	};
	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 = element.parents("form").toArray()[0].name;
			var name = attrs.snhName;
			var model = attrs.snhModel;
			var type = attrs.snhType || 'text';
			var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
			var fullName = form + '.' + name;
			var refExtra = '';
			type = type.toLowerCase();
			if (SUPPORTED_TYPE.indexOf(type) == -1) {
				type = 'text';
			}
			if (type == 'reference') {
				fullName = form + "['" + name + " ']";
				refExtra = '.value';
			}
			htmlText += "    <div id=\"element." + name + "\" class=\"form-group\"";
			if (attrs.ngShow) {
				htmlText += " ng-show=\"" + attrs.ngShow + "\"";
			}
			if (attrs.ngHide) {
				htmlText += " ng-hide=\"" + attrs.ngHide + "\"";
			}
			htmlText += ">\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 + refExtra + ">''?'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 == 'radio' || type == 'inlineradio') {
				htmlText += buildRadioTypes(attrs, name, model, required, type);
			} else if (type == 'choicelist') {
				htmlText += buildChoiceList(attrs, name, model, required);
			} else if (SPECIAL_TYPE[type]) {
				htmlText += buildSpecialTypes(attrs, name, model, required, type, fullName);
			} else if (type == 'reference') {
				htmlText += "      <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
			} else if (type == 'textarea') {
				htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
			} else {
				htmlText += "      <input class=\"snh-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 + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
			htmlText += "    </div>\n";
			return htmlText;

			function buildRadioTypes(attrs, name, model, required, type) {
				var htmlText = "      <div style=\"clear: both;\"></div>\n";

				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						if (type == 'radio') {
							htmlText += "      <div>\n  ";
						}
						htmlText += "        <input ng-model=\"" + model + "\" id=\"" + name + thisOption.value + "\" name=\"" + name + "\" value=\"" + thisOption.value + "\" type=\"radio\"" + passThroughAttributes(attrs) + (required?' required':'') + "/> " + thisOption.label + "\n";
						if (type == 'radio') {
							htmlText += "      </div>\n";
						}
					}
				}

				return htmlText;
			}

			function buildChoiceList(attrs, name, model, required) {
				var htmlText = "      <select class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + ">\n";
				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				htmlText += "        <option value=\"\"></option>\n";
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						htmlText += "        <option value=\"" + thisOption.value + "\">" + thisOption.label + "</option>\n";
					}
				}
				htmlText += "      </select>\n";

				return htmlText;
			}

			function buildSpecialTypes(attrs, name, model, required, type, fullName) {
				var title = SPECIAL_TYPE[type].title.replace('MODEL', model);
				var href = SPECIAL_TYPE[type].href.replace('MODEL', model);
				var icon = SPECIAL_TYPE[type].icon;
				var htmlText = "      <span class=\"input-group\" style=\"width: 100%;\">\n";
				htmlText += "       <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
				htmlText += "       <span class=\"input-group-btn\" ng-show=\"" + fullName + ".$valid && " + model + " > ''\">\n";
				htmlText += "        <a href=\"" + href + "\" class=\"btn-ref btn btn-default\" role=\"button\" title=\"" + title + "\" tabindex=\"-1\" data-original-title=\"" + title + "\">\n";
				htmlText += "         <span class=\"icon icon-" + icon + "\" aria-hidden=\"true\"></span>\n";
				htmlText += "         <span class=\"sr-only\">" + title + "</span>\n";
				htmlText += "        </a>\n";
				htmlText += "       </span>\n";
				htmlText += "      </span>\n";
				return htmlText;
			}

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

			function camelToDashed(camel) {
				return camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
			}
		},
		link: function(scope, element, attrs, ctls) {
			var form = ctls[0].$name;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			if (!scope.$eval(fullName)) {
				fullName = form + '["' + name + ' "]';
			}
			var overrides = {};
			if (attrs.snhMessages) {
				overrides = scope.$eval(attrs.snhMessages);
			}
			scope.$watch(fullName + '.$error', function () {
				var elem = scope.$eval(fullName);
				elem.validationErrors = '';
				var separator = '';
				if (elem.$invalid) {
					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/>';
					}
				}
			}, true);
		}
	};
}

I already released an almost good enough version that I ended up calling 1.0, so I guess we’ll have to call this one 1.1. You can grab the full Update Set, which now includes the CSS as a separate file instead of being pasted into the test widget like it was in the original version. It would probably be beneficial to include some semblance of documentation at some point, but that will have to wait for a later release.

Service Portal Form Fields, Part VII

“Finishing races is important, but racing is more important.”
Dale Earnhardt

After tinkering around with various uses and locations for the Retina Icons that I came across the other day, I finally settled on decorating three of my form field types with action buttons:

Form Fields with related Action Buttons


For the email form field type, the button sends an email, for the tel form field type, the button calls the number, and for the url form field type, the button opens up a new browser and navigates the the URL. To make the magic happen, I added yet another static variable listing out the types for which I created support for actions.

var SPECIAL_TYPE = ['email', 'tel', 'url'];

Then I created a function to format the types listed in that array. The code is essentially the same for all three types, with the differences only being in the icon, the link, and the associated help text or title. I considered creating a JSON object instead of a simple array, and including the three unique values for each type, but to be completely honest, I really didn’t think of doing that until after I had already done it the other way and I was too lazy to go back and refactor everything. So for now, the array drives the decision to utilize the function, and the function contains a bunch of hard-coded if statements to sort out what is unique to each type.

One thing that I did not want to do was have the action buttons out there when there was no value in the field or when the value was not valid. To only show the buttons when there was something there that would actually work with the button code, I added an ng-show to the outer element preventing it from displaying unless the conditions were right.

Action Buttons removed when data is missing or invalid

Aside from the cool Retina Icons and hiding the buttons when not wanted, there isn’t too much else noteworthy about the code. It does work, which is always a good thing, but one day I can see myself going back in here and doing a little optimization here and there. But this is what the initial version looks like today:

function buildSpecialTypes(attrs, name, model, required, fullName) {
	var title = '';
	var href = '';
	var icon = '';
	if (type == 'email') {
		title = 'Send an email to {{' + model + '}}';
		href = 'mailto:{{' + model + '}}';
		icon = 'mail';
	}
	if (type == 'tel') {
		title = 'Call {{' + model + '}}';
		href = 'tel:{{' + model + '}}';
		icon = 'phone';
	}
	if (type == 'url') {
		title = 'Open {{' + model + '}} in a new browser window';
		href = '{{' + model + '}}" target="_blank';
		icon = 'pop-out';
	}
	var htmlText = "      <span class=\"input-group\" style=\"width: 100%;\">\n";
	htmlText += "       <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
	htmlText += "       <span class=\"input-group-btn\" ng-show=\"" + fullName + ".$valid && " + model + " > ''\">\n";
	htmlText += "        <a href=\"" + href + "\" class=\"btn-ref btn btn-default\" role=\"button\" title=\"" + title + "\" tabindex=\"-1\" data-original-title=\"" + title + "\">\n";
	htmlText += "         <span class=\"icon icon-" + icon + "\" aria-hidden=\"true\"></span>\n";
	htmlText += "         <span class=\"sr-only\">" + title + "</span>\n";
	htmlText += "        </a>\n";
	htmlText += "       </span>\n";
	htmlText += "      </span>\n";
	return htmlText;
}

I still have my issues to deal with, but thanks to my pervasive skills at creative avoidance, we can put that off to a later installment, which will also be a good time to release an improved version of the Update Set for all of this.

Service Portal Form Fields, Part VI

“If at first you don’t succeed, call it version 1.0”
Charles Lauller

Well, that was an adventure! Even though I was just about 100% sure that it wouldn’t work, I went ahead and defined yet another type for my snh-form-field tag to experiment with embedding an sn-record-picker. It seemed highly unlikely to me that AngularJS was smart enough to run through the evaluation process on a tag that was rendered in a template that itself was resolved in the evaluation process. But, I like to try things even when I’m pretty sure that they won’t work, so I added reference to my list of supported types and added this to my little test page widget:

        <snh-form-field
          snh-form="form1"
          snh-model="c.data.reference"
          snh-name="reference"
          snh-label="Reference Field"
          snh-type="reference"
          table="'cmn_department'"
          display-field="'name'"
          display-fields="'id'"
          value-field="'sys_id'"
          search-fields="'id,name'"
          default-query="'u_active=true"
          page-size="100"
          snh-required="true"/>

To my utter amazement, it actually worked! I did not expect that. I was pleasantly surprised to see that it worked perfectly fine, and I was very happy that I had decided to give it shot before trying to figure out some other way to incorporate this capability in my own directive. There were, of course, a few little adjustments needed here and there, but hey — it was pretty cool to see that it actually worked without me having to do much of anything. I was both astonished and elated. Elation, though, is a temporary state, and things really started to go down from there.

To start with, it just looked a little funky. It was like a field within a field, but not completely, with one element bleeding over the bottom edge of the other. Something just wasn’t right somewhere.

First attempt at setting up a reference field

That one wasn’t that hard to figure out though. I had added the snh-form-control class to the sn-record-picker tag, but the sn-record-picker generates its own class attribute on a different level, so the two were redundant as well as conflicting. I removed my class attribute and that solved that problem. But that was really an insignificant problem compared to what I noticed next: the field wasn’t validating.

The required field indicator was grey instead of red, no matter whether or not you had selected anything from the drop-down. And no error messages appeared no matter what you did. It was as if validation wasn’t turned on for that field at all. Then I remembered that the model for an sn-record-picker is not a string containing the value selected, but an object containing a property that contains the value selected, appropriately named value. Where the value of every other type of field was <form-name>.<field-name>, the value of an sn-record-picker is <form-name>.<field-name>.value. That meant putting in some conditional code just for the reference type fields, which I had not really had to do for any other types up to this point.

Unfortunately, things still were not working after all of that. Now I was starting to get a little frustrated because the picker itself just worked like a charm right out of the gate, which I absolutely did not expect, but the field validation, which I thought would be the one part to work just fine, still wasn’t working at all. I kept digging, though, and eventually I figure out that when the sn-record-picker renders the name attribute, it throws in an extra little trailing space at the end of field name. Instead of name=”reference” I ended up with name=”reference “. Well, that little extra, unwelcome, trailing space means that <form-name>.<field-name> doesn’t reference that field after all. You need to use <form-name>[‘<field-name-plus-one-space>’] because you can’t represent that trailing space without using the square bracket notation instead of the dot notation. So, I ended up adding yet more type-specific code. I don’t know if that extra trailing space is just an unfortunate mistake or it was put in by design, but if it was a mistake, I hope they don’t fix it one day, because now I am relying on it being there for things to work.

After all of those changes, though, it still wasn’t validating. Things were starting to progress from frustration to irritation. Every time that I thought that I had found and fixed the problem, I would try it out expecting things to be fixed, and it still wouldn’t be working. After a lot of head scratching and debugging alerts and trial and error, I finally was able to figure out that the validation messages only appear once the field had been $touched, and the sn-record-picker fields are never $touched. They can get $dirty, but for some reason, they are never $touched. So I modified my condition on showing the validation errors be $touched or $dirty, and finally, everything worked as it should. Finally.

So here is the way things look so far:

function() {
	var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'inlineradio', 'month', 'number', 'password', 'radio', 'reference', 'tel', 'text', 'textarea', 'time', 'url', 'week'];
	var RESERVED_ATTR = ['ngHide', 'ngModel', 'ngShow', 'class', 'field', '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 model = attrs.snhModel;
			var type = attrs.snhType || 'text';
			var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
			var fullName = form + '.' + name;
			var refExtra = '';
			type = type.toLowerCase();
			if (SUPPORTED_TYPE.indexOf(type) == -1) {
				type = 'text';
			}
			if (type == 'reference') {
				fullName = form + "['" + name + " ']";
				refExtra = '.value';
			}
			htmlText += "    <div id=\"element." + name + "\" class=\"form-group\"";
			if (attrs.ngShow) {
				htmlText += " ng-show=\"" + attrs.ngShow + "\"";
			}
			if (attrs.ngHide) {
				htmlText += " ng-hide=\"" + attrs.ngHide + "\"";
			}
			htmlText += ">\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 + refExtra + ">''?'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 == 'radio' || type == 'inlineradio') {
				htmlText += buildRadioTypes(attrs, name, model, required, type);
			} else if (type == 'choicelist') {
				htmlText += buildChoiceList(attrs, name, model, required);
			} else if (type == 'reference') {
				htmlText += "      <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
			} else if (type == 'textarea') {
				htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
			} else {
				htmlText += "      <input class=\"snh-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 + ".$dirty) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
			htmlText += "     </div>\n";
			return htmlText;

			function buildRadioTypes(attrs, name, model, required, type) {
				var htmlText = "      <div style=\"clear: both;\"></div>\n";

				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						if (type == 'radio') {
							htmlText += "      <div>\n  ";
						}
						htmlText += "        <input ng-model=\"" + model + "\" id=\"" + name + thisOption.value + "\" name=\"" + name + "\" value=\"" + thisOption.value + "\" type=\"radio\"" + passThroughAttributes(attrs) + (required?' required':'') + "/> " + thisOption.label + "\n";
						if (type == 'radio') {
							htmlText += "      </div>\n";
						}
					}
				}

				return htmlText;
			}

			function buildChoiceList(attrs, name, model, required) {
				var htmlText = "      <select class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + ">\n";
				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				htmlText += "        <option value=\"\"></option>\n";
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						htmlText += "        <option value=\"" + thisOption.value + "\">" + thisOption.label + "</option>\n";
					}
				}
				htmlText += "      </select>\n";
				return htmlText;
			}

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

			function camelToDashed(camel) {
				return camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
			}
		},
		link: function(scope, element, attrs, ctls) {
			var form = ctls[0].$name;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			if (!scope.$eval(fullName)) {
				fullName = form + '["' + name + ' "]';
			}
			var overrides = {};
			if (attrs.snhMessages) {
				overrides = scope.$eval(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/>';
					}
				}
			});
		}
	};
}

I’m still not happy about having to specify the form name, still need to do something about the wrong validation message showing up in certain circumstances, and I still haven’t tested all of the support types just yet, but I think it just might be complete enough to go ahead and throw an initial version into an Update Set for anyone who might want to play along at home. There is still a lot that I would like to do before I’m ready to call it really good, but it does work, so there is that. As the man who jumped off of the high-rise building was heard saying as he passed each floor, “So far, so good …”

Service Portal Form Fields, Part V

“There is nothing impossible to him who will try.”
Alexander the Great

After doing a lot of testing and tweaking, I decided that it was time to add a few more options to my list of supported types. Everything up to this point was still a basic text or textarea variation that didn’t require any special coding to produce the INPUT element. Now it is time to try something a little more sophisticated that will require a little bit of extra work. Both radio buttons and select statements work off of a list of choices from which you select your value, so I came up with yet another attribute to support types of this nature. I called it snh-choices, and the expectation is that you will provide a JSON array of label/value pairs in the order in which you want them to be presented. Then I added three new values to my list of supported types, choicelist, radio, and inlineradio. The only difference between radio and inlineradio is that the radio type lists the choices from top to bottom and the inlineradio type lists them from left to right. Other than that, they are essentially the same thing.

To support the new types, I created a couple of functions to render the appropriate INPUT element and the associated choices:

function buildRadioTypes(attrs, name, model, required, type) {
	var htmlText = "      <div style=\"clear: both;\"></div>\n";

	var option = null;
	try {
		option = JSON.parse(attrs.snhChoices);
	} catch(e) {
		console.log('Unable to parse snh-choices value: ' + attrs.snhChoices);
	}
	if (option && option.length > 0) {
		for (var i=0; i<option.length; i++) {
			var thisOption = option[i];
			if (type == 'radio') {
				htmlText += "      <div>\n  ";
			}
			htmlText += "        <input ng-model=\"" + model + "\" id=\"" + name + thisOption.value + "\" name=\"" + name + "\" value=\"" + thisOption.value + "\" type=\"radio\"" + passThroughAttributes(attrs) + (required?' required':'') + "/> " + thisOption.label + "\n";
			if (type == 'radio') {
				htmlText += "      </div>\n";
			}
		}
	}

	return htmlText;
}

function buildChoiceList(attrs, name, model, required) {
	var htmlText = "      <select class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + ">\n";

	var option = null;
	try {
		option = JSON.parse(attrs.snhChoices);
	} catch(e) {
		console.log('Unable to parse snh-choices value: ' + attrs.snhChoices);
	}
	htmlText += "        <option value=\"\"></option>\n";
	if (option && option.length > 0) {
		for (var i=0; i<option.length; i++) {
			var thisOption = option[i];
			htmlText += "        <option value=\"" + thisOption.value + "\">" + thisOption.label + "</option>\n";
		}
	}
	htmlText += "      </select>\n";

	return htmlText;
}

To test out these new types, I modified my little test page yet again to add some additional form fields to try out each of the three new types. The HTML for that page now looks like this:

<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"/>
        <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 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.choicelist"
          snh-name="choicelist"
          snh-label="Choice List"
          snh-type="choicelist"
          snh-required="true"
          snh-choices='[{"value":"1", "label":"Choice #1"},{"value":"2", "label":"Choice #2"},{"value":"3", "label":"Choice #3"},{"value":"4", "label":"Choice #4"}]'/>
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.radio"
          snh-name="radio"
          snh-label="Radio"
          snh-type="radio"
          snh-required="true"
          snh-choices='[{"value":"1", "label":"Choice #1"},{"value":"2", "label":"Choice #2"},{"value":"3", "label":"Choice #3"},{"value":"4", "label":"Choice #4"}]'/>
        <snh-form-field
          snh-form="form1"
          snh-model="c.data.inlineradio"
          snh-name="inlineradio"
          snh-label="In-line Radio"
          snh-type="inlineradio"
          snh-required="true"
          snh-choices='[{"value":"1", "label":"Choice #1"},{"value":"2", "label":"Choice #2"},{"value":"3", "label":"Choice #3"},{"value":"4", "label":"Choice #4"}]'/>
      </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 the screen shot of the latest version of the test page with the examples of the three new field types:

Form field tester with choice list and radio fields

Of course, that still leaves a lot to be tested, and even more that could be added, but I am starting to think that we are finally at that point where there is more work that has been completed than there is left to be done. Still, there are quite a few items that still remain on the needs to be done list:

  • Fix the way validation messages are displayed when the message changes, but the valid state does not,
  • Figure out how to obtain the name of the form in the template function so that it doesn’t have to be passed in as an attribute,
  • Test all of the field types that are specified in the list of valid field types,
  • Add even more choices to the list of valid field types, and
  • Revisit the form-field-extras DIV that we left out, and see what we might be able to with that feature if we brought that back into play.

As far as additional types go, I was thinking about seeing what I could do with the sn-record-picker, but that sounds like it could be its own challenge. That’s an AngularJS provider as well, and I’m not exactly sure how that would work with a provider inside of a provider. But I guess that’s how you find those things out. But I think I better stick with some of the other loose ends that I’ve skipped over before I start out on yet another experiment into the unknown. Although, it would be interesting to try …

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?