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 goBack method, as we've just seen, responds to clicks of the Back button.

  • The feedSelected method is called when FeedReader receives an onSelect event from the search view. The overall sequence of events is as follows:

    • In the search view, a list item (feed story) is clicked, triggering a call to Search.listItemClick.

    • Search.listItemClick generates an onSelect event.

    • FeedReader detects the onSelect event and calls FeedReader.feedSelected.

    • FeedReader.feedSelected makes the detail view active and sets the URL for the WebView (named "webView") inside the detail view.

  • Similarly, the linkClicked method sets the URL for webView in response to onLinkClick events. (With respect to encapsulation, note that while FeedReader receives object data from both onSelect and onLinkClick events, those objects are not processed here, but are passed right back to the detail view.)

  • The viewSelected method is the handler for onSelectView events generated by this.$.pane. If we're navigating to the search view, viewSelected clears the contents of webView in preparation for the next time the detail view is displayed. It also hides the Back button, since that button isn't needed in this view. If we're navigating to the detail view, viewSelected makes the Back button 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

Enyo FeedReader - story list view

to here

Enyo FeedReader - story detail view

(Continue to Part 6: Application Preferences.)