More Fun with Form Fields

“Be not simply good, be good for something.”
Henry David Thoreau

One of the things that I love about incrementally building parts is that you can obtain value from the version that you have right now, yet still leave open the possibility of adding even more value in the future. When I first set out to attempt to construct a universal form field tag, I had no idea of the ways in which that would grow, but I was able to make use of it as it was at every step along the way. The other day I had a need for field that was only required if another field contained a certain value. When I went to set that up using my latest iteration of the form field tag, I realized that the current version of the code does not support that. That’s not really a problem, though; that’s just another opportunity to create a better version!

We already have an snh-required attribute, but in the current version, it simply adds an HTML required attribute to the input element. It would seem simple enough to just replace that with an ng-required attribute instead, and we would be good to go. However, we also have the required indicator to think about — that grey/red asterisk in front of the field label. That needs to go away when something changes and the field is no longer required. But let’s keep things simple and just focus on one thing at a time. In the current version, we use an internal boolean variable called required to control what gets included in the template that we are building. We can continue to use that, keeping it boolean for false, and then making it a string for anything else. The code to do that looks like this:

var required = false;
if (attrs.snhRequired && attrs.snhRequired.toLowerCase() != 'false') {
	if (attrs.snhRequired.toLowerCase() == 'true') {
		required = 'true';
	} else {
		required = attrs.snhRequired;
	}
}

You may wonder at this point why we make it a boolean for false and a string for true, but hopefully that will reveal itself when we look at the rest of the code. The next thing that we can look at is a little snippet of code that repeats itself a few times throughout the script as it is used when building the input element for a number of different field types:

... + (required?' ng-required="' + required + '"':'') + ...

This is a conditional based on the required variable, which will resolve to true for any non-empty string, but false for the boolean value of false. If the value of required is not false, then we use that same variable, which we now know is a string, to complete the ng-required attribute value for the input element. This will work for values of ‘true’ just as easily as for values that contain some kind of complex conditional statement. This was the easy part, and all of the logic was resolved within the code that generates the template.

The required indicator is another story entirely. Since some condition can toggle the element from required to not required, that same condition needs to apply to the required indicator as well. If the value of the snh-required attribute is anything other than simply ‘true’ or false, we will have to incorporate that logic in the indicator element to determine whether or not to show the indicator image. That code now looks like this:

htmlText += "          <span id=\"status." + name + "\"";
if (required) {
	if (required == 'true') {
		htmlText += " ng-class=\"" + model + refExtra + ">''?'snh-required-filled':'snh-required'\"";
	} else {
		htmlText += " ng-class=\"(" + required + ")?(" + model + refExtra + ">''?'snh-required-filled':'snh-required'):''\"";
	}
}
htmlText += "></span>\n";

As before, the first conditional is based on the required variable, and if it is false, then we don’t do anything at all. But in this case, we also have to check to see if it is equal to the string ‘true’, because if it is, we can just do what we were doing before to make this work. If it is not, then we have to include the required condition in the rendered ng-class attribute to toggle the indicator off and on at the same time that the field requirement is being toggled off and on. When the field is not required, the indicator should just go away, and when it is required, it should be there, and it should be red if the field is empty and grey if it has a value.

That’s it for the code changes. To test it, I brought up my old friend the, form field testing widget, and then altered my textarea example to look like this:

<snh-form-field
  snh-model="c.data.textarea"
  snh-name="textarea"
  snh-label="Text Area"
  snh-type="textarea"
  snh-required="c.data.select==2"
  maxlength="20"
  snh-help="This is where the field-level help is displayed. This field should be required if Option #2 is selected above."/>

That should make the textarea field required when Option #2 is selected on the field that is now above:

The textarea is required when Option #2 is selected

… and it should not be required when anything else is chosen:

The textarea is not required when Option #2 is not selected

With everything looking pretty solid, I was ready to generate another Update Set and call it good. Instead, I thought maybe I would just try a few more things, just to be sure. That’s when I discovered the problem.

It’s always something. I tinkered with the last name field to make it only required if the first name value was greater than spaces, and suddenly all of the form fields that followed within that same DIV disappeared.

Missing form fields

That’s not right. Not only is it not right, I have no idea why that is happening. If you put a greater than (‘>’) sign in the value of the snh-required attribute, anything that follows in the same DIV evaporates. I tried a number of things to fix it, and I found quite a few ways to work around it, but I was never able to actually solve the problem. I hate releasing something that has this kind of a bug in it, but since I don’t seem to posses the mental capacity required to remove the flaw at this particular moment in time, that’s what I am going to end up doing. There are work-arounds, though, so I don’t feel that bad about it. Here are some of the ones that worked for me:

  • Enclose the snh-form-field tag with a DIV. Since the problem only wipes out things within the same DIV, if it is the only thing in the DIV, the problem goes away. I actually tried to do that within the template itself, but that doesn’t work; the DIV has to be outside of the tag, not part of the code that is generated by the tag.
  • Encode the greater than sign. Actually, you have to double encode it, as &gt; will not work, but &amp;gt; does the trick. Not my idea of an intuitive solution, but it does work. And again, I tried to do that within the template itself, but that does nothing at all.
  • Don’t use a greater than sign. In my own example, I could have used snh-required=”c.data.firstName” and that would have worked just as well as snh-required=”c.data.firstName>””. Also, you can call a function that contains the greater than condition, which keeps it out of the attribute value as well.

Again, these are just work-arounds. In my mind, you shouldn’t have to do that. Hopefully, in some future version, you won’t. But if you want to play around with it the way that it is, here is the latest Update Set.

Update: There is a better (enhanced) version here (… but it still doesn’t address the > issue.)

Formatted Script Search Results, Corrected

“Never give in, never give in, never, never, never, never.”
Winston Churchill

