Update 19 May 2012: Updated source code for the demo to Dart Editor Build 7552 after finding that the previously compiled js file no longer ran under newer releases of Chrome. The following minor changes were required to the source code in the article to perform the update and generate a new js file:
_feedCont = window.document.query('#feedContainer');
// becomes
_feedCont = document.query('#feedContainer');
// and
xhr = new XMLHttpRequest.getTEMPNAME(feedURL, (jsonRequest)
//is now
xhr = new XMLHttpRequest.get(feedURL, (jsonRequest)
Introduction
A few weeks ago, I let myself get distracted from a project I was working on thinking about Dart's [1] jQuery like DOM manipulation features [2]. It occurred to me that as a quick experiment, I could take the RSS/Atom feed for this blog and see how well I could manipulate the returned XML document using Dart's very nice DOM API. I assumed I could simply use the following standard XHR request to return the feed:
//AJAX request for an XML Document.
XMLHttpRequest RSSRequestObject;
void getRssFeed() {
RSSRequestObject = new XMLHttpRequest();
RSSRequestObject.open("GET", rssFeedURL, true);
RSSRequestObject.on.readyStateChange.add((e) {
reqChange();
});
RSSRequestObject.send(null);
void reqChange() {
if (RSSRequestObject.readyState == 4 && RSSRequestObject.status == 200) {
Document feedDocument = RSSRequestObject.responseXML;
} else {
print(RSSRequestObject.statusText);
}
}
Well, this didn't actually work. After some experimentation and head scratching, it appeared that the XML parser for AJAX requests had not yet been implemented in Dart when I attempted this [3] (note that this bug appears to relate only to library dart:html). But rather than return to the project I was working on, I was now curious to find an alternative approach, and of course that led me to use JSON (Java Script Object Notation) instead. The final Dart widget (a Didget, if you will), which makes use of AJAX (Asynchronous Javascript and XML), JSON and a bit of CORS (Cross Origin Resource Sharing) is included below and you can find the source code here. If you are interested in a walk through of the code, you can find that included after the demo.
The "Didget"
My quick attempt, then, to parse an XML document and manipulate the data with Dart's DOM API (which some may say is not an appropriate use of the API anyway) turned instead into a tabbed panel widget that pulled in JSON data from both this blog as well as my Github account. I decided to use CORS instead of JSON-P for cross-domain data access although it is apparently possible to communicate between JavaScript and Dart to access JSON-P data using the postMessages() method of the DOM [4][5]. I was therefore limited to sites where CORS was actually implemented, and after some investigation, found that the Github v3 API has a very convenient method for accomplishing this [6], especially if all you desire is to retrieve publicly available information (although, oddly, their v3 API doesn't seem to support search queries the way v2 did). I had hoped that Google+ and Twitter would also allow cross-origin access with XHR2 (ie, using the CORS protocol), but this does not appear to be the case [7].
Using JSON with Dart
Once I had decided to abandon XML in place of JSON, the first issue I ran into was a fairly straight forward one - this blog doesn't actually generate a JSON feed, at least not natively. Luckily, I was able to find a 3rd party tool [8] built on a RESTful API that provided the feed in JSON format. An example of retrieving the feed using this API is as follows:
http://www.greatandlittle.com/studios/index.php?rest/blog&f=getPosts&cat_url=Google/Dart
Oddly enough, this didn't actually work either. But at least this time I could tell immediately from the console that the JSON parser was finding problems with the syntax being returned. Using this path with the JSONLint validator [9] confirmed that there were in, fact, syntax errors. The errors were confined to the content field in some of the entries (particularly the very long ones). Feeling the pressure of my long neglected other projects, I decided the best way to move forward was to filter the entries individually since trying to capture all of the entries in one request would likely result in not getting any at all. The getPosts method of the API has three query parameters, count_only, limit and offset, that allows me to step through the feed entries and skip the ones that are returning invalid JSON. So first I needed to get the total number of entries for a given entry category which is easy enough (note that I am using the '+' operator for string concatenation. For performance reasons, Dart doesn't recommend this approach and it is possible in the future the '+' operator may not be supported by the language, at least for concatenating string literals [10][11]):
//Retrieve number of entries in the given category:
void JSONFeedReader() {
feedURL = "http://www.greatandlittle.com/studios/index.php?rest/" +
"blog&f=getPosts&cat_url=Google/Dart&count_only=1";
num totalEntries;
XMLHttpRequest xhr = new XMLHttpRequest.getTEMPNAME(feedURL, (jsonRequest) {
var jsonResponse = JSON.parse(jsonRequest.responseText);
totalEntries = jsonResponse["data"];
iterateEntries(totalEntries);
});
}
Note I am using the getTEMPNAME() method of the XMLHttpRequest Interface in the dart:html library [12]. As the name suggests, this method is still under development and may change in future releases, so it is recommended that you check the DOM API documentation for updates. Currently, this is how it is implemented in Dart Editor [13] build 4577. Ok, so now that we have the total number of entries, we can iterate through each one. At first, I tried something like this (note that I'm using the loop iterator to control the offset in the URL path and $feedLimit is set equal to 1 to return only one post at a time):
//Iterate through each entry and skip those with invalid JSON data:
void iterateEntries(num totalEntries) {
XMLHttpRequest xhr;
var jsonResponse;
HeadingElement titleHeading;
for (var i=0; i<totalEntries; i++) {
feedURL = "http://www.greatandlittle.com/studios/index.php?rest/" +
"blog&f=getPosts&cat_url=Google/Dart&offset=$i&limit=$feedLimit";
xhr = new XMLHttpRequest.getTEMPNAME(feedURL, (jsonRequest) {
try {
jsonResponse = JSON.parse(jsonRequest.responseText);
titleHeading = new Element.tag("h2");
titleHeading.text = jsonResponse["data"][0]["title"];
_output.nodes.add (titleHeading);
processResponse(jsonResponse);
} catch (var exception) {
print("invalid json: $exception<br>");
}
});
}
}
We'll look at the processResponse() function in just a moment, but first notice that this approach has an unacceptable weakness to it. It filters invalid responses fine, but due to the asynchronous nature of the AJAX request, the order in which the valid responses are processed is unpredictable. The for loop continues to execute XMLHttpRequests in parallel with the parsing of the JSON data by the browser. Therefore, there is no set order in how the entries are displayed in the reader and the order may (and often will) change each time the feed is loaded. To correct this, we replace our for loop with an if statement to enable us to process each entry prior to moving on to the next. Note that this results in a recursive loop if the data is found to be invalid (otherwise, iterateEntries() is called from within the processResponse() function):
//Iterate through each entry but process the valid entries before requesting the next entry:
void iterateEntries() {
XMLHttpRequest xhr;
var jsonResponse;
if(totalEntries > 0) {
totalEntries--;
feedOff++;
feedURL = "http://www.greatandlittle.com/studios/index.php?rest/" +
"blog&f=getPosts&cat_url=Google/Dart&offset=$feedOff&limit=$feedLim";
xhr = new XMLHttpRequest.getTEMPNAME(feedURL, (jsonRequest) {
try {
jsonResponse = JSON.parse(jsonRequest.responseText);
processResponse(jsonResponse);
} catch (var exception) {
print("invalid json: $exception");
//Recursive call to try again:
iterateEntries();
}
});
} else {
//Done with blog entries, on to github.
githubFeed();
}
}
When we look later at using XHR2 with our gitHub requests, you'll see that we replace our try-catch error exception handling methodology with an actual error handler attached to the XHR request, but for now this approach seemed to work quite well. Before we discuss the processResponse() function, however, it might help to take a look at the JSON that is returned for one of the valid entries:
{
"data": [
{
"id": "16",
"title": "Keeping Time with Dart - The Clock, Stopwatch and Date Classes",
"excerpt": "<p><em>All that really belongs to us is time...
"content": "<h4>The Clock Class</h4>\n\n\n<p>The <code>...
"user": "user6488972b",
"creadt": "2011-12-12 05:52:24",
"upddt": "2012-01-13 22:09:17",
"url": "2011/12/11/Keeping-Time-with-Dart-Clock-Stopwatch-and-Date-Classes",
"selected": false,
"nb_comments": 0,
"nb_trackbacks": 0,
"category": 8
}
],
"status": "ok"
}
In our processResponse() function, we can then access the individual JSON fields using bracket notation:
//Received a valid entry, use bracket notation to gather and post the information desired:
void processResponse(var jsonResponse) {
HeadingElement titleHeading = new Element.tag("h2");
String link = "http://www.greatandlittle.com/studios/index.php?post/" +
"${jsonResponse["data"][0]["url"]}";
titleHeading.text += "${jsonResponse["data"][0]["title"]}";
_blogOut.nodes.add(titleHeading);
_blogOut.innerHTML += "${jsonResponse["data"][0]["excerpt"]}";
_blogOut.innerHTML += "<a href='$link'>read full entry</a><br>";
_blogOut.innerHTML +=
"Entry created ${jsonResponse["data"][0]["creadt"]}<br>";
_blogOut.innerHTML +=
"Entry modified ${jsonResponse["data"][0]["upddt"]}<br>";
//Finished with this entry, try the next one:
iterateEntries();
}
At this point, I had (sort of) completed what I had set out to do originally and should have really gotten back to my other projects. But the nagging question that had developed during all of this was - this is nice, but what about if I was interested in data from a different domain? How would I go about retrieving and displaying it with Dart?
JSON-P is Great, but CORS is Even Better
The fundamental limitation of XHR, of course, is that its security model limits access to data only on the same domain as that of the requesting agent (known as the same origin policy) [14]. By far the most common method around this limitation (and the most supported) is by using JSON-P (JSON with padding) [15]. JSON-P does not use an AJAX request at all but rather makes use of a callback function which is called when the JSON data arrives from the content provider. This is typically handled with a library like jQuery, but it can also be handled manually as follows:
//Dynamically add a script tag to your DOM that calls a JSONP enabled provider. For example:
<script type="text/javascript" src="http://search.twitter.com/search.json?q=blue%20angels&callback=myCallbackHandler"></script>
//Now pass the returned data to the function that actually makes use of the data.
function myCallbackHandler(data) {
jsonpData(data);
}
A Dart specific example of the above is shown in [4]. But as I mentioned earlier, I was looking more for something I could do completely within Dart. Certainly there had to be another way. And that other way is to use CORS.
CORS (Cross-Origin Resource Sharing) [16] is a protocol that can be used with AJAX directly to allow cross-domain data sharing. Most modern browsers support the protocol, although Mozilla suggests that you check for browser support [17]. Unfortunately, it is not widely supported [7] by API/Client providers. One exception, however, is GitHub, which provides CORS support through version 3 of its API [6]. Once you register your domain, assuming you have an account, access to public data in your account is available through standard AJAX requests using XHR2. For example:
//Define a path to the github repos:
githubUser = "scribeGriff";
githubURL = "https://api.github.com/users/$githubUser/watched";
//Request the data using standard AJAX
void githubFeed() {
var jsonResponse;
githubXHR = new XMLHttpRequest();
//Check if browser supports CORS:
if (githubXHR.withCredentials != null) {
githubXHR.open("GET", githubURL, true);
githubXHR.on.load.add((e) {
jsonResponse = JSON.parse(githubXHR.responseText);
processGits(jsonResponse);
});
githubXHR.on.error.add((e) {
_githubOut.innerHTML += "There was an error loading this feed.<br>";
});
githubXHR.send(null);
} else {
_githubOut.innerHTML += "This browser doesn't appear to support XHR2";
}
}
Processing the returned JSON formatted data is done in much the same way that we handled the blog feed. JSONLint showed us the structure of the data and the rest was just simple bracket notation manipulation. Note that I have started making use of multi-line string literals, a nice feature of Dart, in the next block of code. But be aware that this technique can insert (ie, capture) spaces in your string if you decide to introduce them for formatting purposes. For this example, the added spaces are added only to programmatically constructed HTML and are therefore ignored by the DOM. This may not be the case for other types of string literals.
//Take the JSON formatted response and process using bracket notation:
void processGits(var jsonResponse) {
Date creationTime = new Date.now();
HeadingElement titleHeading = new Element.tag("h2");
titleHeading.text =
"$githubUser is watching ${jsonResponse.length} repositories:";
_githubOut.nodes.add(titleHeading);
HeadingElement dateHeading = new Element.tag("p");
dateHeading.text = "data is current as of $creationTime";
_githubOut.nodes.add(dateHeading);
//Loop through all responses, creating a new div for each entry.
for (var i = 0; i < jsonResponse.length; i++) {
DivElement _reposDiv = new Element.tag("div");
_reposDiv.attributes = ({
"class": "reposClass"
});
_githubOut.nodes.add(_reposDiv);
//Multi-line strings use triple single or double quotes.
_reposDiv.innerHTML +=
'''
<a href='${jsonResponse[i]["owner"]["url"]}'><img src=
'${jsonResponse[i]["owner"]["avatar_url"]}'></a>''';
_reposDiv.innerHTML +=
'''
<a href='${jsonResponse[i]["html_url"]}'>
${jsonResponse[i]["description"]}</a><br>''';
_reposDiv.innerHTML +=
"Coded by ${jsonResponse[i]["owner"]["login"]}<br>";
_reposDiv.innerHTML +=
"Last updated at ${jsonResponse[i]["updated_at"]}<br>";
_reposDiv.innerHTML +=
"This repo has ${jsonResponse[i]["watchers"]} watchers<br>";
}
}
The Tabbed Panel in Dart
So I had these two divs that contained some of the dynamic content that I wanted for my Dart widget, but I didn't want to present it as just two consecutive blocks of information. This didget, however, lended itself quite nicely to a tabbed panel approach. If you use a library like jQuery, you have a lot of choices for building a large variety of tabbed panels. But even without a library, creating tabbed panels is not that difficult. Some solutions try to do tabbed panels with just CSS [18], although with mixed results. For this application, I'm using a fairly simple but easily extensible algorithm [19] that makes use of display style in an interesting way. First, to create our tabs in Dart, we create two divs for each tab (one for focus, the other for ready) and set its display property accordingly:
//Create Tab One Focus Div:
_tabOneFocus = new Element.tag("div");
_tabOneFocus.attributes = ({
"id": "tabOneFocus",
"class": "tab",
"style": "display:block;"
});
_tabOneFocus.innerHTML = "Blog Feed";
_feedCont.nodes.add(_tabOneFocus);
//Create Tab One Ready Div:
_tabOneReady = new Element.tag("div");
_tabOneReady.attributes = ({
"id": "tabOneReady",
"class": "tab",
"style": "display:none;"
});
_tabOneReady.innerHTML = "Blog Feed";
_feedCont.nodes.add(_tabOneReady);
//Create Tab Two Focus Div:
_tabTwoFocus = new Element.tag("div");
_tabTwoFocus.attributes = ({
"id": "tabTwoFocus",
"class": "tab",
"style": "display:none;"
});
_tabTwoFocus.innerHTML = "Repos Watched";
_feedCont.nodes.add(_tabTwoFocus);
//Create Tab Two Ready Div:
_tabTwoReady = new Element.tag("div");
_tabTwoReady.attributes = ({
"id": "tabTwoReady",
"class": "tab",
"style": "display:block;"
});
_tabTwoReady.innerHTML = "Repos Watched";
_feedCont.nodes.add(_tabTwoReady);
Now let's add event listeners to just the tabs whose id indicates that it is a "ready" tab:
//Onclick listener for Tab One Ready Div:
_tabOneReady.on.click.add((e) {
var displayList = [_tabOneFocus,_tabTwoReady,_blogOut];
changeDisplay(displayList);
});
//Onclick listener for Tab Two Ready Div:
_tabTwoReady.on.click.add((e) {
var displayList = [_tabOneReady,_tabTwoFocus,_githubOut];
changeDisplay(displayList);
});
Note how the event listener defines a display list variable displayList which passes to function changeDisplay() a list of elements whose display needs to alternate from its present display setting of "none" to "block". Note also that the code below contains a list of all the elements whose display needs to be programmatically controlled and which is then compared with the list passed to the function as a parameter:
void changeDisplay(var displayList) {
var idList = [_tabOneFocus,_tabTwoFocus,_tabOneReady,
_tabTwoReady,_blogOut,_githubOut];
for(var i = 0; i < idList.length; i++) {
var block = false;
for(var j = 0; j < displayList.length; j++) {
if(idList[i] == displayList[j]) {
block = true;
break;
}
}
if (block) {
idList[i].style.display = "block";
} else {
idList[i].style.display = "none";
}
}
}
The only thing left to do to complete the tabbed panels is the CSS, much of which is left to the designer to craft as he or she pleases. There are just a few things I would like to point out. First, we define a class that applies to all the tabs, making sure to define a width of the tab:
#feedContainer .tab {
font-family: Georgia, "Times New Roman", Times, serif;
font-size: 18px;
font-variant: small-caps;
width: 210px;
white-space: nowrap;
text-align: center;
padding: 20px 10px 5px 10px;
height: 25px;
}
Then, since we only have two tabs, we can simply float tab one to the left and tab two to the right. If you want to use more than two tabs, you can float all tabs to the left and then use margin-left (since we no longer care about the double margin bug of IE6!) to adjust the spacing between each tab.
#feedContainer #tabOneFocus {
cursor: default;
float: left;
background-image: url(images/tabbedPanelTabSelected.png);
background-repeat: no-repeat;
background-position: center top;
margin-left: 40px;
color: #000;
}
#feedContainer #tabOneReady {
float: left;
background-image: url(images/tabbedPanelTabNotSelected.png);
background-repeat: no-repeat;
background-position: center top;
margin-left: 40px;
color: #556270;
}
#feedContainer #tabOneReady:hover {
cursor: pointer;
color: #000;
text-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
}
#feedContainer #tabTwoFocus {
cursor: default;
float: right;
background-image: url(images/tabbedPanelTabSelected.png);
background-repeat: no-repeat;
background-position: center top;
margin-right: 40px;
color: #000;
}
#feedContainer #tabTwoReady {
float: right;
background-image: url(images/tabbedPanelTabNotSelected.png);
background-repeat: no-repeat;
background-position: center top;
color: #556270;
}
#feedContainer #tabTwoReady:hover {
cursor: pointer;
color: #000;
text-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
}
Finally, the divs holding the content need to clear the floats. Note that the initial state of div githubOut's display property is initialized to none:
#feedContainer #blogOut {
clear: both;
padding: 10px 30px;
text-align: left;
}
#feedContainer #githubOut {
clear: both;
padding: 10px 20px;
text-align: left;
display: none;
}
The files for this didget at on github. Please feel free to take a look.
Conclusion: Dart is Fun!
One problem I'm having with Dart, if you can call this a problem, is that the longer I work with it, the less I'm able to drag myself away and work with any other language, like JavaScript. It's not that JS is bad, it's just that Dart is better. And better in a way that makes it addicting - like you can visualize it being the last language you'll ever need to learn. Now wouldn't that be something? But I've neglected my other projects long enough, and need to get back to them. I'm saying this though, even as I'm plotting what I'm going to be doing next in Dart.
Works Cited
[1] Dart's Homepage
[2] Improving the DOM: Dart's DOM API
[3] Dart bug 1142
[4] Dart Discussion: JSONP with Dart
[5] Dart Discussion: Dart with JS APIs
[6] GitHub API v3: Working with CORS
[7] CORS: an insufficient solution for same origin restrictions
[8] Dotclear Plugin dotAjax
[9] JSONLint - The JSON Validator
[10] Dart Puzzlers Chapter 2: Puzzle 13
[11] Dart bug 1866/1867
[12] Interface XMLHttpRequest in dart:html
[13] Dart Editor
[14] Wikipedia: Same Origin Policy
[15] Cross Domain AJAX with JSONP
[16] Cross domain AJAX with CORS
[17] Mozilla: Cross site XMLHttpRequest with CORS
[18] Functional CSS Tabs Revisited
[19] Creating a Tab Panel


















