Lists

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.

List Widgets

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.

Back to the News: Adding a Story List

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.

Note

In Mojo, pathnames are relative to the 'app' directory, not the location of index.html.

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>

Note

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.

A storyList scene

Figure 3-7. A storyList scene

Back to the News: Ajax requests

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 storyList scene with updated stories

Figure 3-8. A storyList scene with updated stories

Back to the News: Adding a feed list

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

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.

The feedList scene

Figure 3-9. The feedList scene

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.

More About Lists

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.

Dynamic lists

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

listWidget

Object

The DOM node for the list widget requesting the items

offset

Integer

Index in the list of the first desired item model object (zero-based)

limit

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.

Table 3-4. noticeUpdatedItems arguments

Argument

Type

Description

offset

Integer

Index in the list of the first object in items; usually the same as offset passed to the itemsCallback

items

Array

An array of the list item model objects that have been loaded for the list

Formatters and dividers

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.80.52