For the most part, I really liked the way my formatted search result widget came out; however, the fact that I lost my results after following the links on the page was something that I just couldn’t accept. In my rush to get that out, I released a version with that obvious flaw, but I couldn’t just let that be the end of it; there had to be a way to make it remember its way back home without losing track of what was there before. There just had to be.

I tried a lot of things. They say that when you lose something, you always find it in the last place that you look. I always wondered why that was considered such profound wisdom, since it seemed quite obvious to me that once you found it, you would stop looking. Of course you always find it in the last place that you looked. When I finally came across a way to get the results that I wanted, I stopped looking. There may be a better way to achieve the same ends, but once I was able to link out to a page and come back to my original results, I was done. I’m not all that proud of the code, but it works, so I’m good.

The problem turned out to be this statement, which takes you to the next URL, which includes the search string parameter:

$location.search('search', c.data.searchFor);

This uses the AngularJS $location service, which is quite powerful, but apparently not quite powerful enough to leave a trail that you could follow back home again. I tried a number of variations to get things to work, but in the end I abandoned this whole approach and just went full old school. I replaced my ng-click with an onclick, and in my onclick function, I replaced that nice, simple one line of code with several others:

function findIt() {
	var url = window.location.pathname + window.location.search;
	if (url.indexOf('&search=') != -1) {
		url = url.substring(0, url.indexOf('&search='));
	}
	url += '&search=' + document.getElementById('searchFor').value;
	window.location.href = url;
}

Now I will be the first one to admit that this is quite a bit more code than that one simple line that I had before. First, I had to glue together the path and the search string to construct a relative URL, then I had to check to see if a search parameter was already present, and if so, clip it off of the end, then add my new search parameter to the search string, and then finally, set the current location to the newly constructed URL. Aesthetically, I prefer the original much, much better, but this older, brute force method has the advantage of actually working the way that I want, so it gets to be the winner.

I still kept my ng-click, but that was just to toggle on a new loading DIV to let the user know that their input was accepted and now we are working on getting them their results. That simple HTML addition turned out like this:

<div class="row" ng-show="c.data.loading">
  <div class="col-sm-12">
    <h4>
      <i class="fa fa-spinner fa-spin"></i>
      ${Wait for it ...}
    </h4>
  </div>
</div>

One other thing that I tinkered with in this iteration was the encoded query string in the Script Include. There is a particular table (sn_templated_snip_note_template) that kept throwing an error message related to security, so I decided to just filter that one out by name to keep that from happening. The new encoded query string now looks like this:

internal_typeCONTAINSscript^active=true^name!=sn_templated_snip_note_template

There might be a few other clean-up odds and ends included that I can’t quite recall right at the moment, but the major change was to finally get it to come back home again after drilling down into one the listed scripts. If you installed the previous version of this Update Set, I would definitely recommend that you replace it with this one.

Formatted Script Search Results

“Everything should be made as simple as possible, but not simpler.”
Albert Einstein

So, I came up with a way to search all of the places in which a chunk of code might be hiding, but to get the results, I have to run it as a background script and parse through the resulting JSON object. I need something a little more user-friendly than that, so I am going to build a Service Portal widget that takes a user entered search string, makes the call to the search script, and then formats the results a little nicer. Then I am going to add an item to the Tools menu that I created for my sn-record-picker tool that will bring up this new widget. This way, all I will have to do is click on the menu, enter my search terms, and hit the button to see the results.

The first thing that we will need to do is to lay out the page. Nothing exciting here: just an snh-form-field text element for capturing the input, a submit button, and a place to display the results, all wrapped up in an snh-panel. It’s a relatively small snippet of HTML, so I can reproduce the entire thing here:

<snh-panel class="panel panel-primary" rect="rect" title="'${Script Search}'">
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.searchFor"
          snh-name="searchFor"
          snh-label="What are you searching for?"
          snh-help="Enter the text that you would like to find in a script somewhere in the system"
          snh-required="true"
          snh-messages='{"required":"Please enter what you would like to find"}'/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12" style="text-align: center;">
        <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-click="findIt()">${Search all scripts}</button>
      </div>
    </div>
  </form>
  <div class="row" ng-show="c.data.result">
    <div class="col-sm-12" ng-show="c.data.result.length==0">
      <p>${No scripts were found to contain the text} <b>{{c.data.searchFor}}</b></p>
    </div>
    <div class="col-sm-12" ng-show="c.data.result.length>0">
      <table class="table table-hover table-condensed">
        <thead>
          <tr>
            <th style="text-align: center;">${Artifact}</th>
            <th style="text-align: center;">${Table Name}</th>
            <th style="text-align: center;">${Table}</th>
          </tr>
        </thead>
        <tbody>
          <tr ng-repeat="item in c.data.result">
            <td data-th="Record"><a href="{{item.table}}.do?sys_id={{item.sys_id}}" title="Open {{item.name}}">{{item.name}}</a></td>
            <td data-th="Table Name">{{item.tableName}}</td>
            <td data-th="Table">{{item.table}}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</snh-panel>

There is an ng-click on the submit button that will call a function on the client-side script, so now is as good a time as any to build that out. Again, there really isn’t all that much code to see here:

function ScriptSearch($scope, $location) {
	var c = this;

	$scope.findIt = function() {
		$location.search('search', c.data.searchFor);
	};
}

You may notice that we don’t actually search for the script at this point; we just branch to a new location. This is a little trick that I stole from my Configurable Data Table Widget Content Selector, which also just takes user input and uses it to build a new URL, and then takes you to that URL where the search actually takes place. The reason for that is so that all of the user input is a part of the page URL, which means that if you drill down and follow any links on the page, when you come back, all of your original search criteria is still intact and the page comes back just the way that you left it. This works really slick in the Service Portal; it’s just really too bad that I didn’t have the same experience when I launched this widget inside of the main UI. But, we’ll deal with that later.

