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

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

User Rating Scorecard

“We keep moving forward, opening new doors, and doing new things, because we’re curious and curiosity keeps leading us down new paths.”
Walt Disney

So, I have been scouring the Interwebs for the different ways people have built and displayed the results of various user rating systems, looking for the best and easiest way to display the average rating based on all of the feedback to date. I wanted it to be graphic and intuitive, but also support fractional values. Even though the choices are whole numbers, once you start aggregating the responses from multiple individuals, the end result is going to end up falling somewhere in between, and on my Generic Feedback Widget, I wanted to be able to show that level of accuracy in the associated graphic.

The out-of-the-box display of a Live Feed Poll show how many votes were received from each of the possible options. That didn’t seem all that interesting to me, as I was looking for a single number that represented the average score, not how many votes each possibility received. Then I came across this.

User Rating Scorecard from W3C

That solution did not support the fractional graphic, but it did include the breakdown similar to the stock Live Feed Poll results, which got me rethinking my earlier perspective on not having any use for that information. After seeing this approach, I decided that it actually would be beneficial to make this available, but only on request. My thought was to display the graphic, the average rating, and the number of votes, and then have a clickable link for the breakdown that you could use if you were interested in more details.

All of that seemed rather complex, so I decided that I would not try to wedge all of that functionality into the Generic Feedback Widget, but would instead build a separate component to display the rating and then just use that component in the widget. This looked like a job for another Angular Provider like the ones that I used to create my Service Portal Form Fields and Service Portal Widget Help. I was thinking that it could something relatively simple to use, with typical usage looking something like this:

<snh-rating values="20,6,15,63,150"></snh-rating>

My theory on the values attribute was that as long as I had the counts of votes for each of the options, I could generate the rest of the data needed such as the total number of votes and the total score and the average rating. The trouble, of course, was that the function that I built to get the info on the votes cast so far did not provide this information. So, back the drawing board on that little doo-dad …

I actually wanted to use a GlideAggregate for this originally, but couldn’t do it when dot-walking the option.order wasn’t supported. But now that I have to group by option to get a count for each, that is no longer an issue and I can actually rearrange things a bit and not have to loop through every record to sum up the totals. Here’s the updated version that returns an array of votes for each possible rating:

currentRating: function(table, sys_id) {
	var rating = [0, 0, 0, 0, 0];
	var pollGR = new GlideRecord('live_poll');
	if (pollGR.get('question', table + ':' + sys_id + ' Rating')) {
		var castGA = new GlideAggregate('live_poll_cast');
		castGA.addAggregate('COUNT');
		castGA.addQuery('poll', pollGR.getUniqueValue());
		castGA.groupBy('option');
		castGA.orderBy('option.order');
		castGA.query();
		while (castGA.next()) {
			var i = castGA.getValue('option.order') - 1;
			rating[i] = parseInt(castGA.getAggregate('COUNT'));
		}
	}
	return rating;
},

That should get me the data that I will need to pass to the new snh-rating tag. Now all I have to do is build the Angular Provider behind that tag to turn those values into a nice looking presentation. That sounds like a good topic for a next installment!

Static Monthly Calendar

“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.”
George Herbert

Recently, I needed to display a monthly calendar with a few days singled out as noteworthy. That got me to thinking, as it usually does, that it would be nice to have the empty shell calendar as a separate component so that it could be re-utilized for other potential purposes. Aside from my own unique requirements, I could see where someone might want to have something like a payroll calendar filled with holidays and paydays or an event calendar with the line-up of live music each evening. Maybe you just want to call out National Step in a Puddle and Splash Your Friends Day or you just want to be able look at a calendar and see when the cafeteria is serving chipped beef on toast with steamed carrots, Tator Tots, and a butterscotch pudding cup. I have no idea what someone might want to do with a configurable calendar, but it seems like the possibilities could be endless, so it might be worth having a reusable part.

And maybe such a part already exists. ServiceNow actually has a built-in calendar that is used quite extensively, so that seemed like a good place to start. Like many ServiceNow components, it is its own Open Source project called, appropriately enough, Full Calendar. It is quite full, indeed, and has a lot of very cool features, but I was looking for something basic and read-only, and for my purposes, I was gong to have to figure out how to turn most of those features off. I just wanted a static, single-month view that was uneditable and not interactive in any way, except for maybe the ability to click on a date and pull up a little more information. I played around with it for a while, but in the end, I decided that it was going to be more work to turn off a bunch of capabilities than it would be to start off with something much more simplistic.

