Aggregate List Columns, Part IX

“You have to finish things — that’s what you learn from, you learn by finishing things.”
Neil Gaiman

Last time, we attempted to wrap this whole thing up with the remaining modifications to the Content Selector Configuration Editor, but we ran into a problem with the code that we borrowed from the sn-record-picker Helper. Now that we have taken a quick detour to resolve that issue, we need to get back to our new pop-up aggregate column editor and apply the same fix to our pilfered code. As with the sn-record-picker Helper, we need to add some code to the server side to leverage the TableUtils object to get our list of tables in the extension hierarchy. Here is the new Server script, with basically the same function as the one that we added to the sn-record-picker Helper, with a few minor modifications due to the difference in our variable names.

(function() {
	if (input && input.tableName) {
		var tu = new TableUtils(input.tableName);
		var tableList = tu.getTables();
		data.tableList = j2js(tableList);
	}
})();

With that now in place, we can update the client-side buildFieldFilter function to call the server side to get the list, and then use that list to build the new filter.

$scope.buildFieldFilter = function() {
	c.data.fieldFilter = 'name=' + c.widget.options.shared.table.value;
	c.data.tableName = c.widget.options.shared.table.value;
	c.server.update().then(function(response) {
		if (response.tableList && Array.isArray(response.tableList) && response.tableList.length > 1) {
			c.data.fieldFilter = 'nameIN' + response.tableList.join(',');
		}
		c.data.fieldFilter += '^elementISNOTEMPTY^internal_type=reference';
	});
};

Now we just need to pop up the editor again and see if that actually fixes the problem that we were having earlier.

Pop-up aggregate column spec editor with field pick list corrected

That’s better! Now when I search for a field on the Incident table, I get to select from all of the fields on the table, not just the ones attached to the primary table. That’s what we wanted to see.

That takes care of the pop-up aggregate column specification editor that we cloned from the existing button/icon specification editor, so now all that is left for us to do is to tweak the code that actually saves the changes once all of the editing has been completed. The Save process actually rebuilds the entire script stored in the Script Include record, so we just need to add some code to create the aggregate column section for each table definition. Once again, we can leverage the existing buttons and icons code as a starting point, and then make the necessary changes to adapt it to use for aggregate columns. Here are the relevant lines from the Save() function in the widget’s Server script:

