Configurable Data Table Widget Content Selector, Revisited

“Learning from mistakes and constantly improving products is a key in all successful companies.”
Bill Gates

When I first conceived of my Configurable Data Table Widget Content Selector, my main focus was on creating a process that would read the JSON configuration file and turn those configuration rules into a functioning widget in accordance with the specifications. That was an interesting challenge that I had a quite a bit of fun with, but I started out by hard-coding the configuration object at the beginning of the widget and I never went back and set things up so that you could reuse the widget with a different configuration. Obviously, that’s not very friendly; now I need to go back in and set things right. The configuration object that you want to use should be an external parameter that gets passed into the widget via some external source such as a URL parameter or widget option. Let’s see if we can’t fix that right now.

I think that the first thing that I want to do is to create a base class for the configuration object script. That will do two things: 1) provide a common foundation of code for all of the configuration objects that you would like to build, and 2) provide a way to identify all of the qualifying scripts as all scripts that extend this particular base class. We’ll call it the ContentSelectorConfig:

var ContentSelectorConfig = Class.create();
ContentSelectorConfig.prototype = {

	initialize: function() {
	},

	getConfig: function(sp) {
		return {
			perspective: this.perspective,
			state: this.state,
			table: this.table
		};
	},

	type: 'ContentSelectorConfig'
};

With our base class established, I can now hack up my earlier configuration object and make it an extension of this new base class:

var MyTaskConfig = Class.create();
MyTaskConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [...],

	state: [...],

	table: {...},

	type: 'MyTaskConfig'
});

For now, I am just going to reuse the list of perspectives, states, and tables that I was using before and focus on the mechanics of making this configuration an external parameter rather than a hard-coded reference. Rather than make the changes to my original My Data widget, though, I decided to clone the widget, give it a new name, and leave the original widget as is. I called my new widget Content Selector, and it started out life as an exact copy of the My Data widget before I started to hack it up.

The first thing that I did was to add a new option to the Option Schema so that we could pass in the name of the ContentSelectorConfig that we want to use. We already had an existing option to display the widget content in a single row rather than a stacked block, so this was just a matter of adding a second option to the existing array of options.

[{
  "hint":"Mandatory configuration script that is an extension of ContentSelectorConfig",
  "name":"configuration_script",
  "section":"Behavior",
  "label":"Configuration Script",
  "type":"string"
},{
  "hint":"If selected, will display the widget content in a single row rather than a stacked block",
  "name":"display_inline",
  "default_value":"false",
  "section":"Presentation",
  "label":"Display Inline",
  "type":"boolean"
}]

Now that our new option has been defined, it’s time to rewrite the code that pulled in the hard-coded configuration object. Here is the original version of the first few lines of code in the server side script:

var mdc = new MyDataConfig();
data.config = mdc.getConfig($sp);
data.config.authorizedPerspective = getAuthorizedPerspectives();
establsihDefaults();
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
data.inline = false;
if (options && options.display_inline == 'true') {
	data.inline = true;
}

We are still going to want to establish the data.config object, but we are going to want to do it using an instance of the class named in our new Option. Last year, when I was working on my Static Monthly Calendar, I needed to turn a class name into an object of that class, and I built a little tool for that, which I called the Instantiator. We can use that same tool here to turn the name of our configuration script into an instance of that class that we can use to pull in the configuration. Here is the restructured code to start out our updated widget:

data.config = {};
data.inline = false;
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
if (options) {
	if (options.configuration_script) {
		var instantiator = new Instantiator();
		instantiator.setRoot(this);
		var configurator = instantiator.getInstance(options.configuration_script);
		data.config = configurator.getConfig($sp);
		data.config.authorizedPerspective = getAuthorizedPerspectives();
		establsihDefaults();
	}
	if (options.display_inline == 'true') {
		data.inline = true;
	}
}

Now we just need to throw it on a page with a Data Table widget, configure the options, and give it a spin. After dragging all of the widgets onto the page in the Page Designer, clicking on the pencil icon in the upper right-hand corner of our update widget will bring up the Options dialog where we can specify our configuration script.

Widget Options dialog

Once the widget Options have been specified, all we need to do is to pull up the page and see if things are still functioning as they should, which appears to be the case.

Testing the completed modifications

Well, that’s about all there is to that. Basically, it does exactly what it did before, but you can now specify the configuration script using the Widget Options instead of having it hard-coded in the script as it was in the original version. Here’s an Update Set with the modifications if you’d like to play around with it on your own.

Static Monthly Calendar, Part III

“Mistakes are the portals of discovery.”
James Joyce