So then I scoured the Interwebs for a basic HTML calendar template, of which there are many, and finally settled on this one:

Calendar template from responsivedesign.is

It pretty much had all of the characteristics that I was looking for, so I cracked open a brand new widget and pasted in the HTML and the CSS. Then I created a new Portal Page and dragged my new widget into a full-width container and pulled it up to see how it looked. Since it is just a template, everything is hard-coded, but at this point, I just wanted to make sure that I had all of the parts and pieces and that it came out looking as it should (which it did). This is just the parts that I had found at this stage, pasted into a new Service Portal Widget.

Calendar template as a Service Portal Widget

OK, so far, so good. This was the basic structure that I wanted; now, I just needed to replace the hard-coded data with something a little more dynamic. To start with, I wanted to be able to navigate to the next month and to the previous month. Nothing beyond that — just a simple forward/backward capability that would let you move around a bit. So I tinkered with the HTML for the header and came up with this:

<header>
  <span class="col-sm-4">
    <button class="btn btn-default" ng-click="newMonth(-1);"><< Previous Month</button>
  </span>
  <span class="col-sm-4">
    <h1>{{data.monthLabel}}</h1>
  </span>
  <span class="col-sm-4">
    <button class="btn btn-default" ng-click="newMonth(1);">Next Month >></button>
  </span>
</header>

This simply divided the header into three equal parts, the “go back” button, the original title (based on a variable now), and the “go forward” button. I had both buttons call the same routine, passing either a +1 or a -1, depending on which direction you wanted to go. That routine lives in the client script, which now looks like this:

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

	$scope.newMonth = function(offset) {
		var yy = parseInt(c.data.year);
		var mm = parseInt(c.data.month) + offset;
		if (mm < 0) {
			mm = 11;
			yy = yy - 1;
		} else if (mm > 11) {
			mm = 0;
			yy = yy + 1;
		}
		var s = $location.search();
		s.month = mm;
		s.year = yy;
		var newURL = $location.search(s);
		spAriaFocusManager.navigateToLink(newURL.url());
	}
}

The routine simply navigates to the same page with different values for the year and month URL parameters. To process those parameters when the page loads, we also need a little code on the server side:

var monthName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var today = new Date();
data.month = today.getMonth();
data.year = today.getFullYear();
if ($sp.getParameter('month') && $sp.getParameter('year')) {
    data.month = $sp.getParameter('month');
    data.year = $sp.getParameter('year');
}
data.monthLabel = monthName[data.month] + ' ' + data.year;

This code basically establishes the values of the month and year based on the current date, and then if there is both a month and a year parameter on the URL, it overrides that initial value with the values passed via the URL. Then, using an array of month names, it establishes the calendar heading label based on the month and year values. At this point, we still have a hard-coded calendar body, but we can now use the new buttons to move forward and back through time, and the heading will change, even if the calendar itself still remains constant. All of that appears to work at this point, so now we just have to make the calendar itself as dynamic as the heading value.

To make the HTML portion simpler, I decided to build out the structure of the days to be displayed in the server side script. The structure is simply an array of 4 to 6 weeks, with each week containing an array of 7 days. The number of weeks in a month is variable, depending on how many 7-day rows it will take to display all of the days in the month, but the number of days in a week is always 7, regardless of how many of those days actually fall in the month to be displayed. The code to build out the model array is fairly self-explanatory, so I won’t dwell on that here, but here it is, for those of you interested in the details:

data.week = [];
data.firstDay = new Date(data.year, data.month, 1);
var offset = data.firstDay.getDay();
data.thisDay = new Date(data.firstDay.getTime());
while (data.thisDay.getMonth() == data.month) {
	var thisWeek = [];
	data.week.push(thisWeek);
	for (var i=0; i<7; i++) {
		var thisDay = {};
		thisWeek.push(thisDay);
		if (data.week.length > 1 || i >= offset) {
			if (data.thisDay.getMonth() == data.month) {
				thisDay.date = data.thisDay.getDate();
				data.thisDay.setDate(data.thisDay.getDate() + 1);
			}
		}
	}
}