script += "',\n				btnarray: [";
var lastSeparator = '';
for (var b=0; b<tableTable[tableState.name].btnarray.length; b++) {
	var thisButton = tableTable[tableState.name].btnarray[b];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisButton.name;
	script += "',\n					label: '";
	script += thisButton.label;
	script += "',\n					heading: '";
	script += thisButton.heading;
	script += "',\n					icon: '";
	script += thisButton.icon;
	script += "',\n					color: '";
	script += thisButton.color;
	script += "',\n					hint: '";
	script += thisButton.hint;
	script += "',\n					page_id: '";
	script += thisButton.page_id;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

As we have done a number of times now, we can make a few global text replacements and alter a few variable names to align with our needs and come up with something that will work for aggregate columns specifications in very much the same way that it is currently working for buttons and icons.

script += "',\n				aggarray: [";
var lastSeparator = '';
for (var g=0; g<tableTable[tableState.name].aggarray.length; g++) {
	var thisAggregate = tableTable[tableState.name].aggarray[g];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisAggregate.name;
	script += "',\n					label: '";
	script += thisAggregate.label;
	script += "',\n					heading: '";
	script += thisAggregate.heading;
	script += "',\n					table: '";
	script += thisAggregate.table;
	script += "',\n					field: '";
	script += thisAggregate.field;
	script += "',\n					filter: '";
	script += thisAggregate.filter;
	script += "',\n					source: '";
	script += thisAggregate.source;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

And that’s all there is to that. That should be everything now, so all that is left to do is to bundle all of this into an Update Set so that folks can play along at home. This effort has been primarily focused on the components of the Customizing the Data Table Widget project, but it has also involved elements of the Configurable Data Table Widget Content Selector series as well as the Content Selector Configuration Editor. Additionally, many of the Service Portal pages that use these components, such as the Service Portal User Directory, also include the Dynamic Service Portal Breadcrumbs widget. Rather than create a new version for each and every one of these interrelated project, I think I will just lump everything together into a single Update Set and call it version 2.0 of the Customizing the Data Table Widget project. Since many of the widgets involved also utilize the Service Portal Form Fields, that will get pulled into the Update Set as well, and just for good measure, I think I will toss in the sn-record-picker Helper, too. That one is not actually directly related to all of the others, but we did steal some code from there, so there might be a few folks who may want to take a look at that one. You can download the whole lot from here. As always, if you have any comments, questions, concerns, or issues, please leave a comment below. All feedback is always welcome. And if you have made it this far, thanks for following along all the way to the end!

But wait … there’s more!

sn-record-picker Helper, Corrected (again!)

“The biggest room in the world is the room for improvement.”
Helmut Schmidt

The other day I was working on adding Aggregate List Columns to my SNH Data Table collection and I lifted some code from the sn-record-picker Helper that looked like it might be fairly close to what I needed at the time. Unfortunately, when I tested it all out, I came to realize that the code that I lifted contained an error, and a pretty serious one at that. The problem is with the filter used on all of the table field drop-downs, which is supposed to include all of fields on the selected table. As it turns out, it includes all of the fields on the primary table, but does not include all of the other fields from any other table in the table hierarchy. That is definitely not good!

Tables can be extended from other tables, and in cases such as the CMDB, there can be quite a number of tables in the extension hierarchy. For example, the cmdb_ci_win_server table extends the cmdb_ci_server table, which extends the cmdb_ci_computer table, which extends the cmdb_ci_hardware table, which extends the cmdb_ci table, which extends the cmdb table. That’s a lot of missing fields if you only include the fields from the primary table. Clearly, this needs to be addressed, and should have been addressed long ago.

In digging through the code, the problem appears to be in this function that creates the filter for all of the field pickers:

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

The current filter that is built turns out to be:

name=(name of selected table)

… when what it really needs to be is:

nameIN(list of all tables in the heirarchy)

That is simple enough to do, but then, the question becomes, where do we get a list of all of the tables in the hierarchy? As usual, the Now Platform has anticipated that someone might need that, and includes a way to get that very information. The solution to our query is the TableUtils object. That object includes a method called getTables() that will return just such a list. Unfortunately for us, this object is only available on the server side, and up until now, our widget did not need to have any server-side code. Well, things change.

Here is how we can use this object to get a Javascript Array of table names:

var tu = new TableUtils(input.table.value);
var tableList = tu.getTables();
data.tableList = j2js(tableList);

The object returned by the getTables() method is actually a Java object, so we have to use the j2js (Java to Javascript) function to turn it into an Array that we can use. Here is the entire new widget Server script with this functionality included:

(function() {
	if (input && input.table.value) {
		var tu = new TableUtils(input.table.value);
		var tableList = tu.getTables();
		data.tableList = j2js(tableList);
	}
})();

With that now available on the server side, we need to go into our client-side function and have it call the server to get the list, and then use that list to rebuild the filter in a way that will include all of the fields on all of the tables involved. Here is the new buildFieldFilter function:

$scope.buildFieldFilter = function() {
	c.data.ready = false;
	c.data.fieldFilter = 'name=' + c.data.table.value;
	c.server.update().then(function(response) {
		if (response.tableList && Array.isArray(response.tableList) && response.tableList.length > 1) {
			c.data.fieldFilter = 'nameIN' + response.tableList.join(',');
		}
		c.data.fieldFilter += '^elementISNOTEMPTY';
	});
};

If there is not more than one table involved, then we can leave the filter with the straight equals condition, but if there is more than one table in the returned list, then we overlay that with the IN condition instead. Either way, we tack on the rest of the original filter, and we should be good to go.

Well, that’s all there is to that. Now I just need to pull all of the parts together for a new Update Set, and we should be good until we come across the next previously undetected problem!

Aggregate List Columns, Part VIII

“We must accept responsibility for a problem before we can solve it. We cannot solve a problem by saying ‘It’s not my problem.’ We cannot solve a problem by hoping that someone else will solve it for us. I can solve a problem only when I say ‘This is my problem and it’s up to me to solve it.'”
M. Scott Peck

Last time, we completed the required modifications to the Configurable Data Table Widget Content Selector and started to tackle changes to the Content Selector Configuration Editor. Now we need to create a new Service Portal widget to handle the editing of new and existing aggregate column specifications. To begin, I cloned the existing Button/Icon Editor widget, and then dug into the HTML to see what it was that we had to work with.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.icon"
      snh-name="icon"
      snh-type="icon"/>
    <snh-form-field
      snh-model="c.widget.options.shared.color"
      snh-name="color"
      snh-type="select"
      snh-choices='[{"label":"default","value":"default"},{"label":"primary","value":"primary"},{"label":"secondary","value":"secondary"},{"label":"success","value":"success"},{"label":"danger","value":"danger"},{"label":"warning","value":"warning"},{"label":"info","value":"info"},{"label":"light","value":"light"},{"label":"dark","value":"dark"},{"label":"muted","value":"muted"},{"label":"white","value":"white"}]'/>
    <snh-form-field
      snh-model="c.widget.options.shared.hint"
      snh-name="hint"/>
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.page_id"
      snh-name="page_id"
      placeholder="Choose a Portal Page"
      table="'sp_page'"
      display-field="'title'"
      display-fields="'id'"
      value-field="'id'"
      search-fields="'id,title'"/>
    <div id="element.example" class="form-group">
      <div id="label.example" class="snh-label" nowrap="true">
        <label for="example" class="col-xs-12 col-md-4 col-lg-6 control-label">
          <span id="status.example"></span>
          <span class="text-primary" title="Example" data-original-title="Example">Example</span>
        </label>
      </div>
      <span class="input-group">
        <a ng-if="!c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">{{c.widget.options.shared.label}}</a>
        <a ng-if="c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">
          <span class="icon icon-{{c.widget.options.shared.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{c.widget.options.shared.hint}}</span>
        </a>
      </span>
    </div>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Basically, it is just a DIV full of SNH Form Fields for all of the properties of a Button/Icon configuration. Since many of the properties of an Aggregate Column specification are the same as those of a Button/Icon configuration, we only need to make the changes for those that are different. Here is what I came up with for my initial version:

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.table"
      snh-name="table"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.field"
      snh-name="field"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.filter"
      snh-name="filter"/>
    <snh-form-field
      snh-model="c.widget.options.shared.source"
      snh-name="source"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

In looking through the rest of the code in the widget, it does not appear that there are any other changes necessary to make this work, so the next thing to do would be to fire up the modified Content Selector Configuration Editor and see how things look.

New pop-up editor for aggregate columns

We can make sure that it actually works by adding an ‘x’ to each value on the form and clicking on the Save button. That should update the values displayed on the main screen.

Modified aggregate column values after saving the edits

The reason that all seems to work, even though the only changes that we made were to the HTML, is that the main widget and the pop-up editor widget share a common object called, appropriately enough, shared. Clicking on the Save button simply blows away the pop-up widget and returns control to the parent widget, where the values entered on the pop-up form are still available in the shared object, even though the pop-up widget is now gone. The new editAggregate function in the parent widget that we copied from the existing editButton function creates and populates this shared object, and then when the pop-up widget closes, uses the values in the shared object to update the edited row.

$scope.editAggregate = function(aggregate, aggArray) {
	var shared = {};
	if (aggregate != 'new') {
		shared.label = aggregate.label;
		shared.name = aggregate.name;
		shared.heading = aggregate.heading;
		shared.table = aggregate.table;
		shared.field = aggregate.field;
		shared.filter = aggregate.filter;
		shared.source = aggregate.source;
	}
	spModal.open({
		title: 'Aggregate Column Editor',
		widget: 'aggregate-column-editor',
		shared: shared
	}).then(function() {
		if (aggregate == 'new') {
			aggregate = {};
			aggArray.push(aggregate);
		}
		aggregate.label = shared.label || '';
		aggregate.name = shared.name || '';
		aggregate.heading = shared.heading || '';
		aggregate.table = shared.table || '';
		aggregate.field = shared.field || '';
		aggregate.filter = shared.filter || '';
		aggregate.source = shared.source || '';
	});
};

At this point, this would all be good enough to get the job done, but for the table and field properties, it would be nice to have a pick list of tables and a pick list of reference fields present on that table rather than just open entry fields. We should be able to do that with a couple of sn-record-pickers, so I could fire up our old friend, the sn-record-picker Helper and configure them, but we can do even better than that. The sn-record-picker Helper is actually built with a bunch of of sn-record-pickers, and it already contains one for a list of tables and several for lists of fields dependent on the table selected. We should be able to just steal most of that in its entirety to get what we want. Here is the SNH Form Field for the table picker:

<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>

With just a few simple modifications, we can adapt this to our needs:

<snh-form-field
  snh-model="c.widget.options.shared.table"
  snh-name="table"
  snh-type="reference"
  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'"/>

All I did here was to change the model to our model, drop the help (it would be helpful, but would make the pop-form way too long if all of the fields included help text), and dropped the closing tag in favor of a closing / at the end of the main tag. Easy peasy. Of course, we have now referenced a buildFieldFilter() function that doesn’t exist, but again, we should be able to just steal that from the sn-record-picker Helper widget. Here it is:

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

We don’t need the c.data.ready indicator for our use case, so we can just snag the whole thing, delete that line, and update the table variable name to match the one that we are using in our widget. Again, pretty simple stuff.

Now we just need to do the same for the field picker. There are several of these in the sn-record-picker Helper widget, but a few them allow multiple selections, so we will want to pick one that is just a single select, such as 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>

Here is my adaptation of that tag for our widget.

<snh-form-field
  snh-model="c.widget.options.shared.field"
  snh-name="field"
  snh-type="reference"
  snh-required="true"
  placeholder="Choose a Field"
  table="'sys_dictionary'"
  display-field="'column_label'"
  display-fields="'element,reference'"
  value-field="'element'"
  search-fields="'column_label,element'"
  default-query="c.data.fieldFilter"/>

That should take care of that. Let’s have a look and see what we have done.

New data pickers for the Table and Field fields

Well, that is really disturbing. Everything is working as intended as far as the relationship between the pickers is concerned, but when I went to select assigned_to from the drop-down, it was not on the list. It appears as if the fields on the list are limited to just those fields on the primary table, and the list does not include any of the fields for any of the tables from which that primary table may have been extended. This is particularly distressing news, as that means that the sn-record-picker Helper widget, from which we lifted this code, has had this issue all along. Not good! We are going to need to stop and fix that right away, and then we can revisit this guy next time out.

sn-record-picker Helper, Revisited

“The only thing that should surprise us is that there are still some things that can surprise us.”
Francois de La Rochefoucauld

I’m not generally one to obsess over SEO or site rankings or any kind of traffic analysis, but occasionally, I will poke around and see what comes up on searches, just for my own amusement. For the most part, this whole web site is for my own amusement, and I don’t really expend a lot energy trying to obtain an audience or generate any kind of interest. Mainly, I just like to create stuff or see what I can do, and then share it with anyone who might have a similar interest. So it surprises me quite a bit to search the web for something and find this site listed twice in the top 5 search results and also be the source of the definitive answer to the top question in the People also ask section of the results:

Google search results for sn record picker servicenow

There are quite a number of web sites out there that discuss this topic, so that just seems pretty amazing to me, all things considered. Let’s face it, my little tidbits of amateur hackery are pretty insignificant compared to the massive volumes of information out there dedicated to the ServiceNow product. I guess I should pay more attention, but I just did not expect to see that.

The other question that this brought to my mind was, why the sn-record-picker Helper? I mean, that was well over a year ago, and there are quite a few other areas of ServiceNow explored on this site. That was basically just a short two-part series, not counting the later correction. The sn-record-picker (also written and searched on as snRecordPicker, although that returns different results) is definitely a Service Portal thing, but I have written about a number of other Service Portal elements such as customizing the data table, dynamic breadcrumbs, integrated help, my delegates widget, my content selector and associated configurator, Highcharts, @mentions, a user directory, and my personal favorite, the snh-form-fields. Outside of the Service Portal, you can find other ServiceNow elements here such as the REST API, the Excel Parser, the Flow Designer, Webhooks, System Properties, Event Management, and a host of other just-as-interesting(?) topics. So what was so special about the sn-record-picker? I have no idea.

Still, it is interesting stuff. At least, it was interesting to me. One day I may invest a little more time into figuring out what drives such things, but other than finding it rather amazing when I stumble across it, I don’t see myself investing a whole lot of time on trying to manipulate or manufacture such results. It’s much more fun to me to build stuff and throw out Update Sets for people to pull down and play around with. Plus, if you have to manipulate the results artificially, then your place in the results is more of a reflection of your manipulation skills rather than the value of the content. I could build an article like this one that repeats the words sn-record-picker and snRecordPicker over and over again, just to see what happens, but no one wants to see that. At least, I don’t think so. Personally, I think my time would be much better spent getting back to building stuff, which is what I like to do anyway!

But there is a certain amount of satisfaction in seeing your own site show up on such a list. These things have a way of constantly shifting over time, though, so we’ll see how long it lasts.

Service Portal Form Fields, Corrected

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

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

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

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

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

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

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

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

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

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

Content Selector Configuration Editor, Part VI

“It is amazing what you can accomplish if you do not care who gets the credit.”
Harry Truman

At the end of our last installment in this series we had coded all of the client-side functions to add and remove Buttons/Icons and Reference Pages, but still needed to create the modal widgets needed to edit the values for those. The Button/Icon editor is the more complex of the two, plus it’s the first one that we encounter on the page, so let’s start with that one, and as usual, let’s start with the HTML.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.icon"
      snh-name="icon"
      snh-type="icon"/>
    <snh-form-field
      snh-model="c.widget.options.shared.color"
      snh-name="color"
      snh-type="select"
      snh-choices='[
        {"label":"default","value":"default"},
        {"label":"primary","value":"primary"},
        {"label":"secondary","value":"secondary"},
        {"label":"success","value":"success"},
        {"label":"danger","value":"danger"},
        {"label":"warning","value":"warning"},
        {"label":"info","value":"info"},
        {"label":"light","value":"light"},
        {"label":"dark","value":"dark"},
        {"label":"muted","value":"muted"},
        {"label":"white","value":"white"}]'/>
    <snh-form-field
      snh-model="c.widget.options.shared.hint"
      snh-name="hint"/>
    <div id="element.example" class="form-group">
      <div id="label.example" class="snh-label" nowrap="true">
        <label for="example" class="col-xs-12 col-md-4 col-lg-6 control-label">
          <span id="status.example"></span>
          <span title="Example" data-original-title="Example">Example</span>
        </label>
      </div>
      <span class="input-group">
        <a ng-if="!c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">{{c.widget.options.shared.label}}</a>
        <a ng-if="c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">
          <span class="icon icon-{{c.widget.options.shared.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{c.widget.options.shared.hint}}</span>
        </a>
      </span>
    </div>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Pretty standard stuff. There are just a few more fields here than with some of the other things that we have been editing in a pop-up window, and one of them happens to be the new icon field type. Also, there is an “Example” field to show you what the Button/Icon will look like based on the values that you have entered. Here’s how it looks in action:

Button/Icon Editor

Other than the additional fields, it’s pretty much the same as the others, and the client-side code is virtually identical to what we have used in the past, with the one exception of having to assign spModal to the $scope so that it can be available to the Icon Picker.

function ButtonIconEditor($scope, $timeout, spModal) {
	var c = this;

	$scope.spModal = spModal;

	$scope.selectIcon = function() {
		spModal.open({
			title: 'Select Icon',
			widget: 'icon-picker',
			buttons: [
				{label: '${Cancel}', cancel: true}
			],
			size: 'sm',
		}).then(function(response) {
			c.widget.options.shared.icon = response.selected;
		});
	};

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

As with our other pop-up editor widgets, there is no server-side code, so that’s the entire widget. The Reference Page editor is even simpler.

<div>
  <form name="form1">
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.table"
      snh-name="table"
      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
      snh-type="reference"
      snh-model="c.widget.options.shared.page"
      snh-name="page"
      snh-required="true"
      placeholder="Choose a Portal Page"
      table="'sp_page'"
      display-field="'title'"
      display-fields="'id'"
      value-field="'id'"
      search-fields="'id,title'"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Here, we just have two sn-record-pickers, one for the reference table and the other for the portal page that you want to bring up whenever someone clicks on a reference value from that table. And once again, the client-side code looks very familiar.

function ReferencePageEditor($scope, $timeout) {
	var c = this;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

That takes care of everything that we discussed last time, but last time we neglected to make allowances for a couple of very import features: the ability to add a new Table and the ability to remove a Table. Since each Perspective has its own list of Tables, we will need to add a New Table button at the bottom of the list for each Perspective. To remove a Table, we can just add a Delete icon next to the Table name for that purpose. That will change the basic HTML structure to now look like this:

<div>
  <h4 class="text-primary">${Tables}</h4>
</div>
<uib-tabset active="active">
  <uib-tab ng-repeat="persp in c.data.config.perspective track by persp.name" heading="{{persp.label}}">
    <div ng-repeat="tbl in c.data.config.table[persp.name] track by tbl.name" style="padding-left: 25px;">
      <h4 style="color: darkblue">
        {{tbl.displayName}}
        ({{tbl.name}})
        <img src="/images/delete_row.gif" ng-click="deleteTable(persp.name, tbl.name)" alt="Click here to permanently delete table {{tbl.name}} from this Perspective" title="Click here to permanently delete table {{tbl.name}} from this Perspective" style="cursor: pointer;"/>
      </h4>
      <uib-tabset active="active">
        <uib-tab ng-repeat="state in c.data.config.state track by state.name" heading="{{state.label}}">
          <div style="padding-left: 25px;">
 
<!-----  all of the specific properties are defined here  ----->
 
          </div>
        </uib-tab>
      </uib-tabset>
    </div>
    <div style="width: 100%;">
      <button ng-click="newTable(persp.name)" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new table to the {{persp.label}} perspective">Add a new Table</button>
    </div>
  </uib-tab>
</uib-tabset>

And of course, we will need functions to handle both the Table Add and Table Remove processes. For the Add, let’s use a pop-up Table selector that will allow you to select a Table from a list. For the Delete, we can adapt one of the other Delete functions that we have already written.

$scope.deleteTable = function(perspective, tableName) {
	var confirmMsg = '<b>Delete Table</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += tableName;
	confirmMsg += ' Table will also delete all information for that Table in every State in this Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this Table?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<c.data.config.table[perspective].length; b++) {
				if (c.data.config.table[perspective][b].name == tableName) {
					a = b;
				}
			}
			c.data.config.table[perspective].splice(a, 1);
		}
	});
};