While experimenting with a number of various configurations for my Static Monthly Calendar, I ran into a number of issue that led me to make a few adjustments to the code, and eventually, to actually build a few new parts that I am hoping might come in handy in some future effort. The first problem that I ran into was when I tried to configure a content provider from a scoped app. The code that I was using to instantiate a content provider using the name was this:

var ClassFromString = this[options.content_provider];
contentProvider = new ClassFromString();

This works great for a global Script Include, but for a scoped component, you end up with this:

var ClassFromString = this['my_scope.MyScriptInclude'];

… when what you really need is this:

var ClassFromString = this['my_scope']['MyScriptInclude'];

I started to fix that by adding code to the widget, but then I decided that it was code that would probably be useful in other circumstances, so I ended up creating a separate global component to turn an object name into an instance of that object. That code turned out to look like this:

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

	_root: null,

	setRoot: function(root) {
		this._root = root;
	},

	getInstance: function(name) {
		var instance;

		var scope;
		var parts = name.split('.');
		if (parts.length == 2) {
			scope = parts[0];
			name = parts[1];
		}
		var ClassFromString;
		try {
			if (scope) {
				ClassFromString = this._root[scope][name];
			} else {
				ClassFromString = this._root[name];
			}
			instance = new ClassFromString();
		} catch(e) {
			gs.error('Unable to instantiate instance named "' + name + '": ' + e);
		}

		return instance;
	},

	type: 'Instantiator'
};

This handles both global and scoped components, and also simplified the code in the widget, which turned out to be just this:

contentProvider = instantiator.getInstance(options.content_provider);

Another issue that I ran into was when I tried to inject content that allowed the user to click on an event to bring up some additional details about the event in a modal pop-up. I created a function called showDetails to handle the modal pop-up, and then added an ng-click to the enclosing DIV of the HTML provided by my example content provider call this new function. Unfortunately, the ng-click, which was added to the page with the rest of the provided content, was inserted using an ng-bind-html attribute, which simply copies in the raw HTML and doesn’t actually compile the AngularJS code. I tried various approaches to compiling the code myself, but I was never able to get any of those to work. Then I came across this, which seemed like just the thing that I needed. I thought about installing in in my instance, but then I thought that I had better check first, because it’s entirely possible that it is already in there. Sure enough, I came across the Angular Provider scBindHtmlCompile, which seemed like a version of the very same thing. So I attached it to my widget and replaced by ng-bind-html with sc-bind-html-compile.

Unfortunately, that just put the compiler into an endless loop, which ultimately resulted in filling up the Javascript console with quite a few of these error messages:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

I searched around for a solution to that problem, but nothing that I tried would get around the problem. I ended up going in the opposite direction and swapping out the ng-click for an onclick, which doesn’t need to be compiled. Of course, the onclick can’t see any of the functions inside the scope of the app, so I had to write a stand-alone UI Script to include with a script tag in order to have a function to call. That function is outside of the scope of the app as well, so I ended up turning the script into yet another generic part that uses the element to get you back to the widget:

function functionBroker(id, func, arg1, arg2, arg3, arg4) {
	var scope = angular.element(document.getElementById(id)).scope();
	scope.$apply(function() {
		scope[func](arg1, arg2, arg3, arg4);
	});
}

You pass it the ID of your HTML element, the name of function that is in scope, and up to four independent arguments that you would like to pass to the function. It uses the element to locate the scope, and then uses the scope to find your desired function and passes in the arguments. After saving the new generic script, I went back into the widget and added a script tag to the widget’s HTML to pull the script onto the page.

<script type="text/javascript" src="/function_broker.jsdbx"></script>

Then I added a function to pop open a modal dialog based on a configuration object passed into the function.

$scope.showDetails = function(modalConfig) {
	spModal.open(modalConfig);
};

Now, I just needed something to pop up to see if it all worked. Not too long ago I made a simple widget to show off my rating type form field, and that looked like a good candidate to use just to see if everything was going to work out the way that it should. I pulled up the ExampleContentProvider that I created earlier, and added one more event in the middle of the month that would bring up this repurposed widget when clicked.