Building the model in the server-side code makes the HTML portion quite simple. I deleted all of the hard-coded example code (except for the day of the week labels) and then replaced it with this:

<div id="calendar">
  <ul class="weekdays">
    <li>Sunday</li>
    <li>Monday</li>
    <li>Tuesday</li>
    <li>Wednesday</li>
    <li>Thursday</li>
    <li>Friday</li>
    <li>Saturday</li>
  </ul>
  <ul ng-repeat="w in data.week" class="days">
    <li ng-repeat="d in w" class="day" ng-class="{'other-month':!d.date}">
      <div class="date" ng-if="d.date">{{d.date}}</div>
    </li>
  </ul>
</div>

Basically, it is just a couple of ng-repeats, one for the array of weeks and another within that for the 7 days of the week. The original calendar template had numbers for all of the days, whether they were in the current month or not, but I didn’t really like that approach, so I did not calculate those values, and threw in an ng-if to keep that number DIV from appearing for those days that do not belong to the month that is the subject of the current display. At this point, we now have a completely empty calendar based on the selected month and year:

Blank calendar with dynamic days based on active month/year

This is pretty much the empty calendar shell that I had envisioned, although at this point there is no way to pass in content for any given day. I still have to work out the best way to go about that, but conceptually, this is exactly the kind of thing that I was hoping to produce: a plain monthly calendar with moderate navigation capabilities and the potential for adding custom content based on the use case. This looks like a good stopping place for now, so I have bundled up the parts thus far and created an Update Set for anyone who might have an interest. Next time, I will add the capability to pass in content, and come up with some kind of example to demonstrate how that all works.

@mentions in the Service Portal, Revisited

“If you don’t have time to do it right, when will you have the time to do it over?”
John Wooden

After adding a mention type to my Service Portal form field tag, I broke my original ment.io example. I actually used that example in building the code to support the mentions type, but I did have to make a few changes in order to get everything to work, and now that I have done that, my original example doesn’t work anymore. I thought about just abandoning it since I got the form field version working, but then I thought that there were some other possible use cases where I wouldn’t want to be using an snh-form-field, so I decided that I had better dig around and see what was what.

The form field tag version did make things quite a bit simpler. Compare the initial version:

<snh-form-field
  snh-model="c.data.textarea"
  snh-name="textarea"
  snh-type="textarea"
  snh-label="${Original ment.io Example}"
  snh-help="While entering your text, type @username anywhere in the message. As you type, an auto-suggest list appears with names and pictures of users that match your entries. For example, if you type @t, the auto-suggest list shows the pictures and names of all users with names that start with T. Click the user you want to add and that user's name is inserted into the @mention in the body of your text."
  mentio=""
  mentio-macros="macros"
  mentio-trigger-char="'@'"
  mentio-items="members"
  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention('textarea', item)"
  mentio-typed-term="typedTerm"
  mentio-id="'textarea'"
  ng-trim="false"
  autocomplete="off"/>

… with the snh-form-field version, and you can see right away that there is a significant difference in size alone:

<snh-form-field
  snh-model="c.data.mention"
  snh-name="mention"
  snh-type="mention"
  snh-label="${snh-form-field ment.io Example}"/>

Just for comparison, I decided to put both on the page (and make them both work!), and then add another using the new feedback field type, and yet another using just a plain, old, ordinary textarea.

new @mentions example page with various versions

The errors on the original weren’t that significant. Mainly I just wanted to put out a new version of the Update Set so that you could see all of the various approaches working on the same page side by side (… well, on top of each other, if you really want to get specific). If you are really curious as to what exactly needed to be changed, you can always compare the old to the new.

Even More Service Portal Form Fields

“Have a bias toward action – let’s see something happen now. You can break that big plan into small steps and take the first step right away.”
Indira Gandhi

After working out all of the little issues in my @mentions example, it occurred to me that it might be even better to just make an @mentions textarea another supported field type like I just did for feedback. This way, you wouldn’t have to bother with all of those mentio- prefixed attributes at all — they would just become part of the internal process of rendering the form field. Some things would have to be standardized, which might limit your flexibility in certain areas, but that still wouldn’t prevent you from mentionizing your own textarea or textarea-type form field. This would just be yet another option.

The first thing to, then, would be to add mention to the list of supported field types:

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

