Reference Type System Properties, Part III

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

Last time, we got far enough along in the System Property value page modifications to demonstrate that we could replace the rendered input element with something else of our own design. Not having a design of our own for an adequate replacement, we implemented the popular creative avoidance strategy by working on all of the other parts and pieces first until we finally ran out of other parts and pieces. Now it is time to come up with a plan and finish this little project up.

I have to admit that I’m not all that proud of what I eventually came up with, but it does satisfy Rule #1, so at this point, I’ll take it and call it good. I tried a number of other things first, but none of those really got me what I wanted, so here we are. The basic plan is pretty simple, and consists of two parts: 1) a hidden input element to replace the text input element so that it can be submitted with the form, and 2) an iframe into which we will put our new input element via a stand-alone page designed for that purpose. I don’t really like the iframe approach, but it does have the benefit of being independently rendered, which gives us the opportunity to leverage the snRecordPicker for our input, which we really cannot do by simply modifying the main page directly after it has been delivered.

So let’s start out with the script that will build the HTML that we will use to replace the original text input element:

function buildHTML(prop) {
	var html = "";
	html += "<input type=\"hidden\" id=\"" + prop.property + "\" name=\"" + prop.property + "\"/>\n";
	html += "<div style=\"width: 100%; height: auto;\">\n";
	html += " <iframe id=\"frame." + prop.property + "\" src=\"/$sp.do?id=reference_properties&sysparm_property=" + prop.property + "&sysparm_table=" + prop.tableName + "&sysparm_column=" + prop.column + "&sysparm_value=" + prop.value + "\" style=\"border: none; height: 65px; width: 100%;\"></iframe>\n";
	html += "</div>\n";
	return html;
}

The hidden input element virtually replaces the original text input element, having the same name and same id. The iframe element is pretty vanilla stuff as well; the only thing of interest really is the src parameter, which points to the Portal Page that we are about to create, and passes along all of the various values needed to make the page do what we want. The Portal Page itself is just a single page with a single container filled with a single widget. The page is not really worth looking at, so let’s just jump right into the widget, as that’s where all of the magic happens. Here is the HTML:

<div id="pickerdiv">
  <sn-record-picker field="field" table="c.data.table" display-field="c.data.column" value-field="'sys_id'" search-fields="c.data.column" page-size="c.data.pageSize"></sn-record-picker>
</div>

Not much to see there, either. It’s just your standard snRecordPicker with pretty much every attribute defined by a variable. We’ll snag the values for those variables off of the URL for the page, which we populated when we constructed the src attribute for the iframe tag. The widget’s client-side script does most of the heavy lifting here:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
}

The only reason for the server-side script is to fetch the display value of the property if the property is valued at the time that the page is delivered to the browser.

(function() {
	if (input) {
		if (input.fieldValue) {
			var gr = new GlideRecord(input.table);
			gr.get(input.fieldValue);
			data.fieldDisplayValue = gr.getDisplayValue(input.column);
		} else {
			data.fieldDisplayValue = '';
		}
	}
})();

That’s about all there is to it. For every property on the page where Type=reference, the standard text input element is replaced with a hidden input element and an iframe, and inside the iframe is a ServiceNow Service Portal page that contains a single widget containing a single snRecordPicker. The parameters for the picker are passed from the iframe to the portal page via URL parameters, which are picked up by the widget and used to configure the snRecordPicker. All changes to the snRecordPicker are copied over to the hidden input field, so when the form is submitted, the selected value is sent to the server and posted to the database.

There was a minor problem with this initial version when trying to figure out the optimum height for the iframe. The height of the snRecordPicker depends on whether or not the drop-down list of choices is present, and I couldn’t find a CSS-centric way of having the iframe automatically adjust for the change in height, nor could I find a way to have the drop-down list of selectable choices overlay the content below, which is outside of the iframe. Finally, I resorted to plain old Javascript, and set up a variable called c.data.expanded to indicate whether or not the pick list was present on the screen. With a little view selection source magic, I was able to figure out that the component to watch had an id of select2-drop-mask, and so I modified the widget’s client-side code to check the required iframe height every second and adjust if needed:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	c.data.expanded = false;
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
	checkHeight();
	function checkHeight() {
		var elem = document.getElementById('select2-drop-mask');
		if (elem) {
			if (elem.style.display == 'none') {
				if (c.data.expanded) {
					c.data.expanded = false;
					setHeight('65px');
				}
			} else {
				if (!c.data.expanded) {
					c.data.expanded = true;
					setHeight('300px');
				}
			}
		}
		setTimeout(checkHeight, 1000);
	}
	function setHeight(newHeight) {
		parent.updateFrameHeight(c.data.property, newHeight);
	}
}

Once that code was in place, the unexpanded state looked like this:

Modified input element with choices collapsed

… and the expanded state looked like this:

Modified input element with choices expanded

It still disturbs my sense of The Way Things Ought To Be for the left-hand edge of the revised input element not to line up with the left-hand edge of all of the other original input elements, but a fix to that was not readily apparent to me, so I have managed to let that go for now and move on to more important things. One day, though, I am going to figure out a way to fix that!

Just to recap, we modified a choice list and added an additional field to a table to provide the capability to define properties of type reference. We then created a UI Script and a Script Include so that we could replace the original input element on the property UI page, and then we created a Service Portal page and associated widget to provide the replacement for the original input element. As soon as I get a chance, I will see if I can wrap all of that up into a single Update Set and get it posted out here in case anyone wants just grab the whole package. All in all, it was a fun little project, but one that I hope to throw away one day when ServiceNow actually supports reference type properties right out of the box and having this little tweak is no longer needed.

Update: Well, it took a little longer than I had hoped to get around to this, but here is that Update Set finally.