The function to pop open the modal Table Selector widget is quite familiar as well.

$scope.newTable = function(perspective) {
	var shared = {};
	spModal.open({
		title: 'Table Selector',
		widget: 'table-selector',
		shared: shared
	}).then(function() {
		c.data.config.table[perspective].push(shared);
	});
};

… and the widget itself we can clone from the Reference Page widget, which already had a Table picker defined.

<div>
  <form name="form1">
    <snh-form-field
      snh-type="reference"
      snh-model="c.data.table"
      snh-name="table"
      snh-change="tableSelected();"
      snh-required="true"
      snh-help="Choose a Table to be added to this Perspective"
      placeholder="Choose a Table"
      table="'sys_db_object'"
      display-field="'label'"
      display-fields="'name'"
      value-field="'name'"
      search-fields="'name,label'"/>
  </form>
</div>

Since all we want them to do is to select a table, we can use an snh-change to call the function and send back the selection.

function TableSelector($scope, $timeout) {
	var c = this;

	$scope.tableSelected = function() {
		if (c.data.table.value > '') {
			c.server.update().then(function(response) {
				c.widget.options.shared.name = response.table.value;
				c.widget.options.shared.displayName = response.table.displayValue;
				$timeout(function() {
					angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
				});
			});
		}
	};
}