That’s getting to be quite a list, but that’s not necessarily a bad thing. Next, we have to add the logic to render the input element:

} else if (type == 'mention') {
	htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" mentio=\"\" mentio-macros=\"macros\" mentio-trigger-char=\"'@'\" mentio-items=\"members\" mentio-search=\"searchMembersAsync(term)\" mentio-template-url=\"/at-mentions.tpl\" mentio-select=\"selectAtMention('" + name + "', item)\" mentio-typed-term=\"typedTerm\" mentio-id=\"'" + name + "'\" ng-trim=\"false\" autocomplete=\"off\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";

So far, so good. Now, where to stuff the template? In my mind, the best place to locate the template would be in the Service Portal itself, as that would ensure that it would only appear on the page once and only once. You could just make it part of your portal page header and be done with it. However, if you didn’t know to do that, or if you moved your widget from a portal that had it to another portal that did not, things would not work and would not be that intuitive as to why that was. Placing it in the widget is another option, but again, you would have to know to do that. Rendering it as part of the form field means that you could have many copies of the template ending up on a single page, but then everything would be self-contained and you wouldn’t have to know to add anything else to include this feature other than the appropriate form field type. Right now, I am thinking that this latter approach is the best option, although I may end up going the other way one day after I’ve had more time to contemplate all of the ramifications of that strategy. For now, I’ve added this line just under the lines above:

	htmlText += getMentionTemplate();

… and this function to provide the template:

function getMentionTemplate() {
	var htmlText = "      <script type=\"text/ng-template\" id=\"/at-mentions.tpl\">\n";
	htmlText += "        <div class=\"dropdown-menu sn-widget sn-mention\">\n";
	htmlText += "          <ul class=\"sn-widget-list_v2\">\n";
	htmlText += "            <li ng-if=\"items.length > 0 && !items[0].termLengthIsZero\" mentio-menu-item=\"person\" ng-repeat=\"person in items\">\n";
	htmlText += "              <div class=\"sn-widget-list-content sn-widget-list-content_static\">\n";
	htmlText += "                <sn-avatar primary=\"person\" class=\"avatar-small\" show-presence=\"true\"></sn-avatar>\n";
	htmlText += "              </div>\n";
	htmlText += "              <div class=\"sn-widget-list-content\">\n";
	htmlText += "                <span class=\"sn-widget-list-title\" ng-bind-html=\"person.name\"></span>\n";
	htmlText += "                <span class=\"sn-widget-list-subtitle\" ng-if=\"!person.record_is_visible\">Cannot see record</span>\n";
	htmlText += "              </div>\n";
	htmlText += "            </li>\n";
	htmlText += "            <li ng-if=\"items.length === 1 && items[0].termLengthIsZero\">\n";
	htmlText += "              <div class=\"sn-widget-list-content\">\n";
	htmlText += "                <span class=\"sn-widget-list-title sn-widget-list-title_wrap\">Enter the name of a person you want to mention</span>\n";
	htmlText += "              </div>\n";
	htmlText += "            </li>\n";
	htmlText += "            <li ng-if=\"items.length === 0 && items.loading && visible\">\n";
	htmlText += "              <div class=\"sn-widget-list-content sn-widget-list-content_static\">\n";
	htmlText += "                <span class=\"sn-widget-list-icon icon-loading\"></span>\n";
	htmlText += "              </div>\n";
	htmlText += "              <div class=\"sn-widget-list-content\">\n";
	htmlText += "                <span class=\"sn-widget-list-title\">Loading...</span>\n";
	htmlText += "              </div>\n";
	htmlText += "            </li>\n";
	htmlText += "            <li ng-if=\"items.length === 0 && !items.loading\">\n";
	htmlText += "              <div class=\"sn-widget-list-content\">\n";
	htmlText += "                <span class=\"sn-widget-list-title\">No users found</span>\n";
	htmlText += "              </div>\n";
	htmlText += "            </li>\n";
	htmlText += "          </ul>\n";
	htmlText += "        </div>\n";
	htmlText += "      </script>\n";
	return htmlText;
}

That does create the potential for more than one template appearing in the source code, but other than the obvious bloat, that doesn’t really hurt anything.

Now we have to deal with the functions, one to fetch the data and one to handle the selection. We will add these to the link section of the provider, starting with the one that fetches the data:

scope.searchMembersAsync = function(term) {
	scope.userSysId = window.NOW.user_id;
	scope.members = [];
	scope.members.loading = true;
	clearTimeout(scope.typingTimer);
	if (term.length === 0) {
		scope.members = [{
			termLengthIsZero: true
		}];
		scope.members.loading = false;
	} else {
		scope.typingTimer = setTimeout(function() {
			scope.snMention.retrieveMembers('sys_id', scope.userSysId, term).then(function(members) {
				scope.members = members;
				scope.members.loading = false;
			}, function () {
				scope.members = [{
					termLengthIsZero: true
				}];
				scope.members.loading = false;
			});
		}, 500);
	}
};

This script required two things that we brought in via the client script function arguments: snMention and $timeout. In this context, $timeout can be replaced with the standard window functions setTimeout and clearTimeout, so that eliminates the need for that one. The other requirement, though, snMention, is still going to have to come in via a client script function argument, and then passed into the scope. I don’t like doing that, as that is yet another thing that the person using this is going to have to know, but I couldn’t see any other way around it. That makes the client script in our example widget now start out like this:

function($scope, snMention) {
	var c = this;
	$scope.snMention = snMention;

The other script we will need is the one that handles the selection:

scope.selectAtMention = function(field, item) {
	if (!scope.mentionMap) {
		scope.mentionMap = {};
	}
	if (!scope.mentionMap[field]) {
		scope.mentionMap[field] = {};
	}
	if (item.termLengthIsZero) {
		return (item.name || "") + "\n";
	}
	scope.mentionMap[field][item.name] = item.sys_id;
	return "@[" + item.name + "]";
};

The major revision here is that we have added a field argument to the function call to allow for the fact that there could be more than one field on the form that contains @mentions. This way, mentions from one field are collected in a different object than mentions from another field. We also added checks for both the base mention map and the field-specific map so that they can be initialized as needed.

To test things out, I added a mention type form field to my form field test widget and gave it a whirl.

Form field test page with @mention field added

Everything seems to work, which is good, so I’ve bundled it all up into yet another Update Set just in case someone wants to dig into all of the details. I’m going to have to quit using the word final in relationship to this little form field experiment, as I keep finding new form field types to toss onto the pile; who knows what will pop up tomorrow …

More Service Portal Form Fields

“Code reuse is the Holy Grail of Software Engineering.”
Douglas Crockford

One of the thing that I like about making parts is that, even after you’ve “completed” your work and put a part on the shelf, you can always go back at some point later on and make it even better. When I first set out to create my form field tag, my primary goal was to save myself some work and to set things up so that things would always come out consistently. Consistency is a nice by-product of reusing the same component over and over again. People like it when things are consistent. So when I came across a need for a field type that I had not built into the current version of my form field tag, my first impulse was to pull it off the shelf, dust it off, and give it a bit of an upgrade.

What I needed was a feedback field, which is really a combination to two separate fields, a rating widget and a comments box. There are all kinds of rating widgets out there where you can configure stars or happy faces or some other graphic to indicate some level of satisfaction, but none of the existing field types in my current form field implementation supported that feature, and none of them included a single label for two input elements. What I wanted to do was to support something like this:

Example feedback entry

For the rating, I peeked under the hood of the Knowledge Article widget, and found the uib-rating tag, which looks like it comes from here. That looked like just the thing that I needed, so I all that was left for me to do was to wrap my labels and decorations around that widget in the same manner as I had for the sn-record-picker and sn-choice-list tags. The code for the stand-alone rating was pretty much just a copy, paste, and slightly modify:

if (type == 'radio' || type == 'inlineradio') {
	htmlText += buildRadioTypes(attrs, name, model, required, type);
} else if (type == 'select') {
	htmlText += buildSelect(attrs, name, model, required);
} else if (SPECIAL_TYPE[type]) {
	htmlText += buildSpecialTypes(attrs, name, model, required, type, fullName, label);
} else if (type == 'reference') {
	htmlText += "      <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
} else if (type == 'choicelist') {
	htmlText += "      <sn-choice-list sn-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-choice-list>\n";
} else if (type == 'rating') {
	htmlText += "      <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else ...

For the combination of a rating and a comment block, things got a little more complicated. In all of my other field types, I passed through to the input element all of the original attributes that did not have some other purpose in rendering out the entire block of code. For the first time, I had more than one input element, as I was combining the rating doodad with the textarea for the comments all under one label. After experimenting with different ways to distinguish attributes for one of the elements from attributes for the other, I decided against making things more complicated than they needed to be, and just assume that all non-standard attributes would be attributed to the textarea. Once that was settled, the resulting code turned to be the following:

...
} else if (type == 'rating') {
	htmlText += "      <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else if (type == 'textarea' || type == 'feedback') {
	if (type == 'feedback' && attrs.snhRatingModel) {
		var max = 5;
		if (attrs.snhRatingMax && parseInt(attrs.snhRatingMax) > 0) {
			max = parseInt(attrs.snhRatingMax);
		}
		htmlText += "      <uib-rating ng-model=\"" + attrs.snhRatingModel + "\" max=\"" + max + "\"/>\n";
	}
	htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
} else {
	htmlText += "      <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
}

As you can see, I did break down and allow for the max attribute by creating an snh-rating-max attribute that would not be passed in to the textarea, but other than that, the rating paired with the comments ends up basically unconfigurable. I may change that one day, but for now, this was all I needed to get me what I was after.

Anyway, I have done a little testing, and it all seems to work so far. If you would like to play around with it on your own, here is an Update Set with all of the relevant parts and pieces.

Fun with Highcharts

“Most of the important things in the world have been accomplished by people who have kept on trying when there seemed to be no hope at all.”
Dale Carnegie

Long before I had ever heard of ServiceNow, I stumbled across a cool little product called Highcharts. In their own words, “Highcharts is a charting library written in pure JavaScript, offering an easy way of adding interactive charts to your web site or web application.” ServiceNow comes bundled with Highcharts included, which I thought was pretty nifty when I figured that out, because you can do quite a bit with Highcharts with very minimal effort. In fact, that’s one of the things that I really like about both products: both allow you to accomplish quite a bit with just a very small investment of time and energy.

To make things even easier, it always helps to have a few ready-made parts at your disposal to speed things along. Highcharts uses simple Javascript objects to configure their charts, and the contents of those objects can be broken down into two rather distinct categories: 1) elements that control the type, look, and feel of the chart and 2) elements that contain the data to be presented in the chart. Once you design a particular chart with the style, colors, and appearance that you find acceptable, you can save that off and reuse it again and again with different data or data from different periods. For the ServiceNow Service Portal, my thought was to create a generic chart widget that could take any chart configuration object as a passed parameter, and then also create a generic chart configuration object generator that would take a chart type and chart data as parameters and return a chart configuration object that could be then passed to the generic chart widget.

