The design for Mojo began with the List. To validate the webOS architecture and the concept of Mojo, the principle webOS architects were challenged to design a list widget that would pull dynamic data from the Contacts database as the user flicked through the list, without any perceptible delay or loss of data (on a low-end CPU, no less). Needless to say, this was not a trivial challenge. However, the challenge was met and the rest of the framework took shape around the resulting design.
The webOS user experience makes extensive use of lists in many applications. Given the form factor and the navigation model, most applications will incorporate a List widget in one way or another. To get the most out of Mojo you need to fully understand the List widget.
The other list widget, the Filter List, is derived from the List widget. It has many of the same features as the List widget, but is designed around a more specialized use case. Our sample application will make use of Filter List in Chapter 5.
Lists are rendered by inserting objects into the DOM using provided HTML templates for both the list container and the individual list rows. Lists can be variable height and include single and multi-line text, images, or other widgets. Some Lists are static, meaning the list items are provided to the widget directly as an array. Other Lists are dynamic, meaning the application provides items as needed for display. Lists can be manipulated in place, with the framework handling deletion, reordering, and add item functions for the application.
There are examples of the list in the core applications, including the Email inbox, the Message chat view, the Contacts list, the Music library, and more. You can see that lists are flexible, yet fast and very efficient.
We’re going to use a List widget in a few places in News. First, we’re going to convert the sample newsfeed to a list, then hook it up to an Ajax call to get the live newsfeed into the application. That should give us a basic news reader for one feed, but to handle multiple feeds we’ll add another List widget as a list of feeds. The application will start to take its basic shape in this section.
We’ll create a list to hold the sample list that we’ve been
working with. Using the palm-generate
tool, create
a new scene for a list view, called
storyList:
$ palm-generate -t new_scene -p "name=storyList"
In the view file, views/storyList/storyList-scene.html,
declares a List widget under a palm-header
that includes
some text to which we’ll later assign the list’s title. The storyListWgt
div is the List widget
declaration:
<div id="feedTitle" class="palm-header center"> Feed Title </div> <div class="palm-header-spacer"></div> <div id="storyListScene" class="storyListScene"> <div x-mojo-element="List" id="storyListWgt" ></div> </div>
Next, create the list templates. These are two HTML files that you put into the views/storyList directory; the container template storyListTemplate.html and the row template storyRowTemplate.html.
All List widgets are all built using HTML templates to lay out
and format the list container and the individual rows. You normally
include these templates as separate HTML files in your scene’s view
folder (where your scene view file is located), but you can also
specify each template’s pathname, which allows you to share templates
between scenes or organize them in other ways. Pathnames are specified
with relative notation scene-dir/template-file
,
where scene-dir
is the directory
for the current scene’s view file. Within the template, you will
reference properties from the list.
The listTemplate
is optional;
it defines the path to an HTML template for the list’s container,
which, if missing, will simply put the list items into the scene
enclosed in div with the palm-list
classname. If
present, the listTemplate
can have
only one top level element.
The itemTemplate
is required;
it is set to the path of an HTML template for the list items. Use the
notation #{property}
to identify
specific items
properties for
insertion into the template.
The storyListTemplate
includes a single line using the palm-list
class to format the list and a
template entry for #{-listElements}
:
<div class="palm-list">#{-listElements}</div>
By default, Mojo will escape any HTML that is inserted into a template to limit the risk of JavaScript insertion into views. You can add a leading hyphen to any property reference to prevent HTML from being escaped on that property. This is required in list container templates for the list widget to render properly.
The storyRowTemplate is a
little more involved, using an outer div with the class palm-row
to
format the row, then each list row has both a title entry and a text
entry:
<div class="palm-row" x-mojo-touch-feedback="delayed"> <div class="palm-row-wrapper"> <div id="storyTitle" class="title truncating-text #{unreadStyle}"> #{title} </div> <div id="storySummary" class="news-subtitle truncating-text"> #{text} </div> </div> </div>
Each entry uses the truncating-text
class, which will cause the entry to be automatically truncated at the
list boundaries with ellipsis to indicate truncation. The templates
#{title}
and #{text}
refer to the list items properties
of those names that are substituted into the template.
The #{unreadStyle}
template references another list items property that indirectly forces
some styling specifically for the story titles that are not read. This
demonstrates that property substitution can be used with any HTML
content. Further on, we will apply some CSS styling to the classname
used in the unreadStyle
property.
Taken together, the scene’s view or HTML files wrap the List
widget with some specific styles to get the visual appearance shown in
Figure 3–6. You should review the SDK’s “User Interface Guidelines”
for a complete discussion of Mojo styling, but to summarize briefly,
there are three levels of styles at work in the
storyList
scene:
palm-list
Is used in the listTemplate
to
drive spacing and the light separator rule that divides the list
entries.
palm-row
Wraps the div tag containing the list entry template to handle background
styles and styling for highlight, selection, swipes, and other
dynamic behavior. It can be modified with additional styles for
first
, last
, single
, and others (a complete list is
provided in Appendix C).
palm-row-wrapper
Also wraps the div tag containing the list entry template and
adjusts spacing within palm-row
.
Back to the example, to implement the feed list handling, add the storyList-assistant.js:
/* StoryListAssistant - NEWS Copyright 2009 Palm, Inc. All rights reserved. Displays the feed's stories in a list, user taps display the selected story in the storyView scene. Major components: - Story View; push story scene when a story is tapped Arguments: - feedlist; Feeds.list array of all feeds - selectedFeedIndex; Feed to be displayed */ function StoryListAssistant(feedlist, selectedFeedIndex) { this.feedlist = feedlist; this.feed = feedlist[selectedFeedIndex]; this.feedIndex = selectedFeedIndex; Mojo.Log.info("StoryList entry = ", this.feedIndex); Mojo.Log.info("StoryList feed = ", Object.toJSON(this.feed)); } StoryListAssistant.prototype.setup = function() { // Setup story list with standard news list templates. this.controller.setupWidget("storyListWgt", { itemTemplate: "storyList/storyRowTemplate", listTemplate: "storyList/storyListTemplate", swipeToDelete: false, renderLimit: 40, reorderable: false }, this.storyModel = { items: this.feed.stories } ); this.readStoryHandler = this.readStory.bindAsEventListener(this); this.controller.listen("storyListWgt", Mojo.Event.listTap, this.readStoryHandler); // Set title into header $("feedTitle").innerHTML=this.feed.title; }; StoryListAssistant.prototype.activate = function() { // Update list models this.storyModel.items = this.feed.stories; this.controller.modelChanged(this.storyModel); }; StoryListAssistant.prototype.cleanup = function() { // Remove event listeners this.controller.stopListening("storyListWgt", Mojo.Event.listTap, this.readStoryHandler); }; // readStory - when user taps on displayed story, push storyView scene StoryListAssistant.prototype.readStory = function(event) { Mojo.Log.info("Display selected story = ", event.item.title, "; Story index = ", event.index); Mojo.Controller.stageController.pushScene("storyView", this.feed, event.index); };
When the scene is instantiated with a call to the StoryListAssistant
function, the passed feed
index assigns the selected feed to this.feed
. The setup method is called before
the scene is pushed and sets up the List widget: the templates are
assigned and renderLimit
is set to
40. You should use the default for your lists, but adjust it if
necessary after testing.
The list’s model items are set to the input feed’s stories
array for display in the list, and
setupWidget
is called to
instantiate the list. A listener is added for any taps on the list,
and the handler, readStory
, will
push the storyView
scene with that
selected story entry.
In the setup()
method, the
list title is assigned to display in the header. You’ll notice that we
use the Prototype function $()
to
retrieve the header’s element ID. This is safe to use in this context,
but as you’ll see in Chapter 10, it’s
not safe in multistage applications.
In the activate()
method, we
provisionally update the list’s model in case reading the selected
story changed the story’s unreadStyle
to read; we
want to reflect changes in status immediately.
Next we have to change the stage-assistant.js to push
the storyList
scene instead of
the storyView
scene:
StageAssistant.prototype.setup = function() { // initialize the feeds model and update the feeds this.feeds = new Feeds(); // Push the first scene this.controller.pushScene("storyList", this.feeds.list, 0); };
For arguments, the storyList
scene takes the
feed list and an index value for the currently selected list. We’re
still using a single default list for now, so the index is set to
0.
Finally, add the new assistant to sources.json, then launch the application. The new scene with all the stories from the sample feed is shown in Figure 3-7.
Chapter 6 covers Ajax requests more completely, but we’ll look briefly at it here to enable dynamic feed lists. Now that we have a list, we’re going to add the capability to load the list and update it through Ajax requests to the feed source.
Ajax requests are a common way of referring to use of the XMLHttpRequest object to make asynchronous HTTP transactions. The Prototype library built into Mojo provides an Ajax.Request object, which simplifies the XMLHttpRequest handling for many transactions.
These transactions provide a key part of building webOS applications by providing the core data services needed to build connected applications. You don’t need to use the Prototype Ajax functions if you’d prefer to use XMLHttpRequest directly.
Dynamic data is a very powerful and important capability that should be exploited by most applications. With the capability to update your application’s data set, you are enabling the user with the most current and accurate information. Without this, the application loses value, as the degree of change is considerable over the course of hours, or even minutes in some cases.
You can write your own Ajax interfaces, but one reason that webOS includes the Prototype library is for its simple, powerful Ajax functions. We’ll add the Ajax request to the feeds.js model, which will request feed data for our default New York Times feed. While the Ajax request is fairly simple, we need to process the RSS and Atom data that the application receives, and that’s a bit more complicated.
We just need to add a URL for the Ajax request and set up some callback functions. See Chapter 6 for a full explanation of the arguments and properties used in Ajax.Request. Add a new method to feeds.js:
// updateFeedRequest - function called to setup and make a feed request updateFeedRequest: function(currentFeed) { this.currentFeed = currentFeed; Mojo.Log.info("URL Request: ", this.currentFeed.url); var request = new Ajax.Request(currentFeed.url, { method: "get", evalJSON: "false", onSuccess: this.updateFeedSuccess.bind(this), onFailure: this.updateFeedFailure.bind(this) }); },
Ajax requests are asynchronous operations, with both success and
error cases, and you’ll need to create callback functions for each of
these cases. The handler for the error case simply logs the error.
Ajax requests return an HTTP status message, which we will convert to
a readable format with Prototype’s Template
function, and then log the results:
// updateFeedFailure - Callback routine from a failed AJAX feed request; // post a simple failure error message with the http status code. updateFeedFailure: function(transport) { // Prototype template to generate a string from the return status.xs var t = new Template("Status #{status} returned from newsfeed request."); var m = t.evaluate(transport); // Post error alert and log error Mojo.Log.info("Invalid feed - http failure, check feed: ", m); },
The handler for the successful case needs to process the feed
before it can be used. In this case, we confirm the successful load by
logging the returned status message, again using the Template
function. Next, there’s some code
to handle when the feed data is returned as text-encoded XML; we can
convert it to XML to enable processing.
The global function ProcessFeed
is called
to determine the feed format and extract the components that we need
for our feed list. We’ll cover this in a moment, but for now, note
that it is called and returns with an explicit error status that, if
equal to errorNone
, means that the
feed was processed successfully. We push the storyList
scene with the processed feed in
that case:
// updateFeedSuccess - Successful AJAX feed request (feedRequest); // uses this.feedIndex and this.list updateFeedSuccess: function(transport) { var t = new Template({key: "newsfeed.status", value: "Status #{status} returned from newsfeed request."}); Mojo.Log.info("Feed Request Success: ", t.evaluate(transport)); // Work around due to occasional XML errors if (transport.responseXML === null && transport.responseText !== null) { Mojo.Log.info("Request not in XML format - manually converting"); // ** These next two lines are wrapped for book formatting only ** transport.responseXML = new DOMParser(). parseFromString(transport.responseText, "text/xml"); } // Process the feed, passing in transport holding the updated feed data var feedError = this.processFeed(transport, this.feedIndex); // If successful processFeed returns News.errorNone, if (feedError !== News.errorNone) { // There was a feed process error; unlikely, but could happen if the // feed was changed by the feed service. Log the error. if (feedError == News.invalidFeedError) { Mojo.Log.info("Feed ", this.nameModel.value, " is not a supported feed type."); } } News.feedListChanged = true; // If NOT the last feed then update the feedsource and request next feed this.feedIndex++; if(this.feedIndex < this.list.length) { this.currentFeed = this.list[this.feedIndex]; this.updateFeedRequest(this.currentFeed); } else { // Otherwise, this update is done. Reset index to 0 for next update this.feedIndex = 0; News.feedListUpdateInProgress = false; } }
this.processFeed()
is
included in Appendix D if you’re
interested in how it works, but it’s not shown here, since it doesn’t
directly affect the Mojo functions being presented. To summarize,
this.processFeed()
is passed an XML
object and an index into the feed list, where it will put the
processed feed. If there’s no index argument, this.processFeed()
will add the new feed to
the end of the list.
For each of the supported formats, the title, text, and URL are
extracted for each of the stories and the feed list is updated with
the new feed data, the stories, and the unreadCount
. If the feed isn’t a well-formed
Atom, RSS 1 (RDF), or RSS 2 format, it will return with an error,
News.invalidFeedError
.
We’ve added some logic to the end of updateFeedSuccess()
to handle multiple feeds
and to flag that the feed list has been changed. We’ll come back to
these in the next section as we expand News to handle multiple
feeds.
Initiate the update with a call from within the stage assistant’s setup method and add the global definitions needed for feed updates:
// --------------------------------------------------------------- // GLOBALS // --------------------------------------------------------------- // News namespace News = {}; // Constants News.unreadStory = "unReadStyle"; News.versionString = "1.0"; News.errorNone = "0"; // No error, success News.invalidFeedError = "1"; // Not RSS2, RDF (RSS1), or ATOM // Session Globals - not saved across app launches News.feedListChanged = false; // Triggers update to db News.feedListUpdateInProgress = false; // Feed update is in progress StageAssistant.prototype.setup = function() { // initialize the feeds model and update the feeds this.feeds = new Feeds(); // Update the news feed list this.feeds.updateFeedRequest(this.feeds.list[0]); // Push the first scene this.controller.pushScene("storyList", this.feeds.list, 0); };
When the application is launched, it displays the default data in the top-level scene. If you tap a story to go to the story view, you’ll see new stories, though. Popping the story view with a back gesture restores the story list view, but now with the updated stories. What’s happening here?
Since Ajax requests are asynchronous, the initial story list
view is pushed before the feed update is completed, but subsequent
views are displayed after receiving the data. The right way to fix
this is to update the storyListWgt
model after the
feed update is complete. You’ll learn one technique for that in the
next section, and a better one in Chapter 10, when we adapt the feed update
process to a background application. The new storyList
scene, when updated with a longer
list of stories, is shown in Figure 3-8.
A News reader that handles one newsfeed isn’t much use, so we’re
going to expand News to handle multiple feeds with another List
widget. This one will present a list of newsfeeds for the user
to select from before pushing the storyList
scene with the selected list. We
will also take advantage of the List widget’s capability to reorder
and delete list entries to enable some management of the
newsfeeds.
We’re still working from a default set of newsfeeds, but let’s expand our feeds model by adding some popular news, sports, and technology feeds:
// Default Feeds.list defaultList: [ { title:"Huffington Post", url:"http://feeds.huffingtonpost.com/huffingtonpost/raw_feed", type:"atom", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Google", url:"http://news.google.com/?output=atom", type:"atom", numUnRead:0, newStoryCount:0, stories:[] },{ title:"New York Times", url:"http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"MSNBC", url:"http://rss.msnbc.msn.com/id/3032091/device/rss/rss.xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"National Public Radio", url:"http://www.npr.org/rss/rss.php?id=1004", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Slashdot", url:"http://rss.slashdot.org/Slashdot/slashdot", type:"rdf", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Engadget", url:"http://www.engadget.com/rss.xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"The Daily Dish", url:"http://feeds.feedburner.com/andrewsullivan/rApM?format=xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Guardian UK", url:"http://feeds.guardian.co.uk/theguardian/rss", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Yahoo Sports", url:"http://sports.yahoo.com/top/rss.xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"ESPN", url:"http://sports-ak.espn.go.com/espn/rss/news", type:"rss", numUnRead:0, newStoryCount:0, stories:[] },{ title:"Ars Technica", url:"http://feeds.arstechnica.com/arstechnica/index?format=xml", type:"rss", numUnRead:0, newStoryCount:0, stories:[] } ],
We have to adjust the Ajax requests to make requests serially
for each feed. We’ll add a new function, updateFeedList
to the feeds model:
// updateFeedList(index) - called to cycle through feeds. This is called // once per update cycle. updateFeedList: function(index) { Mojo.Log.info("Feed Update Start"); News.feedListUpdateInProgress = true; // request fresh copies of all stories this.currentFeed = this.list[this.feedIndex]; this.updateFeedRequest(this.currentFeed); },
Next, we’ll create the feed list scene, which will display the
list of feeds. Use palm-generate
to create the
scene and in the feedList-scene.html file, add a header
titled ‘Latest News’ and the List widget declaration for feedListWgt
:
<div id="feedListScene"> <div id="feedListMain"> <div id="feedList_view_header" class="palm-header left"> Latest News </div> <div class="palm-header-spacer"></div> <!-- Feed List --> <div class="palm-list"> <div x-mojo-element="List" id="feedListWgt"></div> </div> </div> </div>
To format the list, we need the list templates, which in this case are put into the views/feedList directory. First the container template, feedListTemplate.html:
<div class="palm-list">#{-listElements}</div>
and then feedRowTemplate.html to format the individual list entries:
<div class="palm-row" x-mojo-touch-feedback="delayed"> <div class="palm-row-wrapper textfield-group"> <div class="title"> <div class="palm-dashboard-icon-container feedlist-icon-container"> <div class="dashboard-newitem feedlist-newitem"> <span class="unReadCount">#{numUnRead}</span> </div> <div id="dashboard-icon" class="palm-dashboard-icon feedlist-icon"> </div> </div> <div class="feedlist-title truncating-text">#{title}</div> <div class="feedlist-url truncating-text">#{-url}</div> </div> </div> </div>
Create the feed list assistant (feedList-assistant.js), which primarily
completes the widget setup and adds event listeners in the file before
calling the new updateFeedList
method. All this is done within the assistant’s setup method:
/* FeedListAssistant - NEWS Copyright 2009 Palm, Inc. All rights reserved. Main scene for News app. Includes AddDialog-assistant for handling feed entry and then feedlist-assistant and supporting functions. Major components: - FeedListAssistant; manages feedlists - List Handlers - delete, reorder and add feeds Arguments: - feeds; Feeds object */ // ------------------------------------------------------------------- // // FeedListAssistant - main scene handler for news feedlists // function FeedListAssistant(feeds) { this.feeds = feeds; } FeedListAssistant.prototype.setup = function() { // Setup the feed list, but it's empty this.controller.setupWidget("feedListWgt", { itemTemplate:"feedList/feedRowTemplate", listTemplate:"feedList/feedListTemplate", swipeToDelete:true, renderLimit: 40, reorderable:true }, this.feedWgtModel = {items: this.feeds.list}); // Setup event handlers: list selection, add, delete and reorder feed entry this.showFeedHandler = this.showFeed.bindAsEventListener(this); this.controller.listen("feedListWgt", Mojo.Event.listTap, this.showFeedHandler); this.listDeleteFeedHandler = this.listDeleteFeed.bindAsEventListener(this); this.controller.listen("feedListWgt", Mojo.Event.listDelete, this.listDeleteFeedHandler); this.listReorderFeedHandler = this.listReorderFeed.bindAsEventListener(this); this.controller.listen("feedListWgt", Mojo.Event.listReorder, this.listReorderFeedHandler); // Update the feed list this.feeds.updateFeedList(); }; // cleanup - always remove event listeners FeedListAssistant.prototype.cleanup = function() { Mojo.Log.info("FeedList cleaning up"); // Remove event listeners this.controller.stopListening("feedListWgt", Mojo.Event.listTap, this.showFeedHandler); this.controller.stopListening("feedListWgt", Mojo.Event.listDelete, this.listDeleteFeedHandler); this.controller.stopListening("feedListWgt", Mojo.Event.listReorder, this.listReorderFeedHandler); };
You’ll see that we added both the reorderable
and
swipeToDelete
properties to the
feedListWgt
list widget. A
tap-and-hold on a list item will allow the user to move it to a new
position in the list. A Mojo.Event.listReorder
event is fired on the
widget div, which includes the item being moved, as well as the old
and new indexes. The indexes are passed as properties of the event
object, event.toIndex
and event.fromIndex
.
Dragging items horizontally will invoke a special delete UI,
allowing the user to confirm or cancel the operation. If confirmed, a
Mojo.Event.listDelete
event is
fired on the widget div, which includes the item being removed,
event.item
, and its index, event.index
.
We added event listeners for the Mojo.Event.listDelete
and Mojo.Event.listReorder
, and need to provide
handlers for these events:
// ------------------------------------------------------------------------ // List functions for Delete, Reorder and Add // // listDeleteFeed - triggered by deleting a feed from the list and updates // the feedlist to reflect the deletion // FeedListAssistant.prototype.listDeleteFeed = function(event) { Mojo.Log.info("News deleting ", event.item.title, "."); var deleteIndex = this.feeds.list.indexOf(event.item); this.feeds.list.splice(deleteIndex, 1); News.feedListChanged = true; }; // listReorderFeed - triggered re-ordering feed list and updates the // feedlist to reflect the changed order FeedListAssistant.prototype.listReorderFeed = function(event) { Mojo.Log.info("com.palm.app.news - News moving ", event.item.title, "."); var fromIndex = this.feeds.list.indexOf(event.item); var toIndex = event.toIndex; this.feeds.list.splice(fromIndex, 1); this.feeds.list.splice(toIndex, 0, event.item); News.feedListChanged = true; };
In both cases, the framework handles the on-screen changes, but you will need to reflect those changes in the feed model itself. Add listeners to receive the delete and reorder events, and you will receive the indexes for the changes through the event object. You then use these indexes to make the corresponding changes in the feed model.
The lists we set up for News are not using them, but there are other list manipulation options:
If the addItemLabel
property is specified, an additional item is appended to the list.
Tapping it will cause a Mojo.Event.listAdd
event to be fired on
the widget div.
Deleted items that are unconfirmed have a deleted
property
set in the model. You can specify the name of this property using
the deletedProperty
property, and Mojo.Event.propertyChange
events will be
sent when it is updated. If unspecified, the property deleted
will be used. For dynamic lists,
it is important for the application implementation to persist this
value in a database. Otherwise, swiped items will be automatically
undone when they are removed from the cache of loaded
items.
A better option than persisting the deleted property is
using the uniquenessProperty
. This is the
name of an item model property that can be used to uniquely
identify items. If specified, List will maintain a hash of swiped
items instead of setting a deleted property, preventing the app
from having to persist the deleted property.
If the dragDatatype
property is specified, users will be able to drag items to other
lists with the same dragDatatype
value. When this happens,
the item’s old list will
receive a Mojo.Event.listDelete
event, and the new
list will get a Mojo.Event.listAdd
event. In this case, the Mojo.Event.listAdd
event will have the
item and index properties specified, indicating that a specific
item should be added at a specific location.
The other event handler, showFeed()
, pushes the
storyList
scene when a Mojo.Event.listTap
event is received,
meaning that a feed has been tapped:
// ------------------------------------------------------------------------------ // Show feed handler // // showFeed - triggered by tapping a feed in the this.feeds.list. FeedListAssistant.prototype.showFeed = function(event) { Mojo.Controller.stageController.pushScene("storyList", this.feeds.list, event.index); };
Before running the application, change the stage assistant to
push the feedList
scene:
// Push the first scene this.controller.pushScene("feedList", this.feeds);
Change the stage controller to push the
feedList
scene, and when you run the application
now, you’ll see that it’s starting to take the basic structure of the
envisioned application. It has an initial scene that is a list of
available feeds with a count of unread messages, which users can tap
to view individual feeds and messages. We’ve made a lot of changes in
this section, as the List widget has really opened up the
application’s feature set.
You’ll notice that the list entry style is not complete, but we’ll fix that with some CSS in stylesheets/News.css:
/* feedList styles */ .feedlist-title { line-height: 2.0em; } .feedlist-url { font-size: 14px; color: gray; margin-top: −20px; margin-bottom: −20px; line-height: 16px; } .feedlist-icon-container { height: 54px; margin-top: 5px; } .feedlist-icon { background: url(../images/list-icon-rssfeed.png) center no-repeat; } .feedlist-newitem { line-height: 20px; height: 26px; min-width: 26px; -webkit-border-image: url(../images/feedlist-newitem.png) 4 10 4 10 stretch stretch; -webkit-box-sizing: border-box; border-width: 4px 10px 4px 10px;
A few of these styles (feedlist-icon-container
, feedlist-icon
, and feedlist-newitem
) are modified versions of
the framework’s standard dashboard styles. Those styles set up the
icon and new items badge to the left of each feed. The other styles
refine the positioning and appearance of the feed title and
URL.
Now when you run the application the styling should look
complete, but there is still a problem. As we saw with the
storyList
scene in the last section, the feed
updates aren’t reflected in the displayed list view until you tap a
feed then return to the feedList
scene.
We need to be able to update the list widget’s model as each
feed is updated. First, add activate and deactivate methods to the
feedList
scene:
// activate FeedListAssistant.prototype.activate = function() { this.feeds.registerListModel(this); if (News.feedListChanged === true) { this.feedWgtModel.items = this.feeds.list; this.controller.modelChanged(this.feedWgtModel, this); } }; // deactivate FeedListAssistant.prototype.deactivate = function() { Mojo.Log.info("FeedList deactivating"); this.feeds.removeListModel(this.feedWgtModel); };
In the activate()
method,
call to register this assistant with the feeds object so that it will
update this.feedWgtModel
when
changes are made to the feed. Also add an update to the model for any
activation of this scene. In this way, unread count changes are
reflected whenever new stories are viewed in the
storyView
scene. In the deactivate()
method, remove
the registration whenever the feedList
scene is
replaced by another scene.
Then add these new methods to feeds.js, along with an updateListModel()
method that will be called
from within the feed update loop in updateFeedSuccess()
:
// registerListModel(sceneAssistant) - called to register the list model for updates // as the underlying data changes. registerListModel: function(sceneAssistant) { Mojo.Log.info("Model Registered"); this.listAssistant = sceneAssistant; }, // removeListModel() - called to remove the list model from updates // as the underlying data changes. removeListModel: function() { Mojo.Log.info("Model Removed"); this.listAssistant = undefined; }, // updateListModel() - called to update the list. updateListModel: function() { Mojo.Log.info("Model Updated"); if (this.listAssistant !== undefined) { this.listAssistant.feedWgtModel.items = this.list; this.listAssistant.controller.modelChanged(this.listAssistant.feedWgtModel, this); } },
Now when you run the application, the feed list widget is updated as the feed data is updated by the feeds object. You’ll see the unread count (shown in the white badge on each feed) change to reflect the number of stories read in each feed and when fully loaded a view like the one in Figure 3-9.
Note that modelChanged()
causes a full
rendering of the widget. For small changes, it’s
better to use noticeAddedItems()
or noticeUpdatedItems()
to
render only the changed elements.
We’ll make one more change. The application updates the feed
when launched, but it would be nicer to have the feeds update
periodically. We’ll set up an alarm during the stage assistant’s setup
method to fire after 15 minutes has elapsed, using the JavaScript
setTimeout()
method.
We created a setWakeup()
method to
set the alarm and a handleWakeup()
method as the callback when the alarm fires. The handleWakeup()
method sets the next alarm
and calls this.feeds.updateFeedList()
to refresh all
of the feeds:
// ------------------------------------------------------- // setup - all startup actions: // - Setup globals // - Initiate alarm for first feed update StageAssistant.prototype.setup = function() { // initialize the feeds model and update the feeds this.feeds = new Feeds(); // Set up first timeout alarm this.setWakeup(); // Push the first scene this.controller.pushScene("feedList", this.feeds); }; // ------------------------------------------------------------------------ // handleWakeup - called when wakeup timer fires; sets a new timer and calls // for a feed update cycle StageAssistant.prototype.handleWakeup = function() { // Set next wakeup alarm this.setWakeup(); // Update the feed list Mojo.Log.info("Update FeedList"); this.feeds.updateFeedList(); }; // ------------------------------------------------------------------------ // setWakeup - called to setup the wakeup alarm for background feed updates // if preferences are not set for a manual update (value of 0) StageAssistant.prototype.setWakeup = function() { if (News.feedUpdateInterval !== 0) { var interval = News.feedUpdateInterval; News.wakeupTaskId = this.controller.window.setTimeout(this.handleWakeup.bind(this), interval); Mojo.Log.info("Set Interval Timer: ", interval); } }; // ------------------------------------------------------------------------ // cleanup - clear the wakeup alarm for background feed updates if set StageAssistant.prototype.cleanup = function() { if (News.wakeupTaskId !== 0) { News.wakeupTaskId = this.controller.window.clearTimeout(News.wakeupTaskId); Mojo.Log.info("clear Interval Timer"); } };
Define the global News.feedUpdateInterval
at the beginning of
the stage assistant; we’ll use it later when we add a preferences
option to change the update interval. Don’t forget to clear the
timeout when the stage is closed, which in this case happens when the
application is closed.
There are several major features included with lists that aren’t used with News lists, and there is another list widget: Filter List. We’ll use Filter List in Chapter 5 to add a search list to News, but the other features will be briefly touched on here.
The List attributes can optionally include a callback function for supplying
list items dynamically. You do not need to provide the items array
objects at setup time; whenever the framework needs to load items
(speculatively or for display), it will call the callback function
itemsCallback (listWidget,
offset, limit)
, with the arguments described in Table 3-3.
Table 3-3. ItemsCallback arguments
Argument | Type | Description |
---|---|---|
| Object | The DOM node for the
list widget requesting the |
| Integer | Index in the list of the first desired item model object (zero-based) |
| Integer | The number of item model objects requested |
It is understood that the requested data may not be immediately
available. Once the data is available, the given widget’s noticeUpdatedItems()
method should be called to update the list. It’s acceptable to call
the noticeUpdatedItems()
immediately, if desired, or any amount of time later. Lengthy delays
may cause various scrolling artifacts, however. It should be called as
listWidget.mojo.noticeUpdatedItems(offset,
items)
, using the arguments shown in Table 3-4.
The formatters property is a simple hash of property names to formatter functions, like this:
{timeValue: this.myTimeFormatter, dayOfWeek: this.dayIndexToString, ... }
Before rendering the relevant HTML templates, the formatters are applied to the objects used for property substitution. The keys within the formatters hash are property names to which the formatter functions should be applied. The original objects are not modified, and the formatted properties are given new modified names so that the unformatted value is still accessible from inside the HTML template.
The divider function works similar to a data formatter function. It is called
with the item model as the sole argument during list rendering, and it
returns a label string for the divider. For example, the function
dividerAlpha
generates list
dividers based on the first letter of each item:
dividerAlpha = function(itemModel) { return itemModel.data.toString()[0]; };
If you’re defining your own template, you should insert the
property #{dividerLabel}
where you
would want to have the label string inserted.
3.15.17.1