if (dd == 15) {
	response += '<div class="event" id="event15" onclick="functionBroker(\'event15\', \'showDetails\', {title: \'Fifteenth of the Month Celebration\', widget:\'feedback-example\', size: \'lg\'});" style="cursor: pointer;">\n';
	response += '  <div class="event-desc">\n';
	response += '    Fifteenth of the Month Celebration\n';
	response += '  </div>\n';
	response += '  <div class="event-time">\n';
	response += '    Party Time\n';
	response += '  </div>\n';
	response += '</div>\n';

The whole thing is kind of a Rube Goldberg operation, but it should work, so let’s light things up and give it a try.

Modal pop-up from clicking on an Event

After all of the failed attempts at making this happen, it’s nice to see the modal dialog actually appear on the screen! It still seems like there has got to be a simpler way to make this work, but until I figure that out, this will do. If you’d like to play around with it yourself, here’s an Update Set that I hope includes all of the right pieces. There are still a few little things that I would like to add one day, so this may not quite be the last you will see of this one.

Static Monthly Calendar, Part II

“Life affords no higher pleasure than that of surmounting difficulties, passing from one step of success to another, forming new wishes and seeing them gratified.”
Samuel Johnson

Now that we have the empty shell of a static monthly calendar, we can focus our attention on the integration of dynamic, use-specific content. While I have visions of occupying the entirety of the screen real estate devoted to any particular day with colors and graphics and any content that can be conceived by the provider, for now, I am going to stick with the content model that accompanied the template and focus on the mechanics of integrating the content with the calendar. At this juncture, I will leave it to the reader or some future installment to address the full potential of nature of the content, and will instead, devote this writing to the method used to deliver the content to the calendar.

My approach to integrating user-specified content with the generic calendar shell is to utilize a user-defined content provider, which is simply a Script Include that implements a specific function that takes a date as an argument and returns the content in HTML format. To create a simple example, I used the existing sample content from the original template and created a simple script that I called ExampleContentProvider:

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

	getContent: function(yy, mm, dd) {
		var response = '';

		if (dd == 2) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Career development @ Community College room #402\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    2:00pm to 5:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 7) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Group Project meetup\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    6:00pm to 8:30pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 14) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Board Meeting\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    1:00pm to 3:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 22) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Conference call\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    9:00am to 12:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 25) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Conference Call\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    1:00pm to 3:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		}

		return response;
	},

	type: 'ExampleContentProvider'
};

A real content provider would, of course, use some database table or outside source to obtain the raw data from which the HTML would then be generated, but that’s pretty standard stuff that we don’t really need to get into here. Our ExampleContentProvider is simply a means to demonstrate the process of merging the dynamic content with the static calendar. How and from where you obtain your actual content is clearly up to you, but the method that we have implemented will send your process a year, a month, and a day, and will expect your process to send back the appropriate HTML for that specific day in return. You can see the process in action from these new lines in the server-side code:

if (contentProvider && contentProvider.getContent) {
	thisDay.content = contentProvider.getContent(data.year, data.month, thisDay.date);
}

The content provider is optional, so we need to check to make sure that it is there, and we also need to check to make sure that it has the required getContent method, but if all is well in that regard, we set the content for the day to the HTML returned by getContent method of the configured content provider. On the HTML side of things, then, we bind the HTML to the day using an ng-if and an ng-bind-html attribute.

<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>
    <div ng-if="d.content" ng-bind-html="d.content"></div>
  </li>
</ul>

All we need to do now is to get the user-provided content provider passed into the calendar widget somehow. This we can do with a widget option, and to make that happen, we need to add an Option Schema:

[{
    "hint": "Class Name of Script Include that provides content",
    "name": "content_provider",
    "default_value": "",
    "label": "Content Provider",
    "type": "string"
}]

Once the Option Schema is in place, clicking on the pencil icon of the widget while in the Portal Page Designer will open up the Options dialog where you can now enter the name of your content provider:

Specifying the content provider on the widget options dialog

Once specified as an option, you can pick it up in the server-side code:

if (options && options.content_provider) {
	var contentProviderName = options.content_provider;
}

Of course, you really aren’t interested in the name of the content provider … you want an actual instance of the Script Include that you can use to invoke the getContent method. For that, we need to instantiate the object using the name:

var contentProvider;
if (options && options.content_provider) {
	try {
		var ClassFromString = this[options.content_provider];
		contentProvider = new ClassFromString();
	} catch(e) {
		gs.error('Unable to instantiate Content Provider named "' + options.content_provider + '": ' + e);
	}
}

Don’t ask me to explain how all of that works, but I can tell you that it does, in fact, work. If you provide the name of an actual Script Include, it will create a usable instance of it, If you provide anything else, it logs an error, and then it goes on as if you never attempted to configure a content provider.

In addition to specifying the content provider via the Portal Page Designer, you can also specify it when including the calendar widget in another widget. In that case, the code would look something like this:

data.calendarWidget = $sp.getWidget('view-only-monthly-calendar', {content_provider: 'ExampleContentProvider'});

Either way, you have to grab the name from the options and instantiate an actual object before you can use it. Well, that’s just about it for the changes, so the next thing to do will be to bring it up and see how it all works.

Calendar with content provided by external content provider

Not bad! You will obviously want to make your own content provider and get your content from the applicable source, but the calendar widget itself should work for just about any content from any source. There are still a few cosmetic tweaks that I wouldn’t mind doing here and there, but it works, so that’s good enough for now. For those of you who might be interested, here is an Update Set.

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.