To start things off, I decided to build a simple generic chart widget and pass it a hard-coded chart configuration object, just to prove that everything about the widget was functional. There are a lot of sample Highcharts chart objects floating around the Internet, but for my test I just went out to their Your First Chart tutorial page and grabbed the configuration object for that simple example.

Rendered Highchart as displayed on their tutorial page

To grab just the chart object, I copied the highlighted code from the page.

Chart configuration object (highlighted)

Unfortunately, this is not a valid JSON string, which I would need if I was going to pass this around as a parameter, but I could paste it into simple Javascript routine as the value of a variable, and print out a JSON.stringify of that variable and then I ended up with a usable JSON string.

{
   "chart":{
      "type":"bar"
   },
   "title":{
      "text":"Fruit Consumption"
   },
   "xAxis":{
      "categories":[
         "Apples",
         "Bananas",
         "Oranges"
      ]
   },
   "yAxis":{
      "title":{
         "text":"Fruit eaten"
      }
   },
   "series":[
      {
         "name":"Jane",
         "data":[1, 0, 4]
      },{
         "name":"John",
         "data":[5, 7, 3]
      }
   ]
}

With my new hard-coded chart specification object in hand, I set out to create my generic widget. Every Highchart needs a place to live, so I started out with the HTML portion and created a simple chart DIV inside of a basic panel.