Right now, it’s time to tackle server-side script, and once again, you aren’t going to see much here in the way of actual lines of code:

(function() {
	data.searchFor = '';
	data.result = false;
	if ($sp.getParameter('search')) {
		data.searchFor = $sp.getParameter('search');
		data.result = new ScriptUtils().findInScript(data.searchFor);
		gs.setReturnURL('/$sp.do?id=script_search&search=' + data.searchFor);
	}
})();

Basically, we start out by setting the default values for our two data properties, and then if there is a search parameter on the URL, we override those values with the search term and the results of running that search term through our script searcher. That last line was my attempt to preserve the URL so that when I clicked on an item to go look at it, my results would still be displayed when I returned. Unfortunately, that didn’t work, either. One day, I will figure out how to fix that, but for now, each time I leave the page, I have to search all over again to get my results back. Not the way that I want it to work, but at this point, I can live with it.

That’s about it for all of the various parts. Pretty simple stuff, really. Here’s what it looks like in action:

Script searcher in action

All in all, I like the way that it turned out, although it really annoys my sense of The Way Things Ought To Be when I click on one of those items and then come back and find that my search results are all gone. That’s just not right. One day I am going to fix that, but until that day comes, here is an Update Set for anyone who wants to play along at home, and maybe even take care of that little annoyance for me!

I know it’s in here somewhere …

“Three can keep a secret, if two of them are dead.”
Benjamin Franklin

Every once in a while I am chasing some issue or tying to remember how I did something and I know the answer is in the system somewhere, but I’m just not sure where. Since I mainly deal with Javascript code, what I would really like is a way to search all of the places in which script might be stored looking for some term or phrase. And I mean all of the places, including places that might come up later in future versions. What I really want to do is dynamically look at all of the tables and find all of the columns that might store script and then search all of those columns in all of those tables. Fortunately, because of the way in which the Now Platform is constructed, you can easily do exactly that.

The sys_dictionary table holds all of the information on all of the columns in all of the tables, including the column type. There are several ServiceNow column types that might contain script, but fortunately for us, they all contain the word script in their names. That makes it relatively easy to search the dictionary for all of the script columns in all of the tables:

var table = {};
var columnGR = new GlideRecord('sys_dictionary');
columnGR.addEncodedQuery('internal_typeCONTAINSscript^active=true');
columnGR.query();
while (columnGR.next()) {
	var tableName = columnGR.getDisplayValue('name');
	var fieldName = columnGR.getDisplayValue('element');
	if (tableName && fieldName && !tableName.startsWith('var__m_')) {
		if (!table[tableName]) {
			table[tableName] = [];
		}
		table[tableName].push(fieldName);
	}
}

This script builds a map keyed by table name that contains an array of script columns for each table. Before I add a column to the map, I make sure that there is both a field name and a table name, and I also filter out all of the variable mapping tables, as those don’t contain any scripts and some of them actually cause problems when I attempt to use them. Once we have established our target map, it is simply a matter of stepping through it and querying each table for the presence of your search argument in any of those columns:

var found = [];
for (tableName in table) {
	var query = '';
	var separator = '';
	for (var i=0; i<table[tableName].length; i++) {
		query += separator;
		query += table[tableName][i];
		query += 'CONTAINS';
		query += string;
		separator = 'OR';
	}
	var scriptGR = new GlideRecord(tableName);
	scriptGR.addEncodedQuery(query);
	scriptGR.query();
	while (scriptGR.next()) {
		found.push({table: tableName,  tableName: scriptGR.getLabel(), sys_id: scriptGR.getUniqueValue(), name: scriptGR.getDisplayValue('name') || scriptGR.getDisplayValue() || scriptGR.getUniqueValue()});
	}
}
return found;

This portion of the script builds an array of objects containing the table, table name, name, and sys_id of any records found during the search. I bundled the whole thing into a Script Include that I called ScriptUtils, and you can see the entire thing here:

var ScriptUtils = Class.create();
ScriptUtils.prototype = {
	initialize: function() {
	},

	findInScript: function(string) {
		var table = {};
		var found = [];
		var columnGR = new GlideRecord('sys_dictionary');
		columnGR.addEncodedQuery('internal_typeCONTAINSscript^active=true');
		columnGR.query();
		while (columnGR.next()) {
			var tableName = columnGR.getDisplayValue('name');
			var fieldName = columnGR.getDisplayValue('element');
			if (tableName && fieldName && !tableName.startsWith('var__m_')) {
				if (!table[tableName]) {
					table[tableName] = [];
				}
				table[tableName].push(fieldName);
			}
		}
		for (tableName in table) {
			var query = '';
			var separator = '';
			for (var i=0; i<table[tableName].length; i++) {
				query += separator;
				query += table[tableName][i];
				query += 'CONTAINS';
				query += string;
				separator = 'OR';
			}
			var scriptGR = new GlideRecord(tableName);
			scriptGR.addEncodedQuery(query);
			scriptGR.query();
			while (scriptGR.next()) {
				found.push({table: tableName,  tableName: scriptGR.getLabel(), sys_id: scriptGR.getUniqueValue(), name: scriptGR.getDisplayValue('name') || scriptGR.getDisplayValue() || scriptGR.getUniqueValue()});
			}
		}
		return found;
	},

    type: 'ScriptUtils'
};

To test is out, we can can select Scripts – Background from the sidebar menu and enter something like this:

gs.info(JSON.stringify(new ScriptUtils().findInScript('xxx'), null, 4));

After running the above script, I received the following output:

[
    {
        "table": "discovery_proc_handler",
        "tableName": "Process Handler",
        "sys_id": "1f2473269733200010cb1bd74b297576",
        "name": "Java parameters"
    },
    {
        "table": "sys_script",
        "tableName": "Business Rule",
        "sys_id": "4532f571bf320100710071a7bf073929",
        "name": "Obfuscate password"
    },
    {
        "table": "sys_script_execution_history",
        "tableName": "Script Execution History",
        "sys_id": "807208e92ffb48d0ddadfe7cf699b696",
        "name": "Created 2020-03-26 05:55:27"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "326b53699f3010008f88ed93ee4bcc2b",
        "name": "ScrumSecurityManagerDefault"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "3f7d6f17537103003248cfa018dc347c",
        "name": "PwdResetPageInfo"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "57042a36932012001aa1f4b8b67ffb95",
        "name": "TourBuilderUtility"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "88c548dc37010100dcd48c00dfbe5d2e",
        "name": "SnmpIdentityInfoParser"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "8dbee16b530203003248cfa018dc349e",
        "name": "PwdResetPageInfo_V2"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "91a92c70733023008b516cb63cf6a79e",
        "name": "CommunityCacheUtilSNCJSC"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "a7ac57b7c710320003fa9c569b976312",
        "name": "MIDUserConnectivity"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "c54c989f37612100dcd48c00dfbe5df4",
        "name": "CiSchema"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "ee735ba66713220089ec9a6617415a75",
        "name": "CommunityForumImpl"
    },
    {
        "table": "sys_script_include",
        "tableName": "Script Include",
        "sys_id": "eee51271eb223100c46ac2eef106fed4",
        "name": "AddScriptedRESTVersionAjax"
    }
]

Now, there are obviously better ways to format and display that information, but that would be an entire project on its own, so we’ll just save that exercise for another time.

Yet Even More Service Portal Form Fields

“The wise person feels the pain of one arrow. The unwise feels the pain of two.”
— Kate Carne, Seven Secrets Of Mindfulness

While working on my sn-record-picker wizard, I discovered that I never set up checkbox as a field type in my snh-form-field directive, so I had to go back and add that in. I also ended up making a few other improvements, both before and after I released the code, so it’s time to post a new version. The checkbox issue also inspired me to include a couple of other field types, so I’ve tossed those in as well. The two additional field types that I added were modeled after the radio and inlineradio types, but by replacing the radio buttons with checkboxes, the user has the option of selecting more than one of the available choices. I call these new types multibox and inlinemultibox.

Example multibox field type

I started out by just copying the function that built the radio types and then giving it a new name. Then I went up to the section where the function was called and added another conditional to invoke this new function based on the value of the snh-type attribute.