On server side, we just make sure that we have both a table name and a display name.

(function() {
	data.table = {};

	if (input && input.table && input.table.value) {
		data.table = input.table;
		if (!data.table.displayValue) {
			data.table.displayValue = getItemName(data.table.value);
		}
	}

	function getItemName(key) {
		var ciGR = new GlideRecord(key);
		return ciGR.getLabel();
	}
})();

That pretty much wraps up all of the editing functions. We still need to throw in some code to save all of the changes, but that might get pretty involved, so let’s say we save that for our next installment.

sn-record-picker Helper, Corrected

“We are what we repeatedly do. Excellence, then, is not an act, but habit.”
Aristotle

The other day I was using my sn-record-picker Helper to create a picker that allowed multiple selections and I discovered that there were a couple of undetected errors in there that needed to be cleaned up. I rarely have an occasion to use the multiple=”true” option, so I never noticed the issues before. The first one was relatively simple: there was an extra trailing quote in the generated code for the multiple attribute. That was easy enough to fix. The other one was a little more complicated. The live example of the configured picker was never set up to handle multiple selections. That seemed like it would be a relatively easy fix, but it turned out to be a little more complicated than I realized.

My first thought was to just add the multiple attribute to the tag, and set the value to the value of the checkbox on the form, thinking that it would resolve to true or false and take care of the problem.

