Part 5: Working with Multiple Views
These days, the typical RSS feed story contains only a headline and the first sentence or two of text--to read the full article, you have to go to the originating Web site. In light of that fact, let's make our application more useful by enabling the user to click through from an item in the list of feed stories to the full, original version of the story. To do this, we'll introduce a second view--for displaying the original article--to go along with our existing view, which will continue to display the search box and search results.
We'll also take this opportunity to refactor our search-related code out of FeedReader.js and into its own file. This refactoring not only makes our application easier to read and understand; it also exemplifies one of the basic design goals of Enyo--the encapsulation of data. By encapsulation, we mean that application objects should serve as coordinators of specialized data-processing objects, without having detailed knowledge of the data being processed. (As an example of a coordinating function, FeedReader.js will continue to house the code for determining which view is currently visible.)
Search.js
Here are the contents of the new file, Search.js:
enyo.kind({ name: "MyApps.Search", kind: enyo.VFlexBox, events: { onLinkClick: "", onSelect: "" }, components: [ {name: "getFeed", kind: "WebService", onSuccess: "gotFeed", onFailure: "gotFeedFailure"}, {kind: "RowGroup", caption: "Feed URL", components: [ {kind: "InputBox", components: [ {name: "feedUrl", kind: "Input", flex: 1, value: "http://feeds.bbci.co.uk/news/rss.xml"}, {kind: "Button", caption: "Get Feed", onclick: "btnClick"} ]} ]}, {kind: "Scroller", flex: 1, components: [ {name: "list", kind: "VirtualRepeater", onSetupRow: "getListItem", components: [ {kind: "Item", layoutKind: "VFlexLayout", components: [ {name: "title", kind: "Divider"}, {name: "description", kind: "HtmlContent", onLinkClick: "doLinkClick"} ], onclick: "listItemClick" } ] } ]} ], create: function() { this.inherited(arguments); this.results = []; }, btnClick: function() { var url = "http://query.yahooapis.com/v1/public/yql?q=select%20" + "title%2C%20description%2C%20link%20from%20rss%20where%20url%3D%22" + this.$.feedUrl.getValue() + "%22&format=json&callback="; this.$.getFeed.setUrl(url); this.$.getFeed.call(); }, getListItem: function(inSender, inIndex) { var r = this.results[inIndex]; if (r) { this.$.title.setCaption(r.title); this.$.description.setContent(r.description); return true; } }, gotFeed: function(inSender, inResponse) { this.results = inResponse.query.results.item; this.$.list.render(); }, gotFeedFailure: function(inSender, inResponse) { enyo.log("got failure from getFeed"); }, listItemClick: function(inSender, inEvent) { var feed = this.results[inEvent.rowIndex]; this.doSelect(feed); } });
Those familiar with the earlier installments of our tutorial will recognize that most of this code has been copied verbatim from the previous version of FeedReader.js. Let's look at the changes that were made to enable the new functionality.
First, we've made a change to retrieve additional feed data--in btnClick we've extended the Yahoo API query to obtain the original-article URL for each story in addition to the title and description.
Second, we've added new event-related code. The Item object, for example, now has an onclick handler. When a click occurs within the Item (but not on a link), it triggers the new listItemClick method.
Next, in the Item kind's components block, we've made several changes to the description component in order to accommodate the growing number of RSS feed providers that are embedding hyperlinks in their feed description data. The description is now defined as an object of kind HtmlContent, and we've assigned it a handler for onLinkClick events. (An onLinkClick event is an event generated by an HtmlContent object when the user clicks on a link within the HtmlContent.) The statement onLinkClick: "doLinkClick" is a shorthand way of forwarding the onLinkClick event to the owner of the HtmlContent control, which is our FeedReader object.
(Note: If you want the content in an HtmlContent to be subject to filtering--i.e., escaping of the ampersand (&), less than (<), and greater than (>) characters--set the allowHtml property of the HtmlContent to false. allowHtml is actually false for controls by default, but HtmlContent explicitly overrides this to make its allowHtml default to true.)
Finally, notice that both onLinkClick and a second new event, onSelect, are defined in the events block. The onSelect event is fired by the call to this.doSelect in listItemClick.
FeedReader.js
New event-related code can also be found in FeedReader.js, which now looks like this:
enyo.kind({ name: "MyApps.FeedReader", kind: enyo.VFlexBox, components: [ {kind: "PageHeader", components: [ {kind: enyo.VFlexBox, content: "Enyo FeedReader", flex: 1}, {name: "backButton", kind: "Button", content: "Back", onclick: "goBack"} ]}, {name: "pane", kind: "Pane", flex: 1, onSelectView: "viewSelected", components: [ {name: "search", className: "enyo-bg", kind: "MyApps.Search", onSelect: "feedSelected", onLinkClick: "linkClicked"}, {name: "detail", className: "enyo-bg", kind: "Scroller", components: [ {name: "webView", kind: "WebView", className: "enyo-view"} ] } ] } ], create: function() { this.inherited(arguments); this.$.pane.selectViewByName("search"); }, feedSelected: function(inSender, inFeed) { this.$.pane.selectViewByName("detail"); this.$.webView.setUrl(inFeed.link); }, linkClicked: function(inSender, inUrl) { this.$.webView.setUrl(inUrl); this.$.pane.selectViewByName("detail"); }, viewSelected: function(inSender, inView) { if (inView == this.$.search) { this.$.webView.setUrl(""); this.$.backButton.hide(); } else if (inView == this.$.detail) { this.$.backButton.show(); } }, goBack: function(inSender, inEvent) { this.$.pane.back(inEvent); } });
Note that the search view has now become a component within a Pane object (called "pane"). Our second view ("detail") is also defined as a component (specifically, a Scroller wrapped around a WebView) within the same pane. Both views are styled with the CSS class "enyo-bg", and we control which one is currently visible by calling this.$.pane.selectViewByName(viewName), as illustrated in create and feedSelected. When the FeedReader is instantiated, the search view is selected; when the user clicks on a story, the detail view is selected.
To allow the user to navigate from the detail view back to the search view, we've added a new Back button. When this is pressed, we'll call the new goBack method.
While most of the code that was previously in FeedReader.js has been moved to Search.js, the current incarnation of FeedReader has gained four new methods related to view management:
-
The
goBackmethod, as we've just seen, responds to clicks of theBackbutton. -
The
feedSelectedmethod is called whenFeedReaderreceives anonSelectevent from thesearchview. The overall sequence of events is as follows:-
In the
searchview, a list item (feed story) is clicked, triggering a call toSearch.listItemClick. -
Search.listItemClickgenerates anonSelectevent. -
FeedReaderdetects theonSelectevent and callsFeedReader.feedSelected. -
FeedReader.feedSelectedmakes thedetailview active and sets the URL for theWebView(named"webView") inside thedetailview.
-
-
Similarly, the
linkClickedmethod sets the URL forwebViewin response toonLinkClickevents. (With respect to encapsulation, note that whileFeedReaderreceives object data from bothonSelectandonLinkClickevents, those objects are not processed here, but are passed right back to thedetailview.) -
The
viewSelectedmethod is the handler foronSelectViewevents generated bythis.$.pane. If we're navigating to thesearchview,viewSelectedclears the contents ofwebViewin preparation for the next time thedetailview is displayed. It also hides theBackbutton, since that button isn't needed in this view. If we're navigating to thedetailview,viewSelectedmakes theBackbutton visible.
depends.js
There's one more thing we need to do to get this all to work--update depends.js to reflect the presence of the new Search.js file:
enyo.depends( "source/FeedReader.js", "source/Search.js", "css/FeedReader.css" );
Taken together, the foregoing changes bring us from here

to here

(Continue to Part 6: Application Preferences.)