if (type == 'multibox' || type == 'inlinemultibox') {
	htmlText += buildMultiboxTypes(attrs, name, model, required, type, option);
} else if (type == 'radio' || type == 'inlineradio') {
	htmlText += buildRadioTypes(attrs, name, model, required, type, option);
} else if (type == 'select') {
...

I also had to add the new types to the list of supported types, which now looks like this:

var SUPPORTED_TYPE = ['checkbox', 'choicelist', 'date', 'datetime-local',
 'email', 'feedback', 'inlinemultibox', 'inlineradio', 'mention', 'month',
 'multibox', 'number', 'password', 'radio', 'rating', 'reference', 'select',
 'tel', 'text', 'textarea', 'time', 'url', 'week'];

Once that was all taken care of, I set out to tackle hacking up the copied radio types function to make it work for checkboxes. Since the whole point of the exercise was to allow you to select more than one item, I couldn’t use the same ng-model for every option like I did with the radio buttons. Each option had to be bound to a unique variable, and then I still needed a place to store all of the options selected. I decided to take a page out the sn-record-picker playbook and create a single object to which you could bind to all of this information. It ended up looking very similar the one used for the sn-record-picker.

$scope.multiboxField = {
    value: ''
    option: {}
    name: 'examplemultibox'
};

The value string will be in the same format as an sn-record-picker when you add the attribute multiple=”true”: a comma-separated list of selected values. To make that work in this context, I had to add an additional hidden field bound to the value property, and then bind the individual checkboxes to a property of the option object using the value of the option as the property name. To keep the value element up to date, I invoke a new function to recalculate the value every time one of the checkboxes is altered.

scope.setMultiboxValue = function(model, elem) {
	if (!model.option) {
		model.option = {};
	}
	var selected = [];
	for (var key in model.option) {
		if (model.option[key]) {
			selected.push(key);
		}
	}
	model.value = selected.join(',');
	elem.snhTouched = true;
};

More on that last line, later. For now, let’s get back to the function that builds the template for the multibox types. As I said earlier, I started out by just copying the function that built the radio types and then giving it a new name (buildMultiboxTypes). Now I had to hack it up to support checkboxes instead of radio buttons. The finished version came out like this:

function buildMultiboxTypes(attrs, name, model, required, type, option, fullName) {
	var htmlText = "      <div style=\"clear: both;\"></div>\n";
	htmlText += "      <input ng-model=\"" + model + ".value\" id=\"" + name + "\" name=\"" + name + "\" type=\"text\" ng-show=\"false\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
	for (var i=0; i<option.length; i++) {
		var thisOption = option[i];
		if (type == 'multibox') {
			htmlText += "      <div>\n  ";
		}
		htmlText += "        <input ng-model=\"" + model + ".option['" + thisOption.value + "']\" id=\"" + name + thisOption.value + "\" name=\"" + name + thisOption.value + "\" type=\"checkbox\" ng-change=\"setMultiboxValue(" + model + ", " + fullName + ")\"/> " + thisOption.label + "\n";
		if (type == 'multibox') {
			htmlText += "      </div>\n";
		}
	}

	return htmlText;
}

Other than the hidden field for the value and the call to update the value whenever any of the boxes are checked, it turned out to be quite similar to its radio-type cousin. When I first put it all together, it all seemed to work, but I couldn’t get my error messages to display under the options. As it turns out, the error messages only display if the form has been submitted or the element in question has been $touched. Since the value element is hidden from view and only updated via script, it gets changed, but never $touched. I tried many, many ways to set that value, but I just couldn’t do it. But I won’t be denied; I ended up creating my own attribute (snhTouched), and set it to true whenever any of the boxes were altered. Then, to drive the appearance of the error messages off of that attribute, I had to add a little magic to this standard line of the template:

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

I think that’s about it for the changes related to the new multibox types. One other thing that I ended up doing, which I have been hesitant to do, is bring back the snh-form attribute. I never liked having to specify the form, and I thought that I had found a way around that, but it seems that even that method does not work 100% of the time. In fact, it can crash the whole thing with a null pointer exception in certain circumstances. So, I finally broke down and brought back the snf-form attribute as an optional fallback if all else fails. The new logic will take the form name if you provide it, and if not, it will attempt to find the form name on its own, and if that fails for whatever reason, then it will default to ‘form1’.

var form = attrs.snhForm;
if (!form) {
	try {
		form = element.parents("form").toArray()[0].name;
	} catch(e) {
		//
	}
}
if (!form) {
	form = 'form1';
}

There were a few other little minor tweaks and improvement, but nothing really worthy of mentioning here. One thing that I thought about, but did not do, was to build in any kind of support for a minimum number of checkboxes checked. You can make it required, which means that you have select at least one item, but there isn’t any way right now to say that you must select at least two or you can’t check more than four. I may try to tackle that one day, but I’m going to let that go for now. Here is an Update Set for anyone who wants to play around on their own.

sn-record-picker Helper, Part II

“If I had eight hours to chop down a tree, I’d spend six hours sharpening my ax.”
Abraham Lincoln

Now that we have all of the form fields and controls laid out on the page, the next order of business is to build the function that will use the form input to create the desired sn-record-picker. We have already specified an ng-submit function on the form, so all we have to do at this point is to actually create that function in the client-side script. This function is actually pretty vanilla stuff, and just takes the data entered on the screen and stitches it all together to form the resulting sn-record-picker tag.

$scope.createPicker = function() {
	var picker = '<sn-record-picker\n    table="' + "'";
	picker += c.data.table.value + "'";
	picker += '"\n    field="';
	picker += c.data.field;
	if (c.data.filter) {
		picker += '"\n    default-query="' + "'";
		picker += c.data.filter + "'";
	}
	picker += '"\n    display-field="' + "'";
	picker += c.data.displayField.value + "'";
	if (c.data.displayFields.value) {
		picker += '"\n    display-fields="' + "'";
		picker += c.data.displayFields.value + "'";
	}
	if (c.data.searchFields.value) {
		picker += '"\n    search-fields="' + "'";
		picker += c.data.searchFields.value + "'";
	}
	if (c.data.valueField.value) {
		picker += '"\n    value-field="' + "'";
		picker += c.data.valueField.value + "'";
	}
	if (c.data.multiple) {
		picker += '"\n    multiple="true"';
	}
	if (c.data.placeholder) {
		picker += '"\n    page-size="';
		picker += c.data.pageSize;
	}
	if (c.data.pageSize) {
		picker += '"\n    placeholder="';
		picker += c.data.placeholder;
	}
	c.data.generated = picker + '">\n</sn-record-picker>';
	c.data.ready = true;
	return false;
};

One of the last few things that happens in that script is to set c.data.ready to true. I set that variable up to control whether or not the rendered tag should appear on the screen. Altering any of the fields on the screen sets it to false, which hides the text box containing the generated code until you click on the button again. To make that work, I just added an ng-show to the enclosing DIV:

<div class="col-sm-12" ng-show="c.data.ready">
  <snh-form-field
    snh-model="c.data.generated"
    snh-name="generated"
    snh-label="Your sn-record-picker:"
    snh-type="textarea">
  </snh-form-field>
  <p><a href="javascript:void(0);" onclick="copyToClipboard();">Copy to clipboard</a></p>
</div>

The other thing that you will notice inside of that DIV is the Copy to clipboard link. That one uses a standard onclick rather than an ng-click, because that’s a DOM operation, which is outside the scope of the AngularJS controller. The referenced script, along with another DOM script that I use to set the height of that textarea, are placed in a script tag with the HTML.

<script>
function fixTextareaHeight() {
	var elem = document.getElementById('generated');
    elem.style.height = (elem.scrollHeight + 10)  + 'px';
}
function copyToClipboard() {
	var elem = document.getElementById('generated');
	elem.select();
	elem.setSelectionRange(0, 99999)
	document.execCommand('copy');
	alert("The following code was copied to the clipboard:\n\n" + elem.value);
}
</script>

Clicking on that Copy to clipboard link copies the code to the clipboard and also throws up an alert message to let you know what was copied.

Alert message after copying the code to the clipboard

The next thing on my list was to actually place the code on the page so that you could see it working. I tried a number of things to get that to work, including ng-bind, ng-bind-html, ng-bind-html-compile, and sc-bind-html-compile, but I could never get any of that to work, so I ultimately gave up on trying to use the actual generated code, and did the next best thing, which was to just set up a picker of my own using the selected options.

        <div class="col-sm-12" ng-show="c.data.ready">
          <snh-form-field
            snh-model="c.data.liveExample"
            snh-name="liveExample"
            snh-label="Live Example"
            snh-type="reference"
            snh-change="optionSelected()"
            placeholder="{{c.data.placeholder}}"
            table="c.data.table.value"
            display-field="c.data.displayField.value"
            display-fields="c.data.displayFields.value"
            value-field="c.data.valueField.value"
            search-fields="c.data.searchFields.value"
            default-query="c.data.filter">
          </snh-form-field>
        </div>

This approach allowed me to add a modal pop-up that showed the value of the item selected.

Modal pop-up indicating option selected

The code to make that happen is in the client-side controller:

$scope.optionSelected = function() {
	spModal.open({
		title: 'Selected Option',
		message: '<p>You selected  "<b>' + c.data.liveExample.value + '</b>"</p>',
		buttons: [
			{label: 'Close', primary: true}
		],
		size: 'sm'
	});
};

One other thing that I should mention is that the snh-form-field directive that I used in this example is not same as the last version that I had published. To support the multiple=true/false option, I needed a checkbox, and for some reason, I never included that in the list of many, many field types that included in that directive. I also had to tweak a few other things here and there, so it’s no longer the same. I should really release that separately in a future installment, but for now, I will just bundle it with everything else so that this will all work as intended.

I wrapped the whole thing in an snh-panel, just to provide the means to add some documentation on the sn-record-picker tag. I had visions of gathering up all of the documentation that I could find and assembling it all into a comprehensive help screen document, but that never happened. Still, the possibility is there with the integrated help infrastructure.

Pop-up widget help screen

I also added a sidebar menu item so that I could easily get to it within the main UI. It may be a Service Portal Widget under the hood, but it doesn’t really belong on the Service Portal. It’s a development tool, so I added the menu item so that it could live with all of the other development tools in the primary UI. If you want to take it for a spin yourself, here is an Update Set.

Update: There is a better (corrected) version here.

sn-record-picker Helper

“You are never too old to set another goal or to dream a new dream.”
Les Brown

One of the cool little doodads packaged with ServiceNow is the sn-record-picker. On the Service Portal side of the house, the sn-record-picker gives you a type-ahead search of any ServiceNow table with a considerable number of flexible features. I use it quite a lot, but not often enough to intuitively recall every configuration option available. Although this is a widely used facet of the Service Portal platform, the documentation for this component is relatively sparse, which is uncharacteristic for ServiceNow. A number of individuals have attempted to provide their own version of the needed documentation, and I have even considered doing that myself, but that only solves half of my problem. The other thing that I can never remember is the names of tables and fields, which you need to know whenever you set up an sn-record-picker. What I would really like is some kind of on-line wizard that stepped me through all of the necessary parts and pieces needed to build the sn-record-picker that I need at the time, and it would be even better if had the ability to go ahead and build it so that I could see it live once I completed all of the steps needed to construct it. I looked around for such a tool and couldn’t find one, so I decided to build it myself.

Here’s the idea: create a Service Portal widget that has input fields for all of the configuration options along with some kind of Build It button that would both create the sn-record-picker code based on the user input, and put the code live on the page so that you could see it in action. It seemed like it would be quite useful when it was finished, and fairly simple to put together. After all, how hard could it be?

The first thing that you need for an sn-record-picker is a Table. Since we are building a widget, the easiest way to select a Table from a list would be to use an sn-record-picker. So, it would seem appropriate that the first field on our new sn-record-picker tool would, in fact, be an sn-record-picker. Technically, I will be using an snh-form-field in practice, but under the hood, there is still an sn-record-picker doing all of the heavy lifting.

<snh-panel title="'${sn-record-picker Tool}'" class="panel-primary">
  <form id="form1" name="form1" ng-submit="createPicker();" novalidate>
    <div class="row">
       <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.table"
          snh-name="table"
          snh-type="reference"
          snh-help="Select the ServiceNow database table that will contain the options from which the user will select their choice or choices."
          snh-change="buildFieldFilter();"
          snh-required="true"
          placeholder="Choose a Table"
          table="'sys_db_object'"
          display-field="'label'"
          display-fields="'name'"
          value-field="'name'"
          search-fields="'name,label'">
        </snh-form-field>
      </div>
    </div>
  </form>
</snh-panel>

I had to look up the name of the ServiceNow table of tables, because I couldn’t remember what it was (sys_db_object), but that may just be because I never knew what it was in the first place. I also had to look up the column names for all of the fields that I needed. I should be able to avoid all of that effort once all of this comes together and I can use my new tool, which of course, is whole point of this exercise. Configuring the table selector is enough to get things started, though, and I don’t like to do too much without giving things a whirl, so let’s throw this widget onto a portal page and hit the Try It! button.

sn-record-picker tool with table selector

Once you have the table selected, you can start selecting from the fields defined for that table. Once again, this is an excellent use for an sn-record-picker, but for the table fields we will need to filter the choices based on the table selected. To do that, we need to build an encoded query for a filter. On the client-side script, we can create a function to do just that:

$scope.buildFieldFilter = function() {
	c.data.fieldFilter = 'elementISNOTEMPTY^name=' + c.data.table.value;
};

Now that we have our filter defined, we can reference it in the next picker that we will need, the Primary Display field:

<snh-form-field
  snh-model="c.data.displayField"
  snh-name="displayField"
  snh-label="Primary Display Field"
  snh-type="reference"
  snh-help="Select the primary display field."
  snh-required="true"
  snh-change="c.data.ready=false"
  placeholder="Choose a Field"
  table="'sys_dictionary'"
  display-field="'column_label'"
  display-fields="'element'"
  value-field="'element'"
  search-fields="'column_label,element'"
  default-query="c.data.fieldFilter">
</snh-form-field>

The default-query attribute of the Display Field sn-record-picker is set to c.data.fieldFilter, which is the variable that contains the value that is recalculated every time a new selection is made on the Table sn-record-picker. Whenever you select a different table, the filter is updated and then the list of available options for the Display Field selector changes to just those fields found on the selected table. This technique will be utilized for the Primary Display Field, the Additional Display Fields, the Search Field, and the Value Field.

In addition to the basic table and field attributes, there are also a number of other attributes that need to be included in the tool as well. I’m not even sure that I know all of the possible attributes that might be available, but my thought is that I will add all of the ones that I know about and then toss the others in when I learn about them. It turns out that there a quite a few, though, and after putting them all in with their associated help information, it made my page long and narrow, and put the button and results way, way down at the bottom of the page. I didn’t really like that, so I decided to split the page into two columns, and to hide any optional parameters unless needed. That format turned out to be much better that what I had originally; I like it much better.

Picker tool split into two columns

To temporarily hide the optional fields, I added an anchor tag with an ng-click above the form field, and gave both the anchor tag and the form field ng-show attributes based on the same boolean variable so that either one or the other would appear on the page.

<div class="col-sm-12" ng-show="c.data.table.value > ''">
  <p><a href="javascript:void(0)" ng-click="c.data.showFilter = true;" ng-show="!c.data.showFilter">Add an optional query filter</a></p>
  <snh-form-field
    ng-show="c.data.showFilter"
    snh-model="c.data.filter"
    snh-name="filter"
    snh-help="To limit the records retrieved from the table, enter an optional Encoded Query"
    placeholder="Enter a valid Encoded Query"
    snh-change="c.data.ready=false">
  </snh-form-field>
</div>

Now that I have configured all of the form fields for all of the attributes, the next thing to do will be to build the code that turns the user’s input into an actual sn-record-picker, and then makes it available for copy/paste operations, and hopefully, to try out right there on the wizard form. That’s actually quite a bit, so I think I will save all of that for a future installment.

User Rating Scorecard, Part IV

“You can’t have everything. Where would you put it?”
Steven Wright

Well, I had a few grand ideas for my User Rating Scorecard, but not all ideas are created equal. Some are quite a bit easier to turn into reality than others. Not that any of them were bad ideas — some just turned out to be a little more trouble than they were worth when I sat down and tried to turn the idea into code. I had visions of using Retina Icons and custom images for the rating symbol, but the code that I stole for handling the fractional rating values relied on the content property of the ::before pseudo-element. The value of the content property can only be text; icons, images, or any other HTML is not valid in that context and won’t get resolved. Basically, what I had in mind just wasn’t going to work.

That left me two choices: 1) redesign the HTML and CSS for the graphic to use something other than the content property, or 2) live with the restrictions and try to do what I wanted using unicode characters. I played around with choice #1 for quite a while, but I could never really come up with anything that gave me all of the flexibility that I wanted and still functioned correctly. So, I finally decided to see what was behind door #2. There are a lot of unicode characters. In fact, there are considerably more of those than there are Retina Icons, but not being able to use an image of your own choosing was still quite a bit more limiting than what I was imagining. Sill, it was better than just hard-coded starts, so I started hacking up the code to see if I could make it all work.