<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"
  multiple="c.data.multiple">

Unfortunately, that didn’t work. It didn’t really do anything bad, it just did not render out as a multiple selection picker, even when I checked the box. I thought maybe that it needed to be interpreted/resolved, so I surrounded the variable with double curly braces.

  multiple="{{c.data.multiple}}">

Things really went South at that point. The whole thing crashed with the following error:

invalid key at column 2 of the expression [{{c.data.multiple}}] starting at [{c.data.multiple}}].

So, I tried a number of other, different things, none of which seemed to do the trick. Apparently, you have to hard-code the value of that attribute to true or it just won’t work. So much for making it dynamic. So, in the end, I had to create two versions of the element, one for single and another nearly identical one for multiple, and then show or hide them based on the value of the c.data.multiple variable.

<snh-form-field
  ng-hide="c.data.multiple"
  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>
<snh-form-field
  ng-show="c.data.multiple"
  snh-model="c.data.liveExample"
  snh-name="liveExample"
  snh-label="Live Example"
  snh-type="reference"
  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"
  multiple="true">
</snh-form-field>

Not the most elegant solution, but it does work, so there’s that. One thing that did not work on the multiple version was the modal pop-up on change. That works pretty slick on the single selection version, but on the multiple, the change event never fires. I played around with that for a while looking for a solution, but I finally gave up and just removed that attribute from the multiple version, since it didn’t actually do anything. On the multiple version, everything that you have selected is already displayed right there in front of you, so I figured that we weren’t losing all that much by my not finding a ready solution to the problem.