<div class="panel panel-default">
  <div class="panel-body form-horizontal">
    <div class="col-sm-12">
      <div id="chart-container" style="height: 400px;"></div>
    </div>
  </div>
</div>

On the server side, I wanted to be able to accept a chart configuration object from either input or options, so I ended up with this:

(function() {
	if (input && input.chartObject) {
		data.chartObject = input.chartObject;
	} else {
		if (options && options.chart_object) {
			try {
				data.chartObject = JSON.parse(options.chart_object);
			} catch (e) {
				gs.addErrorMessage("Unable to parse chart specification: " + e);
			}
		}
	}
})();

On the client side, I also wanted to be able to accept a chart configuration object from a broadcast message from another widget, and I also needed to add the code to actually render the chart, so that turned out to be this:

function ($scope) {
	var c = this;
	if (c.data.chartObject) {
		var myChart = new Highcharts.chart('chart-container', c.data.chartObject);
	}
	if (c.options.listen_for) {
		$scope.$on(c.options.listen_for, function (event, config) {
			if (config.chartObject) {
				var myChart = new Highcharts.chart('chart-container', config.chartObject);
			}
		});
	}
}

One more thing to do was to set up the Option schema for my two widget options, which is just another JSON string.

[{"hint":"An optional JSON object containing the Chart specifications and data",
"name":"chart_object",
"default_value":"",
"section":"Behavior",
"label":"Chart Object",
"type":"string"},
{"hint":"The name of an Event that will provide a new Chart specification object",
"name":"listen_for",
"default_value":"",
"section":"Behavior",
"label":"Listen for",
"type":"string"}]

