JavaScript Walkthrough #1: Gadget
| Note: the following two sections do a basic walkthrough of JavaScript code from two files. This code is annotated much more extensively in the source files available with the gadget download. |
As one might imagine, things got more complicated when it came to the JavaScript. The primary JavaScript file, dnetgadget.js, has two primary purposes:
- Use some Ajax functionality to read the RSS feed and populate the headline elements
- Handle navigation events
In addition, the initial function needs to do some Gadget-related housekeeping, such as hooking up the flyout and settings pages. For example, in setup() I have:
System.Gadget.Flyout.file = 'flyout.html';
This tells the Gadget to use its built-in flyout functionality, with very cool Gadget shrinkage, on my flyout.html file. Similarly,
System.Gadget.settingsUI = 'settings.html';
sets the built-in Settings dialogue to display my settings.html file. One more line specifies the event handler to trigger when the user closes the Settings panel. More on that in a moment.
System.Gadget.onSettingsClosed = settingsClosed;
Because the feature spec for this particular Gadget includes category and subject filtering, as well, this code required some additional functionality to process choices made on the Settings page. As a result, the code checks for existing values for both "category" and "subject". Note that the System.Gadget class provides a static Settings object for storage of Gadget-wide variables, which also assists in maintaining state between the various Gadget pages and instantiations of the Gadget. Undeclared variables default to empty strings rather than nulls, so in my setup() function, I also do a quick check:
if (System.Gadget.Settings.read('category') == '')
{
System.Gadget.Settings.write('category','All');
}
if (System.Gadget.Settings.read('subject') == '')
{
System.Gadget.Settings.write('subject','All');
}
Once the setup finishes, routine Ajax code starts up the http object, pulling headlines from an RSS syndication url provided by DevX:
try {
titles = new ActiveXObject('Msxml2.XMLHTTP');
url = 'http://services.devx.com/outgoing/destinationNetCategorized.xml';
titles.open('GET', url, true);
titles.onreadystatechange = catchRSSFeed;
titles.send(null);
} catch (err) {
headline.innerHTML = 'Could not open RSS feed. (Error: ' + err.Description + ')';
titles = null;
}
For handling the category and subject settings, I decided to go with client-side filtering, which offered the performance benefit of doing a single http call, rather than server-side filtering, which would require new calls every time new filtering options were chosen. To handle this kind of filtering, I chose to create an array of indices that referenced members of the http object holding the headlines. This alleviated the overhead of duplicating heavy and redundant http objects or multi-dimensional arrays.
function filterHeadlines() {
headline.innerHTML = 'Filtering headlines...';
System.Gadget.Flyout.show = false;
filtered = new Array();
thisFItem = -1;
var currCat = System.Gadget.Settings.read('category');
var currSub = System.Gadget.Settings.read('subject');
for (var filtNum=0; filtNum<items.length; filtNum++)
{
if (((currCat == 'All') ||
(items[filtNum].selectSingleNode('categories').text.indexOf(currCat,0) >= 0)) &&
((currSub == 'All') ||
(items[filtNum].selectSingleNode('categories').text.indexOf(currSub,0) >= 0)))
{
thisFItem++;
filtered[thisFItem] = filtNum;
}
}
if (thisFItem == -1)
{
headline.innerHTML = 'No headlines available based on the selected filters.';
navplace.innerHTML = '0 of 0';
}
// Otherwise, reset to the first available headline and display it.
else
{
thisFItem = 0;
thisItem = filtered[thisFItem];
updateHeadline();
}
}
When flipping through headlines, I decided to keep things simple. Because of the filtering technique, navigation would actually affect the filtered list, and then populate page elements based on the original content object.
function next() {
if ((filtered != null) && (filtered.length > 0))
{
if (thisFItem < filtered.length-1)
{
thisFItem++;
}
else
{
thisFItem = 0;
}
thisItem = filtered[thisFItem];
updateHeadline();
}
}
function prev() {
if ((filtered != null) && (filtered.length > 0))
{
if (thisFItem > 0)
{
thisFItem--;
}
else
{
thisFItem = filtered.length-1;
}
thisItem = filtered[thisFItem];
updateHeadline();
}
}
Updating the headline thus becomes a simple affair:
function updateHeadline() {
System.Gadget.Flyout.show = false;
headline.innerHTML = items[thisItem].firstChild.text;
navplace.innerHTML = (thisFItem + 1) + ' of ' + filtered.length;
}
Because I chose to combine flyout and main functionality in the same JavaScript source, this code includes the flyout bits. This type of flyout is really simple. When the user clicks the headline, the flyout() function writes relevant data to the Settings object, then shows the flyout. Since flyout.html catches the onload event of the body, this triggers the next function, buildFlyout(), which sets the flyout dimensions and reads those Settings variables into the page elements. Download the Gadget and take a look inside to see what I mean.
Remember how the setup() function assigns an event handler to System.Gadget.onSettingsClosed? I finish the file with that handler, which demonstrates how to catch the Ok and Cancel click action on the pre-built Settings panel. Note that this is part two of a sequence that kicks off in the JavaScript for the Settings page, which is described next.
function settingsClosed(event)
{
if(event.closeAction == event.Action.commit)
{
filterHeadlines();
}
else if (event.closeAction == event.Action.cancel)
{
}
}