In my original version, the 5 stars were just an integral part of the rating CSS file:

.snh-rating::before {
    content: '★★★★★';
    background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

To make that more flexible, I just need to replace the hard-coded stars with a CSS variable:

.snh-rating::before {
    content: var(--char-content);
    background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

This would allow me to accept a new attribute as the unicode character to use and then set the value of that CSS variable to a string of those characters. My original Angular Provider had the template defined as a string, but to accommodate all of my various options, I had to convert that to a function. The first order of business in the function was to initialize all of the default values.

var max = 5;
var symbol = 'star';
var plural = 'stars';
var charCode = '★';
var charColor = '#FFCC00';
var subject = 'item';
var color = ['#F44336','#FF9800','#00BCD4','#2196F3','#4CAF50'];

Without overriding any of these defaults, the new version would produce the same results as the old version. But the whole point of this version was to provide the capability to override these values, so that was the code that had to be added next:

if (attrs.snhMax && parseInt(attrs.snhMax) > 0) {
	max = parseInt(attrs.snhMax);
}
if (attrs.snhSymbol) {
	symbol = attrs.snhSymbol;
	if (attrs.snhPlural) {
		plural = attrs.snhPlural;
	} else {
		plural = symbol + 's';
	}
}
if (attrs.snhChar) {
	charCode = attrs.snhChar;
}
var content = '';
if (attrs.snhChars) {
	content = attrs.snhChars;
} else {
	for (var i=0; i<max; i++) {
		content += charCode;
	}
}
if (attrs.snhCharColor) {
	charColor = attrs.snhCharColor;
}
if (attrs.snhSubject) {
	subject = attrs.snhSubject;
}
if (attrs.snhBarColors) {
	color = JSON.parse(attrs.snhBarColors);
}

The above code is pretty straightforward; if there are attributes present that override the defaults, then the default value is overwritten by the value of the attribute. Once that work has been done, all that is left is to build the template based on the variable values.

var htmlText = '';
htmlText += '<div>\n';
htmlText += '  <div ng-hide="votes > 0">\n';
htmlText += '    This item has not yet been rated.\n';
htmlText += '  </div>\n';
htmlText += '  <div ng-show="votes > 0">\n';
htmlText += '    <div class="snh-rating" style="--rating: {{average}}; --char-content: \'' + content + '\'; --char-color: ' + charColor + ';">';
htmlText += '</div>\n';
htmlText += '    <div style="clear: both;"></div>\n';
htmlText += '    {{average}} average based on {{votes}} reviews.\n';
htmlText += '    <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 1;" ng-hide="c.data.show_breakdown == 1">Show breakdown</a>\n';
htmlText += '    <div ng-show="c.data.show_breakdown == 1" style="background-color: #ffffff; max-width: 500px; padding: 15px;">\n';
for (var x=max; x>0; x--) {
	var i = x - 1;
	htmlText += '      <div class="snh-rating-row">\n';
	htmlText += '        <div class="snh-rating-side">\n';
	htmlText += '          <div>' + x + ' ' + (x>1?plural:symbol) + '</div>\n';
	htmlText += '        </div>\n';
	htmlText += '        <div class="snh-rating-middle">\n';
	htmlText += '          <div class="snh-rating-bar-container">\n';
	htmlText += '            <div style="--bar-length: {{bar[' + i + ']}};--bar-color: ' + color[i] + ';" class="snh-rating-bar"></div>\n';
	htmlText += '          </div>\n';
	htmlText += '        </div>\n';
	htmlText += '        <div class="snh-rating-side snh-rating-right">\n';
	htmlText += '          <div>{{values[' + i + ']}}</div>\n';
	htmlText += '        </div>\n';
	htmlText += '      </div>\n';
}
htmlText += '      <div style="text-align: center;">\n';
htmlText += '        <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 0;">Hide breakdown</a>\n';
htmlText += '      </div>\n';
htmlText += '    </div>\n';
htmlText += '  </div>\n';
htmlText += '</div>\n';

Now, all we have to do is try it out. Let’s configure a few of thee options to override the defaults and then see how it all comes out. Here is one sample configuration that uses 7 hearts instead of the default 5 stars:

  <snh-rating
     snh-max="7"
     snh-char="♥"
     snh-symbol="heart"
     snh-char-color="#FF0000"
     snh-values="'2,6,11,13,4,77,36'"
     snh-bar-colors='["#FFD3D3","#F4C2C2","#FF6961","#FF5C5C","#FF1C00","#FF0800","#FF0000"]'>
  </snh-rating>

… and here’s how it looks once everything is rendered:

Rating widget output with default values overridden

So, it’s not every single thing that I had imagined, but it is much more flexible than the original. Like most things, it could still be even better, but for now, I’m ready to call it good enough. If you want to play around with it on your own, here is an Update Set.

User Rating Scorecard, Part III

“All things are created twice. There’s a mental or first creation, and a physical or second creation of all things. You have to make sure that the blueprint, the first creation, is really what you want, that you’ve thought everything through. Then you put it into bricks and mortar. Each day you go to the construction shed and pull out the blueprint to get marching orders for the day. You begin with the end in mind.”
Stephen Covey

I like the way that my 5 star rating scorecard widget came out, but I still want to be able to do more than just stars, and be more flexible than having just a 1 to 5 rating system. Ultimately, it would be nice to be able to customize things a bit more with different icons or images, be able to pick your own colors, and even have a different image for each rating value. As always, I would want to have a default value for just about everything, so your wouldn’t have to specify most of that if you didn’t need or want to, but it would be nice to have the option. I’d like to be able to support the following customization attributes:

  • snh-max – this would be the upper limit on rating value, and I would make it totally optional, with a default value of 5, which seems to be pretty universal for most use cases.
  • snh-icon – this would be the name of one of the stock Retina Icons, which again, would be optional and default to star if you didn’t override it.
  • snh-image – this would be a link to an uploaded image file, which would serve the same purpose as the snh-icon, but provide the ability have a custom image not found in the stock icon set. Obviously, you could only have one or the other, so there would have to be some hierarchy established in the event that someone specified both an icon and an image.
  • snh-image-array – this would be an array of links much like the snh-image attribute, but instead of a single image, it would provide the capability to have a different image for each rating value. Again, there would have to be some kind of hierarchy established to avoid a conflict between an array of images, a single image, or a single icon.
  • snh-name – this would be the text equivalent of the icon or image, and would default to Star, to match the default icon. This name would be used in some of the labels, and would allow you to replace labels such as 1 Star with 1 Bell or 1 Thumbs Up.
  • snh-plural – this would just be the plural version of snh-name, and really only necessary if adding an “s” to the end of the word didn’t come out right. In the examples above, 2 Stars or 2 Bells would be OK, but 2 Thumbs Ups wouldn’t be right, so you would want to add snh-plural=”Thumbs Up” to fix that.
  • snh-icon-color – this would give you the ability to choose the color of the selected icon, and again, I would make this optional and have a default color selected so you wouldn’t have to specify this unless you wanted something different.
  • snh-bar-colors – this would be an array of HTML colors codes that would give you the ability to choose the colors in the vote breakdown bar chart. Like most other items on my wish list, this would be optional and there would be default values that would be used in the event that you elected not to customize this particular parameter.

These are all of the things that I can think of at the moment, but I am sure that others will come up over time. Of course it’s one thing to have all of these ideas in your head and quite another to actually write the code to make it happen. At this point, these are just ideas. If I want to actually see it in action, I will have to actually do the work!

Generic Feedback Widget, Part X

When I added the User Rating Scorecard to my Generic Feedback Widget, I only wanted to show it when appropriate, so I added an ng-show attribute to the snh-rating element:

<snh-rating ng-show="data.includeRating" snh-values="data.ratingValues"></snh-rating>

I did the same thing to the snh-form-field element for the rating entry for the very same reason:

<snh-form-field
       ng-show="data.includeRating"
       snh-model="data.rating"
       snh-name="rating"
       snh-type="rating"
       snh-label="Rate this {{data.tableLabel}}"/>

Providing a rating and viewing the cumulative results of ratings was something that I wanted to be optional, as including a rating in feedback is not always appropriate depending on the use case. To populate the data.includeRating variable with the desired alternative, I created a widget option, which I defined with the following JSON option schema:

[{
    "hint": "Set to TRUE if you want to include a rating with the text feedback",
    "name": "include_rating",
    "default_value": "false",
    "label": "Include Rating?",
    "type": "boolean"
}]

Once the option schema has been established, editing the widget instance in the Service Portal Page Designer will pop open a modal dialog where you can check a box to indicate that you would like ratings to be included for this instance of the widget.

Option dialog in Service Portal Page Designer

In the server side script, I look for this option and use it to determine the value of the data.includeRating variable.

data.includeRating = false;
if (options && options.include_rating) {
	data.includeRating = true;
}

This makes the widget a little more flexible, as you can check the box for one circumstance and leave it unchecked (the default) for others. Checking the box will get you the rating score card, user ratings under each user comment heading, and the ability to rate the item when you provide feedback. Without the box checked, all of those items are removed from view.