Saturday, June 07, 2008

Writing An Exchange Rates Google Gadget

There are many exchange rate gadgets for iGoogle, but i wanted one as small and simple as possible, so i decided to write my own. There's a lot of useful documentation on Google Gadgets Developer website. Data is retreived from the European Central Bank website. Beware that several features (like settings) aren't available in the embedded version.



You can use any editor to write a Google Gadget, but the Google Editor Gadget is very useful. You should also use the Google Developer Gadget to easily add a gadget and disable caching for it. Here is my iGoogle shared tab with these two gadgets.

A gadget consists of some xml, some html and some javascript to do the work. A Hello World gadget looks like this:


<?xml version="1.0" encoding="UTF-8"?>
<Module>
<ModulePrefs title="hello world example" />
<Content type="html"><![CDATA[
Hello, world!
]]></Content>
</Module>


Module parameters, such as the title, are declared in the "ModulePrefs" tag. If the gadget requires special features, they should be declared here. I wanted my gadget to adjust its height according to the length of the content, so I requested the "dynamic-height" feature.


<ModulePrefs title="__MSG_mp_title__" title_url="http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html"">
<Require feature="dynamic-height"/>
<Locale messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.ALL_ALL.xml"/>
<Locale lang="ro" messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.ro_ALL.xml"/>
<Locale lang="de" messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.de_ALL.xml"/>
</ModulePrefs>


Beside English, I added localization for German and Romanian. The language used depends on the language set in iGoogle. If the iGoogle language is not one of these two, it will fallback to the default language, English. The structure of a localization file is very simple:


<messagebundle>
<msg name="mp_title">Euro Wechselkurs (EZB)</msg>
<msg name="up_currencyCodes">Währungs-Abkürzung</msg>
<msg name="up_numberOfColumns">Anzeige auf # Spalten</msg>
<msg name="invalid_data">Ungültige Daten.</msg>
<msg name="on">Am</msg>
</messagebundle>


The "name" attribute of the "msg" element can be used in the gadget to retrieve the value of the element (here the German translation of the strings I used).


<UserPref name="currencyCodes" display_name="__MSG_up_currencyCodes__" datatype="list" default_value="USD|GBP|JPY|CAD" />
<UserPref name="numberOfColumns" display_name="__MSG_up_numberOfColumns__" default_value="2" datatype="enum" >
<EnumValue value="1" />
<EnumValue value="2" />
<EnumValue value="3" />
<EnumValue value="4" />
<EnumValue value="5" />
<EnumValue value="6" />
<EnumValue value="7" />
<EnumValue value="8" />
</UserPref>


These are the settings for my gadget. I used a list setting to store the currencies the user wants to display, and an enum setting to store the number of columns the data should be displayed on. I initiliazed both settings with a default value. The items in a list are separeated with a "|" character. You can retrieve this settings both as an array or as a string. Strings like __MSG_up_numberOfColumns__ will be replaced with the appropriate message from the localization files.

All the html and javascript goes into the "Content" element. The type attribute of the Content element can can be "url" or "html". If you want to create a gadget from a webpage you should set the attribute to "url". I speciefied the content of my gadget inline, so I set "html". All the html and javascript should be written in a CDATA section, to avoid html tags screwing your xml.


<script type="text/javascript">
function receivedData(response)
{
//do something
}

function requestData()
{
var url = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml";
_IG_FetchXmlContent(url, receivedData);
}

_IG_RegisterOnloadHandler(requestData);
</script>


The content_div element will act as a container for the output. _IG_RegisterOnloadHandler is self-explaining: it registers a function to run when the module loads. _IG_FetchXmlContent loads asynchronously a XML file from the European Central Bank website. It registers the function receivedData to run when the data arrives. These functions are written in the reverse order because unlike languages like C#, in javascript you must declare a function before using it.


