www.webOShelp.net

Get the latest on:webOS RSS FeedwebOS Facebook page webOS Twitter Feed

Home Forums

The www.webOShelp.net WebOS / Mojo Developers' Forum

WebOS Mojo Development => Mojo Tutorials/Sample code/applications => Topic started by: Ken Young on April 21, 2009, 11:40:07 PM



Title: Sample Mojo code for webOS News Reader application
Post by: Ken Young on April 21, 2009, 11:40:07 PM
Here's the code from the tutorial being presented in the book Palm webOS by Mitch Allen, published by O'Reilly (http://oreilly.com/catalog/9780596801816/).  I'll keep it up to date as additional chapters are released and the code evolves.

I'm also working on writing up a few articles that will help explain the concepts being presented in the book along with the code.  Work has been pretty busy lately, however, so it may take some time to get through the whole chapter (it was pretty packed with info).  I'll try to update the site with something new daily, though, so check back often!

This was the application file structure at the end of Chapter 2.  It has since evolved quite a bit since then.  I'll take a screenshot of the updated structure and post it when I get a chance.

(http://www.weboshelp.net/images/webos_application_file_structure.png)

appinfo.json
Code:
{
    "title": "News",
    "type": "web",
    "main": "index.html",
    "id": "com.palm.app.news",
    "version": "1.0",
    "icon": "icon.png"
}

index.html
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <title>News</title>
    <script src="/usr/palm/frameworks/mojo/mojo.js" type="text/javascript"
 x-mojo-version="1"></script>
    <script src="app/assistants/stage-assistant.js" type="text/javascript"></script>
    <script src="app/assistants/storyView-assistant.js"
 type="text/javascript"></script>

    <!-- application stylesheet should come in after the one loaded
 by the framework -->
    <link href="stylesheets/News.css" media="screen" rel="stylesheet"
 type="text/css" />
</head>

<body>
</body>
</html>

storyView-scene.html
Code:
<div id='storyViewScene'>
    <div class="palm-page-header multi-line">
            <div id="test" class="palm-page-header-wrapper">
                    <div id="storyViewTitle" class="title left">#{title}</div>
            </div>
    </div>
     <div id="storyViewSummary" class="itemFull">#{text}</div>
</div>
<div x-mojo-element="Button" id="previousStory"></div>
<div x-mojo-element="Button" id="nextStory"></div>

storyView-assistant.js
Code:
//  StoryViewAssistant(storyFeed, storyIndex)
//
//  Passed a story element, displays that element in a full scene view and offers options
//  for next story (right command menu button), previous story (left command menu button)
//  and to launch story URL in the browser (view menu).

function StoryViewAssistant(storyFeed, storyIndex) {

    //    Save the passed arguments for use in the scene.
    //
    this.storyFeed = storyFeed;
    this.storyIndex = storyIndex;
}

StoryViewAssistant.prototype.setup = function() {

 // Hide Previous Button if first story, and Next Button if last one

    if (this.storyIndex > 0)    {
        this.controller.setupWidget("previousStory",
            this.attributes = {
                disabledProperty: 'disabled'
                },
            this.model = {
                buttonLabel : "Previous",
                buttonClass: '',
                disabled: false
            });

            this.controller.listen('previousStory', Mojo.Event.tap,
              this.previousStory.bindAsEventListener(this));
    }    else    {
        $('previousStory').hide();
    }

    if (this.storyIndex < this.storyFeed.stories.length-1)    {
        this.controller.setupWidget("nextStory",
            this.attributes = {
                disabledProperty: 'disabled'
                },
            this.model = {
                buttonLabel : "Next",
                buttonClass: '',
                disabled: false
            });

        this.controller.listen('nextStory', Mojo.Event.tap,
          this.nextStory.bindAsEventListener(this));
    }    else     {
        $('nextStory').hide();
    }

};

StoryViewAssistant.prototype.previousStory = function(event) {
    Mojo.Controller.stageController.swapScene("storyView", this.storyFeed,
 this.storyIndex-1);
};

StoryViewAssistant.prototype.nextStory = function(event) {
    Mojo.Controller.stageController.swapScene("storyView", this.storyFeed,
 this.storyIndex+1);
};

StoryViewAssistant.prototype.activate = function(event) {

    $("storyViewTitle").innerHTML = this.storyFeed.stories[this.storyIndex].title;
    $("storyViewSummary").innerHTML = this.storyFeed.stories[this.storyIndex].text;

    if (this.storyFeed.stories[this.storyIndex].unReadStyle === unReadFormatting)   {
        this.storyFeed.numUnRead--;
        this.storyFeed.stories[this.storyIndex].unReadStyle = "";
    }
};

StoryViewAssistant.prototype.deactivate = function(event) {

};

StoryViewAssistant.prototype.cleanup = function(event) {

};

storyList-scene.html
Code:
<div class='palm-header'>
  <div class='palm-header-center'><span id='feedTitle'></span>
  </div>
</div>
<div class="palm-header-spacer"></div>
<div class="palm-list">
  <div id="storyListWgt" x-mojo-element="List"></div>
</div>

storyList-assistant.js
Code:
//
//    StoryListAssistant - Displays the feed's stories in a list, user taps display the
//       selected story in the storyView scene.
//
//    Arguments:
//        selectedFeed        Feed to be displayed
//

function StoryListAssistant(selectedFeedIndex) {
    this.feed = feedList[selectedFeedIndex];
    this.feedIndex = selectedFeedIndex;
}

StoryListAssistant.prototype.setup =  function() {

    //  Setup story list with standard news list templates.
    //
    this.controller.setupWidget("storyListWgt",
        this.storyAttr = {
            itemTemplate: "storyList/storyRowTemplate",
            listTemplate: "storyList/storyListTemplate",
            swipeToDelete: false,
            renderLimit: 40,
            reorderable: false
        },
        this.storyModel = {
            items: this.feed.stories
        }
    );

    this.controller.listen("storyListWgt", Mojo.Event.listTap,
      this.readStory.bindAsEventListener(this));
};

StoryListAssistant.prototype.activate =  function() {

    //  Set title into header
    $("feedTitle").innerHTML=this.feed.title;

    //  Update story list model in case unReadCount has changed
    this.controller.modelChanged(this.storyModel);

};

//    readStory - handler when user taps on displayed story, push that story to
//      the storyView scene

StoryListAssistant.prototype.readStory = function(event) {

    Mojo.Controller.stageController.pushScene("storyView", this.feed, event.index);
};


StoryListAssistant.prototype.deactivate = function(event) {
};

StoryListAssistant.prototype.cleanup = function(event) {
};


storyListTemplate.html
Code:
<div class="palm-list">#{listElements}</div>

storyRowTemplate.html
Code:
<div class="palm-row" x-mojo-tap-highlight="momentary">
  <div id="storyTitle" class="listTitle truncating-text #{unReadStyle}">#{title}</div>
  <div id="storyText" class="listText truncating-text">#{text}</div>
</div>

stage-assistant.js
Code:
//  Globals - used throughout the News application
//

var feedList = [];        //    News feed list

//  Feedlist entry is:
//  feedList[x].title                    String  Title entered by user
//  feedList[x].url                      String  Feed source URL in unescaped form
//  feedList[x].type                     String  Feed type: either rdf (rss1), rss (rss2) or atom
//  feedList[x].numUnRead                Integer How many stories are still unread
//  feedList[x].stories                  Array   Each entry is a complete story:
//    feedList[x].stories[y].title       String  Story title or headline
//    feedList[x].stories[y].text        String  Story text
//    feedList[x].stories[y].unReadStyle String  Style to apply when unRead
//    feedList[x].stories[y].url         String  Story url
//

//    Push default feeds onto list; these will get overwritten by what's stored in the database but these will be used otherwise

feedList.push({title:"Huffington Post",
    url:"http://feeds.huffingtonpost.com/huffingtonpost/raw_feed",
 type:"atom", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Google",
    url:"http://news.google.com/?output=atom", type:"atom", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"BBC News",
    url:"http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml", type:"rss",
    acFreq:0, numUnRead:0, stories:[]});
feedList.push({title:"New York Times",
     url:"http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml",
 type:"rss", acFreq:0, numUnRead:0, stories:[]});
feedList.push({title:"MSNBC",
    url:"http://rss.msnbc.msn.com/id/3032091/device/rss/rss.xml",
 type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"National Public Radio",
    url:"http://www.npr.org/rss/rss.php?id=1004", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Slashdot",
    url:"http://rss.slashdot.org/Slashdot/slashdot", type:"rdf", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Engadget",
    url:"http://www.engadget.com/rss.xml", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"The Daily Dish",
    url:"http://feeds.feedburner.com/andrewsullivan/rApM?format=xml",
 type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Guardian UK",
    url:"http://feeds.guardian.co.uk/theguardian/rss", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Yahoo Sports",
    url:"http://sports.yahoo.com/top/rss.xml", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"ESPN",
    url:"http://sports-ak.espn.go.com/espn/rss/news", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});
feedList.push({title:"Ars Technica",
    url:"http://feeds.arstechnica.com/arstechnica/BAaf", type:"rss", acFreq:0,
    numUnRead:0, stories:[]});

var curFeedIndex = 0;     //    Tracks current index for feedlist updates
var curFeedSource = feedList[0];

//  Constants

var unReadFormatting = "unReadStyle";       //      Prepended to story titles when unread

function StageAssistant () {

}

StageAssistant.prototype.setup = function() {
     this.controller.pushScene("storyList", curFeedIndex);
};

function StageAssistant () {
}

StageAssistant.prototype.setup = function() {
    this.controller.pushScene("storyView");
};



Title: Re: Sample Mojo code for webOS News Reader application
Post by: Ken Young on April 21, 2009, 11:40:40 PM
feedList-scene.html
Code:
<div class='palm-header'>
    <div class='palm-header-center'>News</div>
</div>
<div class="palm-header-spacer"></div>
 <div class="palm-list">
    <div id="feedListWgt" x-mojo-element="List"></div>
</div>
<!--    Adding text field within a drawer and group box   -->
<div id='feedDrawer' x-mojo-element="Drawer">
  <div class="palm-group">
    <div class="palm-group-title">

      <!-- Title and error status for invalid feeds                         -->
      <span id="add-feed-title" x-mojo-loc="">Add News Feed Source</span>
    </div>

    <div class="palm-group unlabeled">
      <div class="palm-list">

        <div class='palm-row first'>
          <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true">
            <div class="title">
              <div class="label">URL</div>
              <div id="newFeedURL" x-mojo-element="TextField" align="left"></div>
            </div>
          </div>
        </div>

        <div class='palm-row last'>
          <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true">
            <div class="title">
              <div class="label">Title</div>
              <div id="newFeedName" x-mojo-element="TextField" align="left"></div>
            </div>
           </div>
        </div>

      </div>
    </div>

    <div x-mojo-element="Button" id="okButton"></div>
  </div>
</div>

feedList-assistant.js
Code:
FeedListAssistant.prototype.setup = function() {

    //    Setup the feed list
    //
    this.controller.setupWidget("feedListWgt",
    this.feedWgtAttr = {
         itemTemplate:"feedList/feedRowTemplate",
         listTemplate:"feedList/feedListTemplate",
         swipeToDelete:true,
         renderLimit: 40,
         addItemLabel:"Add...",
         reorderable:true
    },
    this.feedWgtModel = {items: feedList});

//  Setup Drawer for add Feed; closed to start
         //
         this.controller.setupWidget('feedDrawer', {property:'myOpenProperty'}, this.feedDrawerModel={myOpenProperty:false});

        //    Setup text field for the new feed's URL
        //
        this.controller.setupWidget(
            "newFeedURL",
            this.urlAttributes = {
                property: "value",
                hintText: "RSS or ATOM feed",
                focus: true,
                limitResize: true,
                textReplacement: false,
                enterSubmits: false
            },
            this.urlModel = {value : ""});

    //    Setup text field for the new feed's name
    //
        this.controller.setupWidget(
            "newFeedName",
            this.nameAttributes = {
                    property: "value",
                    hintText: "Optional",
                    limitResize: true,
                    textReplacement: false,
                    enterSubmits: false
            },
            this.nameModel = {value : ""});

    //    Setup button and event handler
    //
    this.controller.setupWidget("okButton",
        this.attributes = {
            },
        this.model = {
            buttonLabel: "Add Feed",
            buttonClass: "addFeedButton",
            disabled: false
        });

    this.controller.listen('okButton', Mojo.Event.tap, this.checkIt.
bindAsEventListener(this));


    //    Setup event handlers list selection, add feed, delete feed and reorder feed list
    //
    this.controller.listen('feedListWgt', Mojo.Event.listTap,
      this.showFeed.bindAsEventListener(this));
    this.controller.listen('feedListWgt', Mojo.Event.listDelete,
      this.listDeleteHandler.bindAsEventListener(this));
    this.controller.listen('feedListWgt', Mojo.Event.listReorder,
      this.listReorderHandler.bindAsEventListener(this));

    //  Set feedsInitialized to false so that activate knows that it's being
    //  launched or pushed from the background
    //
    this.feedsInitialized = false;

    //  Start the Ajax Request sequence
    //
    this.updateFeedList();
};

//    feedRequest - function called to setup and make a feed request
//
//    Uses prototype's Ajax.Request and sets up callback routines:
//        onSuccess        feedRequestSuccess
//        onFailure        feedRequestFailure
//

FeedListAssistant.prototype.feedRequest = function(curFeed) {

    var request = new Ajax.Request(curFeed.url, {
        method: 'get',
        evalJSON: 'false',
        onSuccess: this.feedRequestSuccess.bind(this),
        onFailure: this.feedRequestFailure.bind(this)
    });
};

//    feedRequestFailure
//
//    Callback routine from a failed AJAX feed request (feedRequest);
//      post a simple failure error message with the http status code.
//
FeedListAssistant.prototype.feedRequestFailure = function(transport) {

    //    Use the Prototype template object to generate a string from the return status.
    //
    var t = new Template("Status #{status} returned from newsfeed request.");
    var m = t.evaluate(transport);
    Mojo.Log.info("........","Invalid feed - http failure (", m);
};

//    feedRequestSuccess
//
//    Callback routine from a successful AJAX feed request (feedRequest); use globals:
//        curFeedIndex    Index for the feed being updated
//        feedList        Holds the processed feeds, will have the current
//                         feed updated at exit; has old version of feed at entry
//

FeedListAssistant.prototype.feedRequestSuccess = function(transport) {

    var    feedError = errorNone;
    var t = new Template($L("Status #{status} returned from newsfeed request."));
    Mojo.Log.info("........","Feed Request Success: ", t.evaluate(transport));

    //    DEBUG - Work around
    if (transport.responseXML === null && transport.responseText !== null) {
            Mojo.Log.info("........","Request not in XML format - manually converting");
            transport.responseXML = new DOMParser().parseFromString
(transport.responseText, 'text/xml');
     }

    //    Process the feed, identifying the current feed index and passing in
    //      the transport object holding the updated feed data
    //
    feedError = ProcessFeed(transport, curFeedIndex);

    //    If successful processFeed returns errorNone
    if (feedError == errorNone)    {

        //    Update the feedList model object with the updated feed data in feedList
        this.feedWgtModel.items = feedList;
        this.controller.modelChanged(this.feedWgtModel);

    } else     {
        //    There was a feed process error
        if (feedError == errorUnsupportedFeedType)    {
            Mojo.Log.info("........","Feed ", this.nameModel.value,
             " is not a supported feed type. (#", errorUnsupportedFeedType, ").");
        }
    }

    //    Increment the index. If this is NOT the last feed then
    //      update the feedsource and request to retrieve the next feed
    //
    curFeedIndex++;
    if(curFeedIndex < feedList.length) {
        curFeedSource = feedList[curFeedIndex];
        this.feedRequest(curFeedSource);
    } else {

        //    Otherwise, this update is done. Reset index to 0 for next update;
        //      if the timer is null, the set the interval to feedUpdateInterval
        //
        curFeedIndex = 0;
        this.feedsInitialized = true;
    }
};

//    updateFeedList (FeedListAssistant) - called to cycle through the feeds either on activate or
//        after the interval timer has fired. This is split from feedRequest
//        so that the latter can be called on each feed. This is called once per
//        update cycle.
//

FeedListAssistant.prototype.updateFeedList = function() {
    // request fresh copies of all stories
    curFeedIndex = 0;
    curFeedSource = feedList[curFeedIndex];
    this.feedRequest(curFeedSource);
};

//    listDeleteHandler - triggered by deleting a feed from
//    the list and updates the feedList to reflect the deletion
//
FeedListAssistant.prototype.listDeleteHandler =  function(event) {

    var deleteIndex = event.index;
    feedList.splice(deleteIndex, 1);
    this.feedWgtModel.items = feedList;
};

//    listReorderHandler- triggered re-ordering feed list and
//    updates the feedList to reflect the changed order
//
FeedListAssistant.prototype.listReorderHandler =  function(event) {

    var  fromIndex = event.fromIndex;
    var  toIndex = event.toIndex;
    feedList.splice(fromIndex, 1);
    feedList.splice(toIndex, 0, event.item);
    this.feedWgtModel.items = feedList;
};

//  addNewFeed - triggered by "Add..." item in feed list and invokes the AddDialog
//    Assistant defined above.
//
FeedListAssistant.prototype.addNewFeed = function() {

    this.feedDrawerModel.myOpenProperty = !this.feedDrawerModel.myOpenProperty;
    this.controller.modelChanged(this.feedDrawerModel, this);
};

feedListTemplate.html
Code:
<div class="palm-list">#{listElements}</div>

feedRowTemplate.html
Code:
div class="palm-row" x-mojo-tap-highlight="momentary">
    <div class="palm-row-wrapper">
        <div class="icon right"><div class="unReadCount">#{numUnRead}</div></div>
        <div class="title truncating-text">#{title}</div>
    </div>
</div>

news.css
Code:
/*    News CSS - App overrides of palm scene and widget styles.     */

.palm-page-header-wrapper    {
    padding-top: 5px;
    padding-left: 12px;
    padding-right: 10px;
    font-weight: bold;
}

.itemFull {
    vertical-align: top;
    font-size: 12pt;
    font-weight: normal;
    text-align: left;
    padding-right: 5px;
    padding-left: 5px;
}
.listTitle {
    padding-top: 10px;
    padding-right: 5px;
    padding-left: 5px;
    font-size: 14pt;
    text-align: left;
    font-weight: normal;
}

.listText {
    vertical-align: bottom;
    padding-bottom: 10px;
    padding-right: 10px;
    padding-left: 5px;
    font-size: 10pt;
    height: 20px;
    font-weight: normal;
    text-align: left;
    overflow-x: hidden;
    overflow-y: hidden;
}
.unReadStyle {
    font-weight: bold;
}
.unReadCount {
    background:  url('/'/../images/unread-background.png'') no-repeat ;
    width: 40px;
    height: 50px;
    position: relative;
    margin-top: 5px;
    top: 6px;
    padding-top: 3px;
    vertical-align: middle;
    text-align: center;
    color: white;
}
.addFeedButton  {
    width: 263px;
}

sources.json
Code:
[
  {
    "source": "app\/controllers\/stage-assistant.js"
  },
  {
    "source": "app\/controllers\/storyList-assistant.js",
    "scenes": "storyList"
  },
  {
    "source": "app\/controllers\/storyView-assistant.js",
    "scenes": "storyView"
  }
]