Finally, I had to go down to the bottom of the widget page where the related records tabs are found and Edit the dependencies to add Highcharts as a dependency. This will bring in the Highcharts code and make all of the magic happen.

That pretty much completes the widget, so now I just needed to create a page on which to put the widget so that I could test it out. Once I created the page, I was able to pull up the widget options using the pencil icon in the page designer and enter in my JSON object for the chart configuration.

Adding the JSON chart object as a widget option

At that point, all that was left was for me to click on the Try it button and look at my beautiful new chart. Unfortunately, all I got to see was a nice big blank chunk of whitespace where my chart should have appeared. Digging into the console error messages, I came across the following:

Highcharts not defined

Well, either my dependency did not load, or my widget couldn’t see the code. I tried $rootScope.Highcharts and $window.Highcharts and $scope.Highcharts, but nothing worked. Then I thought that maybe it was a timing issue and so I put in a setTimeout to wait a few seconds for everything to load, but that didn’t work either. So then I added a script tag to the HTML thinking that maybe the dependency wasn’t pulling in the script after all. Nothing.

At that point, I decided to give up just trying different things on my own and see if I could hunt down some other widget that might already be working with Highcharts and see how those were set up. From the Dependencies list, I used the hamburger menu next to the Dependency column label to select Configure > Table, and then on the Table page, I scrolled down to where I could click on Show list to bring up the list of dependencies. Then I filtered the list for Highcharts to find that there were quite a few existing widgets that pulled in Highcharts as a dependency.

The secret, it appeared, was some additional code in the Link section of the widget, a section that I have always ignored, mainly because I have never understood its purpose or how it worked.

function(scope, element, attrs, controllers) {
	scope.$watch('chartOptions', function() {
		if (scope.chartOptions) {
			$('#chart-container').highcharts(scope.chartOptions);
			scope.chart = $('#chart-container').highcharts();
		}
	});
}

Now this is all AngularJS mystical nonsense sprinkled with Highcharts fairy dust as far as I was concerned, because I have no idea what the Link section of a widget is for or what all of this code actually does. However, it is attached to widgets that actually work, so I got busy cutting and pasting. The other thing that I had to do based on the examples that I was examining was to modify my client-side code a little, which now looked like this:

function ($scope) {
	var c = this;
	if (c.data.chartObject) {
		$scope.chartOptions = c.data.chartObject;
	}
	if (c.options.listen_for) {
		$scope.$on(c.options.listen_for, function (event, config) {
			if (config.chartObject) {
				$scope.chartOptions = config.chartObject;
			}
		});
	}
}

Yes, we are now deep into the realm of Cargo Cult Programming, but I’ve never been too proud to copy other people’s working code, even if I didn’t understand it. Now let’s hit that Try it button one more time …

That’s better!

Well, look at that! I’m not sure how all of that works, but it does work, so I’m good. Now that I have my functioning generic chart widget, I can start working on my chart object generator and then see if I can’t wire the two of them together somehow …