function receivedData(response)
{
//get settings
var prefs = new _IG_Prefs(__MODULE_ID__);
var currencyCodesArray = prefs.getArray("currencyCodes");
var currencyCodesString = prefs.getString("currencyCodes");
var useTwoColumns = prefs.getBool("useTwoColumns");
var numberOfColumns = prefs.getInt("numberOfColumns");


When data arrives error checking should be the first thing to do. But since we support more languages, we should get the settings to be able to provide the error message in the correct language. Settings are initialized with a call to new _IG_Prefs(__MODULE_ID__). I read the currencyCodes list both as a an array and a string.


//check for errors
if (response == null || typeof(response) != "object" || response.firstChild == null)
{
_gel("content_div").innerHTML = "<em>" + prefs.getMsg("invalid_data") + "</em>";
return;
}


Some basic error checking: if data isn't available, notify the user and return. Localized messages are retrieved using the getMsg function of the preferences object.


//get data
var exchangeRatesDate = response.getElementsByTagName("gesmes:Envelope").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube").item(0).getAttribute("time");
var itemList = response.getElementsByTagName("gesmes:Envelope").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube");
var rowsArray = new Object();
for (var i = 0; i < itemList.length; i++)
{
var currencyCode = itemList.item(i).getAttribute("currency");
if(currencyCodesString.search(currencyCode) != -1)
{
rowsArray[currencyCode] = new Object();
rowsArray[currencyCode]["code"] = itemList.item(i).getAttribute("currency");
rowsArray[currencyCode]["rate"] = itemList.item(i).getAttribute("rate");
rowsArray[currencyCode]["mult"] = itemList.item(i).getAttribute("multiplier") != null ? currencyNode.getAttribute("multiplier") : "1";
}
}


The data is retrieved from the received XML. For each currency item in the XML, we test if the user has added it in his settings. That is why I retrieved the currencyCodes list as both an array and a string: it's easier to search in a string than iterating through an array. If the currency is found in that string, a new associative array with information like the currency code, the exchange rate and the multiplicator is added in as an element in another associative array, the key being the currency code.


//sort data
var sortedRowsArray = new Array();
for(var i = 0; i < currencyCodesArray.length; i++ )
if(rowsArray[currencyCodesArray[i]] != undefined)
sortedRowsArray.push(rowsArray[currencyCodesArray[i]]);


What happens if the user adds a wrong currency code in his list? We iterate through the currencyCodesArray and test if the rowsArray contains information about the currencies. Beside that, I wanted to 'sort' the currencies in the same order they were added.


//output html
var html = "";
html += "<span class='head'><span class='date'> " + prefs.getMsg("on") + " " + exchangeRatesDate + " </span>";
html += "<img class='flag' alt='EUR flag' src='http://1984.hosting.googlepages.com/EUR.png'> 1 EUR <span class='date'>=</span></span>";
html += "<table class='currencyTable' align='center' summary='Exchange Rates'>";
for(var i = 0; i < sortedRowsArray.length; i++)
{
if(i % numberOfColumns == 0) html += "<tr>";
html += "<td><img class='flag' alt='' src='http://1984.hosting.googlepages.com/" + sortedRowsArray[i]["code"] + ".png'></td>";
html += "<td class='number'>" + sortedRowsArray[i]["mult"] * sortedRowsArray[i]["rate"] + "</td>";
html += "<td>" + sortedRowsArray[i]["code"] + "</td>";
if(i % numberOfColumns == numberOfColumns - 1 || i == sortedRowsArray.length - 1) html += "</tr>";
else html += "<td> </td>";
}
html += "</table>";

_gel("content_div").innerHTML = html;
_IG_AdjustIFrameHeight();
}


Finally, the code handling the HTML output. Because most people will watch less than 4-5 currencies, it would be a waste of space on your homepage to leave the gadget 200px high. A call to _IG_AdjustIFrameHeight will shrink the gadget to an optimum height (remember i requested the dynamic height feature).

If you want to have a look at the code, here it is.

If you like, you can add the gadget to your iGoogle page by clicking on this button. Add to Google