So that’s it: two little fixes. It’s not all that much, but it does correct a couple of annoying little problems, so here’s a fresh Update Set with the corrections in place.

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

Configuration Item Icon Assignment Widget, Part II

Walking on water and developing software from a specification are easy if both are frozen.”
Edward V. Berard

It wasn’t entirely true that my empty shell CI Icon Assignment widget was completely devoid of all code underneath. I did have to toss in the function that launches the Retina Icon Picker in order to show that in action, but I was able to steal most of that code from the little test page that I had built to try that guy out.

$scope.selectIcon = function(inx) {
	spModal.open({
		title: 'Select Icon',
		widget: 'icon-picker',
		buttons: [
			{label: '${Cancel}', cancel: true}
		],
		size: 'sm',
	}).then(function(response) {
		c.data.itemArray[inx].icon = response.selected;
	}});
};

In my original test page, I had a second response function to handle the Cancel button, but for our purposes here, that’s not really necessary. If you hit the Cancel button on the pop-up on this widget, it will just close and nothing else will happen, which is perfectly fine. The other alteration that I had to make was to include an index argument so that when you did make a selection, it was applied to the appropriate line. Other than that, it’s pretty much what we were working with earlier.

But the icon picker is not the only element on the screen that needs a little code behind it. We’ll need to do something with the Configuration Item picker as well. Selecting a CI from the picker should result in a new row added to the table for the selected CI, and then the picker should be cleared in preparation for another selection to be made. Since we have to go look up the label for our second column, that sounds like a server side operation, so our client side code will be pretty simple:

$scope.addSelected = function() {
	c.server.update();
	$('#snrp').select2("val","");
};

The first line just kicks the process over to the server side, and then then second line clears out the value of the sn-record-picker so that it is ready for another selection. Over on the server side, we do the work to add the new CI type to the list, as long as it isn’t already there.

if (input) {
	data.itemArray = input.itemArray;
	if (input.classToAdd && input.classToAdd.value) {
		addClassToList(input.classToAdd.value);
	}
}

function addClassToList(key) {
	var foundIt = false;
	for (var i=0; !foundIt && i<data.itemArray.length; i++) {
		if (data.itemArray[i].id == key) {
			foundIt = true;
		}
	}
	if (!foundIt) {
		var thisItem = {};
		thisItem.id = key;
		thisItem.name = getItemName(key);
		data.itemArray.push(thisItem);
	}
	input.classToAdd = {};
}

function getItemName(key) {
	var ciGR = new GlideRecord(key);
	return ciGR.getLabel();
}

That takes care of adding an item to the list. Now we have to add some code to handle removing an item from the list. We put a Delete icon at the end of each line, so we just need to build the code that will run when someone clicks on that icon. Since it is a delete, we will want to pop up a quick confirm dialog before we actually remove the item from the list, just to make sure that they did that on purpose. All of that can be handled on the client side, and fairly simply.

$scope.removeItem = function(inx) {
	spModal.confirm('Remove ' + c.data.itemArray[inx].id + ' from the list?').then(function(confirmed) {
		if (confirmed) {
			c.data.itemArray.splice(inx, 1);
		}
	});
};

To test all of that out, I added a few items to the list, selected an icon for each, and then tried to remove a few. During my testing, I realized that adding enough items to the list scrolls the icon selector off of the page and out of sight, which I didn’t really like, so I did a little rearranging and put the selector on top of the list instead of underneath. With that change, the modal delete confirmation now looks like this:

Delete confirmation pop-up on reconfigured screen layout

So now we have all of the code in place to select a CI, to select an icon for the CI, and to remove a CI from the list. Now all we have left to do is to put some code under the two buttons at the bottom of the screen. Of course, coding the Save button would be much easier if I knew where I was going store this information once I am through building the list, but I haven’t really given much thought to that just yet, since we hadn’t quite gotten to that part until just this moment. Now that writing the code for the buttons is the only thing left to do, it’s time to figure that out. But that could potentially be an entire installment in and of itself, so I think we’ll stop here and save that exercise for another day.

Formatted Script Search Results, Enhanced

“Delivering good software today is often better than perfect software tomorrow, so finish things and ship.”
— David Thomas, The Pragmatic Programmer: From Journeyman to Master

It always seems to happen. The more I play around with some little bauble that I’ve thrown together, the more ideas that pop into my head to make it just a little bit better. I’ve actually found my script search tool to be quite useful now that I have finally gotten it to behave the way in which I would expect it to behave. But the more that I use it, the more that I find that I want add to it.

One thing that I realized after I released the code was that you can also find script in Condition String fields. This blows my whole theory that all column types that contain script have the word script in their name. Still, I can tweak my encoded query just a bit to ensure that these columns are searched as well. I just need to switch from this:

internal_typeCONTAINSscript^active=true^name!=sn_templated_snip_note_template

… to this:

active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINSscript^ORinternal_type=condition_string

Another thing that crossed my mind while I was searching for something the other day was that, in addition to scripts, I would really like to have this same capability for searching HTML. At the time, it seemed like it wouldn’t be too difficult to just clone all of the parts and convert them to a second set that was dedicated to HTML instead of Javascript. When I took a look at doing that, though, I realized that with just little bit of extra code, I could make the parts that I had work for both, and not have to create an entirely new second set.

The first thing that I tackled was my Script Include, ScriptUtils, To start out, I renamed it to SearchUtils, since it was now going to handle both searching all of the script columns and searching all of the HTML columns. Then I added a second argument to the function call called type, so that I could differentiate between script search requests and HTML search requests. The only real difference in the code between searching for script and searching for HTML is which columns on which tables will be queried, which means that the only real difference between the two operations is the query used to find the columns and tables. In the current version, that query was hard-coded, so I switched that to a variable and then set the value of the variable to the new version of my encoded query for scripts, and then overrode that value with a new query for HTML if the incoming type argument was set to ‘html’.

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

	findInScript: function(string, type) {
		var table = {};
		var found = [];
		var encodedQuery = 'active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINSscript^ORinternal_type=condition_string';
		if (type == 'html') {
			encodedQuery = 'active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINShtml';
		}
		var columnGR = new GlideRecord('sys_dictionary');
		columnGR.addEncodedQuery(encodedQuery);
...

Beyond that point, everything else remains the same as it was in the earlier version. That takes care of the Script Include. Now we have to tackle the associated Widget. Here we can use the same basic philosophy, defaulting everything to a script search, and then overriding that default if an HTML search is requested. For a Widget, that request can come in the form of an additional URL parameter that we can also call type. We can establish the default values based on a script search, and then if the URL parameter type is equal to ‘html’, we can then change to the values for an HTML search.

(function() {
	data.type = 'script';
	data.title = 'Script Search';
	if ($sp.getParameter('type') && $sp.getParameter('type') == 'html') {
		data.type = 'html';
		data.title = 'HTML Search';
	}
	data.loading = false;
	data.searchFor = '';
	data.result = false;
	if ($sp.getParameter('search')) {
		data.loading = true;
		data.searchFor = $sp.getParameter('search');
		data.result = new SearchUtils().findInScript(data.searchFor, data.type);
		data.loading = false;
	}
})();

That was all there was to adding an HTML version of the script searching capability. To get to it, I pulled my Search Script menu item up in edit mode by clicking on the little pencil to the right of the title, made a few minor modifications and then selected Insert from the context menu to create a new menu item.

Cloning the Search Script menu item to create the Search HTML menu item

With the new menu item added to the sidebar, the only thing left to do was to click on the item and give it a go.

New HTML Search function in action

Well, that didn’t turn out too bad. I should have changed the name of the button to something that didn’t have the word script in it, but other than that one little nit, it all seems to work. I’ll fix up that button one day, but for now, here is an Update Set that includes all of the parts for the current version.

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.