Introduction to webOS SDK Development: Part 3

Introduction to webOS SDK Development: Part 3

This entry is part 3 of 5 in the series Introduction to webOS SDK Development

Welcome to the third part of our series on webOS SDK development. In part 2, we supplied static data to a list. This tutorial will show you how to load dynamic data into a list. We’re going to use AJAX and YQL to achieve this.

Spinner and Scrim

Let’s start by changing the list scene’s HTML. Edit app/views/list/list-scene.html to contain the following:

<div id="myHeader" class="tuts-header palm-page-header multi-line">
		<div id="titleImage" class="title-image"></div>
</div>
<div class="palm-scrim" id="search_divScrim" style="display:none;">
  <div id="search_divSpinner" x-mojo-element="Spinner"></div>
</div>
<div id="listWrapper" class="palm-list main-list">
      <div id="MyList" x-mojo-element='List'></div>
</div>

The header and list are similar to the ones in the main scene. What we’re adding is a scrim and a spinner. What is a scrim and a spinner you ask? A spinner shows a spinning (surprise!) image as an indication that an operation is in progress. It’s a good choice to display a spinner for every operation that is going to take a while (and remember, since we are on a mobile device, operations that get remote data over a wireless connection might take a while). Additionally, we use a scrim (a translucent layer used to obscure the background UI) to hide the background UI while the spinner is displayed because it would make no sense to interact with the application while a operation is pending.

We also add a wrapping div around our list to push it down below the header. Define the necessary class in stylesheet/tutsplus.css for that:

.main-list {
	padding-top: 48px;
}

Next, head over to app/assistants/list-assistant.js to add the application logic. First we define the list model. Unlike last time, where the model data was static, our listmodel won’t have any data in it -it will get loaded into the list later.

this.myListModel = { items : [] };
this.myListAttr = {
	itemTemplate: "list/itemTemplate",
	renderLimit: 10,
	dividerTemplate: "list/dividerTemplate",
	dividerFunction : this.whatPosition
};

List Divider

We define two new properties in our list attributes this time: dividerTemplate and dividerFunction. Let me explain dividers first. They are essentially elements put between list entries to group them. In our app we want to group the displayed articles by date. Go ahead and create the dividerFunction:

ListAssistant.prototype.whatPosition = function(listitem){
    var myDate = new Date(listitem.pubdate);
    var ds=Mojo.Format.formatDate(myDate,{date:"long",countryCode:"US"});
    return ds;
}

A listitem is passed to our function and we create a javascript date object out of its pubDate property (it refers to the publication date we get from the RSS feed). We then reformat that date with a Mojo function to a long date string (e.g September 6, 2010) and return that. The list logic will then use that date to group list items together that have the same date. The dividerTemplate defines how the actual divider looks. Edit app/views/list/dividerTemplate.html:

<table class="palm-divider labeled">
	<tr>
		<td class="left"></td>
		<td class="label">
			#{dividerLabel}
		</td>
		<td class="right"></td>
	</tr>
</table>

Each time the list renders a divider, it inserts the above HTML-code in replaces #{dividerLabel} with the date string.

Lets create the list template next, edit app/views/list/itemTemplate.html:

<div class="palm-row grid-cell" x-mojo-tap-highlight="immediate">
  <div><span class="button #{clsname}">#{category}</span><span class="creator">by #{creator}</span><div style="clear:both;"></div></div><div class="ellipsis"><b>#{data}</b></div><div class="descr">#{description}</div>
</div>

Again, we specifiy how each row of the list is laid out and what data of the model is displayed. Also add the new classes to the stylesheets/tutsplus.css:

.pubdate {
	font-size: 10px;
}
.creator {
	font-size: 12px;
	background-color: #a0a0a0;
	float: right;
	padding: 3px 3px;
	text-align: right;
	margin-right: 14px;
	margin-top: 4px;
	color: white;
}
.ellipsis {
  padding: 10px 0px;
  margin-left: 14px;
  font-size: 19px;
  width: 95%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.descr {
  font-size: 14px;
  margin-left: 14px;
  width: 95%;
}
.button {
   width: 95%;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: clip;
   margin-left: 14px;
   padding: 3px 3px;
   -webkit-border-radius: 8px;
   color: white;
   font-size: 14px;
   text-decoration: none;
   vertical-align: middle;
}
.Nettuts {
	border-top: 1px solid #4a9082;
	background: #2e6a60;
}
.Vectortuts {
	background: #19487e;
}
.Psdtuts {
	background: #a51500;
}
.Activetuts {
	background: #a5290a;
}
.Aetuts {
	background: #4a3a57;
}
.Cgtuts {
	background: #73434f;
}
.Phototuts {
	border-top: 1px solid #3297b5;
	background: #2e92b2;
}
.Audiotuts {
	background: #3d6b00
}
.Mobiletuts {
	border-top: 1px solid #ffd200;
	background: #d19c00;
}

At the end of the setup function, add the last missing piece, the setup of the spinner:

	this.controller.setupWidget("search_divSpinner", { spinnerSize : "large" }, { spinning: true } );
}

Note that we set it to spin already, but because the DIV containing the spinner is hidden we won’t see the spinner image.

Allright, let’s move on and edit the activate function:

ListAssistant.prototype.activate = function(event) {
	/* put in event handlers here that should only be in effect when this scene is active. For
	   example, key handlers that are observing the document */
	this.headerTitleElement.innerHTML="<img src="+this.titleimage+">"
	this.getData();
}

AJAX and YQL

We display the title-image and a call to getData. This will load the data we want to display in our list. Go ahead and add the function getData:

ListAssistant.prototype.getData = function() {
	$("search_divScrim").show();

Before get the data, we show the DIV containing the spinner. We’ll show the spinner while the load operation is in progress. Our goal is to display the lastest posts from the selected tutsplus site into our list. Each tutsplus site exports its latest articles in a RSS feed. How do we read the RSS Feed to use in our application? We’re going to use YQL, the Yahoo! Query Language is an expressive SQL-like language that lets you query, filter, and join data across Web services (http://developer.yahoo.com/yql/). I won’t go into details about YQL here, you can read more about it on nettuts.

Here’s how we get the data from mobiletuts with YQL:

select * from rss where url='http://feeds.feedburner.com/mobile-tuts-summary'

Use the YQL console at http://developer.yahoo.com/yql/console to try it out. Select JSON as the output format. Here is the (shortened) result:

{
 "query": {
  "count": "1",
  "created": "2010-09-07T08:41:32Z",
  "lang": "en-US",
  "results": {
   "item": [
    {
     "title": "Introduction to webOS SDK Development: Part 2",
     "link": "http://mobile.tutsplus.com/tutorials/webos/introduction-to-webos-sdk-development-part-2/",
     "comments": "http://mobile.tutsplus.com/tutorials/webos/introduction-to-webos-sdk-development-part-2/#comments",
     "pubDate": "Mon, 30 Aug 2010 12:00:40 +0000",
     "creator": "Markus Leutwyler",
     "category": [
      "webOS",
      "webOS internet",
      "webOS rss",
      "webOS SDK",
      "webOS table view"
     ],
     "guid": {
      "isPermaLink": "false",
      "content": "http://mobile.tutsplus.com/?p=2392"
     }
   ]
  }
 }
});

Looks like we can use most of that data to display in our list. How do we get it into our list, you ask? AJAX is the answer. We are going to use an AJAX request to call the YQL webservice. Since mobiletuts uses a different feed than the other sites, we need to change the feed url manually.

var feed=this.title.toLowerCase();
if (feed=='mobiletuts') {
	feed='mobile-tuts-summary';
} else {
	feed=feed+'-summary';
}
var query="Select * from rss where url='http://feeds.feedburner.com/"+feed+"'";
var url = "http://query.yahooapis.com/v1/public/yql?q="+encodeURIComponent(query)+"&format=json";
var request = new Ajax.Request(url, {
        method: 'get',
        asynchronous: true,
        evalJSON: "false",
        onSuccess: this.parseResult.bind(this),
	on0: function (ajaxResponse) {
	        // connection failed, typically because the server is overloaded or has gone down since the page loaded
	        Mojo.Log.error("Connection failed");
	        },
	onFailure: function(response) {
	        // Request failed (404, that sort of thing)
	        Mojo.Log.error("Request failed");
	        },
	onException: function(request, ex) {
	        // An exception was thrown
	        Mojo.Log.error("Exception");
	},
});
}

We are using Prototype’s Ajax.Request function to call the Yahoo webservice. Since AJAX calls are asynchronous, we don’t know when we get back the data from the webservice. We specify the function to call when the data is received in the onSuccess callback: this.parseResult.bind(this)

There’s something new in how the callback is called, notice the added statement .bind(this). Let me explain what “this” and scope in javascript means: In JavaScript, functions are executed in a specific context, referred as “scope”. Inside the function the this keyword becomes a reference to that scope. For example, the variable this.title that we’re using in the function getData is local to that function and won’t be available in another function. Enter .bind(this). “Binding” basically determines the meaning, when a function runs, of the “this” keyword. In our example, when we call this.parseResult.bind(this), it’s variables that were referenced through this are available in the parseResult function.

The data returned from the webservice call ends up in the transport object that passed to the parseResult function. We’re interested in the transport.reponse Text Property, which contains the output as a JSON String. We convert that to an object by calling evalJSON. We can then loop through the properties of the JSON data and collect the data that we want to fill into our list.

Parsing JSON

ListAssistant.prototype.parseResult = function(transport) {
  var newData = [];
  var data=transport.responseText;
  try {
    var json = data.evalJSON();
  }
    catch(e) {
    Mojo.Log.error(e);
  }
  k=0;
  for (j=0;j<json.query.count;j++) {
      var cat="";
      var categories=json.query.results.item[j].category;
      for (i=0;i<categories.length;i++) {
  	if (i==0) { cat=categories[i]; } else { if (i<3) { cat=cat+' / '+categories[i]; } }
      }
      var descr=json.query.results.item[j].description;
      var ipos=descr.indexOf('[...]');
      if (ipos==-1) { ipos=descr.indexOf('...'); }
      if (ipos!=-1) { descr=descr.substr(0,ipos); }
      descr=descr+'...'
      newData[k] = {data: json.query.results.item[j].title, guid: json.query.results.item[j].guid.content, category: cat, pubdate: json.query.results.item[j].pubDate, creator: json.query.results.item[j].creator, clsname: this.title, description: descr };
      k++;
  }

Since the categories per article are dynamic, we are just taking the first 3 categories out of the JSON data and construct a new category string out of it (named cat). We also need to shorten the description, because the feed sometimes contains HTML-Strings that we don’t want to display. Allright, we have parsed our response JSON and constructed a new array out of it. This array is the base for our list model.

  this.myListModel["items"] = newData;
  this.controller.modelChanged(this.myListModel , this);
  // hide the spinner
  $("search_divScrim").hide();
};

First, we pass the array newData to the items of our list model and then notifiy the list that there’s a new model ready to work with. The list will then render the list with the new data. At last, we hide our spinner to show the user that the loading process has ended.

Package the app, install and run it. For each tutsplus site you select, you should now see the list being populated with the latest articles.

Wrap up

Congratulations! We have read the contents of a RSS through YQL and fed that data into our list. In part 4 we’re going to add the last missing piece to our application!

Series Navigation«Introduction to webOS SDK Development: Part 2Introduction to webOS SDK Development: Part 4»

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Justin

    Ok, a little lost. Do we remove the original application logic for the list from the MainAssistant? And all the rest goes into the ListAssistant? Poking around with the code, but as of now my app is broken.

  • Markus
    Author

    MainAssistant remains unchanged, all the described changes are in ListAssistant.

  • Musk

    Justin, remember to use ‘palm-log -f com.mystuff.tutsplus’ (or whatever you named it) to watch for errors. I had an extra “}” at one point and it was reporting ListAssistant was undefined.

    I’m having another issue now. Based on the log output below, it seems ListAssistant.prototype.setup is getting passed a null value somehow:

    > error: An exception occurred in the ‘list’ scene’s setup() method.
    > error: Error: Cannot set property ‘innerHTML’ of null, line undefined, file undefined
    > warning: WidgetController: Could not instantiate widget ‘ MyList ‘, since it has not been set up.
    > info: About to activate scene list
    > error: An exception occurred in the ‘list’ scene’s activate() method.
    > error: Error: Cannot set property ‘innerHTML’ of undefined, line undefined, file undefined

    I *think* the first two errors occur when this is executed:

    this.controller.get(‘result’).innerHTML=”You have selected “+this.title+” in the list.”

    And of course the activate() errors occur when that method is called.

    If there’s some issue with the value(s) being passed to the above code, I’d expect the culprit to be in main-assistant.js. However, I didn’t modify anything in the main-assistant for Part 3 of this tutorial. The Part 2 code worked as expected before modifying it for Part 3.

    Anyway, this is a great intro to webOS and I’d appreciate any debugging tips that can be offered.

    • Markus
      Author

      Error: Cannot set property ‘innerHTML’ of null, line undefined, file undefined

      that probably means that this.controller.get(‘result’) is undefined … check your view html code if a div with the id result exists.

  • Musk

    Thank you, Markus, that was it exactly: I was missing divs for both result and image:

    I added those and was able to push the list scene with a header. However, I had other problems too. As noted in the log, the WidgetController couldn’t instantiate MyList because it wasn’t set up. I solved that by adding this to the list assistant’s setup just before setting up the search_divSpinner widget:

    this.controller.setupWidget(“MyList”, this.myListAttr, this.myListModel);

    Although MyList is setup in main-assistant.js during Part 2 of the tutorial, Part 3 doesn’t mention it for the list assistant.

    After that, the MyList widget could be instantiated, although the list still didn’t load. My last problem was with the list assistant’s activate method.

    this.headerTitleElement.innerHTML=”"

    ^^^That code was trying to set the innerHTML property on this.headerTitleElement.innerHTML, which was undefined. I’m still not sure if I’m missing something or if it’s a typo in the tutorial but I was able to get MyList to load properly by doing this:

    1) Commenting out these two lines from the setup method, which add text to the top of the list scene that you don’t need since there will be an actual header:

    // this.controller.get(‘result’).innerHTML=”You have selected “+this.title+” in the list.”
    // this.controller.get(‘image’).innerHTML=”"

    2) Modifying the problem line above in the activate method so that it calls innerHTML for “titleImage” (the name of the div in my list-scene.html) instead of headerTitleElement:

    this.controller.get(‘titleImage’).innerHTML=”"

    There is no final screenshot to compare this to, but the result looks right to me. There’s a gray header with the appropriate logo in it and a scrollable list of articles with author, title and preview, sub-divided with a palm-divider that’s labeled with the date.

  • Reiner

    It should be noted that this tutorial currently contains three errors:

    1) DELETE the following line from ListAssistant.prototype.setup

    this.controller.get(‘result’).innerHTML=”You have selected “+this.title+” in the list.”
    this.controller.get(‘image’).innerHTML=”"

    2) DELETE from ListAssistant.prototype.activate:

    this.headerTitleElement.innerHTML=”"

    3) ADD the following line to the ListAssistant.prototype.setup

    this.controller.setupWidget(“MyList”,this.myListAttr,this.myListModel);

    Markus, please fix the source code in your tutorial accordingly.

    • http://martinhacks.blogspot.com Martin Hacks

      I can confirm those three mistakes. The app works now.

  • louis

    Thanks for the great tutorial, the part was staright forward. However, I am currently encountering these errors

    1. About to activate scene “main”

    2. info: transition ended

    3. Warning : the scene assistant “ListAssistant” is not defined. Did you remember to include it in sources.json?

    4. Error: the scene ‘list’ could not be pushed because an exception occured

    5. Error require failed: the scene assistant “ListAssistant” is not defined. Did you remember to include it in sources.json. line undefined, file undefined.

    I am thinking that the cause of thethird and fourth are because the main scene is being activated instead of the list.

  • Louis

    thanks for the tutorial, its great especially the part 2.

    However, I am encountering these errors

    Uncaught SyntaxError: Unexpected identifier, app/assistants/list-assistants.js:73

    i assume it is referring to line 73 and the syntax is
    ” select * from rss where url = “http://feeds.feedburner.com/mobile-tuts-summary”"

    • http://- Stefan

      Hi Louis,

      please do not use ” at … url = http…
      The url-String has to be in ….

      Hope this helps…