Part 6: Application Preferences

In this installment of the tutorial, we'll do something that should be relevant to just about every application developer--save and retrieve app-specific preferences. Specifically, we'll enable the user to save a default feed URL, which will be used to pre-populate the Feed URL box each time the FeedReader app launches.

Preferences.js

In keeping with Enyo's emphasis on encapsulation, we'll create a new file, Preferences.js, to house the code for processing preference data. It will also contain some simple UI for collecting the default feed URL from the user. Here are the contents of our new file:

enyo.kind({
  name: "MyApps.Preferences",
  kind: enyo.VFlexBox,
  events: {
      onReceive: "",
      onSave: "",
      onCancel: ""
  },
  components: [
      {
          name: "getPreferencesCall",
          kind: "PalmService",
          service: "palm://com.palm.systemservice/",
          method: "getPreferences",
          onSuccess: "getPreferencesSuccess",
          onFailure: "getPreferencesFailure"
      },
      {
          name: "setPreferencesCall",
          kind: "PalmService",
          service: "palm://com.palm.systemservice/",
          method: "setPreferences",
          onSuccess: "setPreferencesSuccess",
          onFailure: "setPreferencesFailure"
      },
      {kind: "PageHeader", content: "Enyo FeedReader - Preferences"},
      {kind: "VFlexBox",
          components: [
              {kind: "RowGroup", caption: "Default Feed", components: [
                  {name: "defaultFeedInput", kind: "Input"}
              ]},
              {kind: "HFlexBox", pack: "end", style: "padding: 0 10px;",
                  components: [
                      {name: "saveButton", kind: "Button",
                          content: "Save", onclick: "saveClick"},
                      {width: "10px"},
                      {name: "cancelButton", kind: "Button",
                          content: "Cancel", onclick: "cancelClick"}
                  ]
              }
          ]
      },
  ],
  create: function() {
      this.inherited(arguments);
      this.$.getPreferencesCall.call(
      {
          "keys": ["defaultFeed"]
      });
      // keep this updated with the value that's currently saved to the service
      this.savedUrl = "";
  },
  getPreferencesSuccess: function(inSender, inResponse) {
      this.savedUrl = inResponse.defaultFeed;
      this.$.defaultFeedInput.setValue(this.savedUrl);
      this.doReceive(this.savedUrl);
  },
  getPreferencesFailure: function(inSender, inResponse) {
      enyo.log("got failure from getPreferences");
  },
  setPreferencesSuccess: function(inSender, inResponse) {
      console.log("got success from setPreferences");
  },
  setPreferencesFailure: function(inSender, inResponse) {
      console.log("got failure from setPreferences");
  },
  showingChanged: function() {
      // reset contents of text input box to last saved value
      this.$.defaultFeedInput.setValue(this.savedUrl);
  },
  saveClick: function(inSender, inEvent) {
      var newDefaultFeedValue = this.$.defaultFeedInput.getValue();
      this.$.setPreferencesCall.call(
      {
          "defaultFeed": newDefaultFeedValue
      });
      this.savedUrl = newDefaultFeedValue;
      this.doSave(newDefaultFeedValue);
  },
  cancelClick: function() {
      this.doCancel();
  }
});

Let's step through the file from top to bottom.

Each MyApps.Preferences object will be of kind VFlexBox.

There are three new events--onSave and onCancel, which are triggered by the Save and Cancel buttons, respectively, as well as onReceive, which is fired each time we receive a successful response to a request for preference data.

The components block starts with definitions for two services, getPreferencesCall and setPreferencesCall. Both of these are derived from the PalmService kind, and both call methods on com.palm.systemservice. To get or set preference data, we'll use the methods getPreferencesCall.call and setPreferencesCall.call, respectively.

Continuing in the components block, the next thing we see is a PageHeader, which was previously declared in FeedReader.js. It makes sense to move the PageHeader into the individual views because that gives us the flexibility to customize PageHeader content on a per-view basis (e.g., "Enyo FeedReader - Preferences"). Also, we've omitted the Back button from the PageHeader, since this view has other controls that will trigger navigation events.

Then comes our UI--a RowGroup with an Input inside for collecting the default feed URL, and an HFlexBox containing saveButton and cancelButton. Notice that the HFlexBox has a "style" property that specifies 10 pixels of padding between the box's content and the box's right edge. There's also a "pack" property, whose value is set to "end". Since an HFlexBox is horizontally oriented, this tells the box to begin packing its contents from the right (i.e., the "end"), leaving any extra space on the left.

The rest of the file consists of various methods:

  • create invokes getPreferencesCall.call and sets up a variable (this.savedUrl) where we'll store the results from the call.

  • getPreferencesSuccess is triggered if getPreferencesCall.call returns successfully. It stores the returned value--extracted from a key called "defaultFeed"--in this.savedUrl, then updates the text input box with the same value. Finally, it fires an onReceive event with the returned value as the argument. The onReceive event is then handled by FeedReader.

  • getPreferencesFailure is triggered if getPreferencesCall.call fails. Right now it just notes the failure in the log.

  • Similarly, setPreferencesSuccess and setPreferencesFailure log the success or failure of setPreferencesCall.call.

  • showingChanged is called automatically each time the visibility of the preferences view changes. It resets the value of the text input box to the last saved value (this.savedUrl).

  • saveClick is where most of the action is. When the Save button is clicked, saveClick uses the current value of the text input box to populate the params object sent to setPreferencesCall.call. (Note that we are instructing the service to store the value in a key called "defaultFeed".) saveClick then updates the value of this.savedUrl. Finally, saveClick fires an onSave event, which is handled by FeedReader.

  • cancelClick fires an onCancel event, which is also handled by FeedReader.

Here's a glimpse of the finished preferences view in action:

Enyo FeedReader - preferences view

FeedReader.js

Turning now to FeedReader.js, that file reflects a host of changes including--but not limited to--the addition of event handlers for onSave, onCancel, and onReceive. Its components block now looks like this:

components: [
  {name: "pane", kind: "Pane", flex: 1,
      components: [
          {name: "search", className: "enyo-bg", kind: "MyApps.Search",
              onSelect: "feedSelected", onLinkClick: "linkClicked"},
          {name: "detail", className: "enyo-bg", kind: "MyApps.Detail",
              onBack: "goBack"},
          {
              name: "preferences",
              className: "enyo-bg",
              kind: "MyApps.Preferences",
              onReceive: "preferencesReceived",
              onSave: "preferencesSaved",
              onCancel: "goBack"
          }
      ]
  },
  {kind: "AppMenu",
      components: [
          {caption: "Preferences", onclick: "showPreferences"},
      ]
  }
]

You may notice that the detail view has been broken out into a separate file (Detail.js), which defines the MyApps.Detail kind. We'll examine the detail view in due time, but for now let's continue to follow the code relating to preferences.

We've declared a new view ("preferences") of kind MyApps.Preferences in FeedReader's main display pane. Within the declaration, you'll find the names of the handler methods for the three new events from Preferences.js.

Access to the preferences view is provided by a new AppMenu, which contains one item, called "Preferences". When clicked, that item triggers the new method FeedReader.showPreferences, which--unsurprisingly--shows the preferences view.

In the browser, the AppMenu should look something like this:

Enyo FeedReader - AppMenu

(Note: When running in the browser, you can bring up the AppMenu by using the keystroke combination

CTRL + `

If the AppMenu is visible and you want to dismiss it without making a selection, click any part of the browser window outside of the AppMenu.)

Now let's take a look at the methods in our revised FeedReeder.js:

  openAppMenuHandler: function() {
      this.$.appMenu.open();
  },
  closeAppMenuHandler: function() {
      this.$.appMenu.close();
  },
  feedSelected: function(inSender, inFeed) {
      this.$.pane.selectViewByName("detail");
      this.$.detail.setUrl(inFeed.link);
  },
  linkClicked: function(inSender, inUrl) {
      this.$.detail.setUrl(inUrl);
      this.$.pane.selectViewByName("detail");
  },
  showPreferences: function() {
      this.$.pane.selectViewByName("preferences");
  },
  preferencesReceived: function(inSender, inDefaultUrl) {
      this.$.search.setFeedUrl(inDefaultUrl);
  },
  preferencesSaved: function(inSender, inFeedUrl) {
      this.$.search.setFeedUrl(inFeedUrl);
      this.$.pane.back();
  },
  goBack: function(inSender, inEvent) {
      this.$.pane.back(inEvent);
  }

The current version of FeedReader contains eight methods, whereas the previous version had only five. Of those five methods, one, goBack, has been carried over unchanged, while two others, feedSelected and linkClicked, have changed only minimally (both now call setUrl on this.$.detail instead of this.$.webView, since webView has been moved into Detail.js). The other two methods, create and viewSelected, have been removed, with their functionality being relocated to the kind definitions of the individual views, as appropriate.

Let's look at the five newly-added methods:

  • openAppMenuHander and closeAppMenuHandler are fairly straightforward, being the handlers for requests to open or close the AppMenu. We haven't defined the open and close events explicitly; they are automatically generated by Enyo in response to specific user input.

  • showPreferences, as mentioned earlier, shows the preferences view when the "Preferences" item in the AppMenu is clicked.

  • preferencesReceived is the handler for onReceive events generated by Preferences.js. If a request for saved default feed data returns successfully, preferencesReceived passes the returned value to the search view, so it can update the contents of its text input box.

  • preferencesSaved is the handler for onSave events generated by Preferences.js. If the user saves a default feed value, this method (like the previous one) passes the saved value to the search view. We then navigate back to whichever view was active before the preferences view was displayed.

Detail.js

Now let's take a look at Detail.js. While the file itself is new, its contents should look pretty familiar:

enyo.kind({
  name: "MyApps.Detail",
  kind: "VFlexBox",
  events: {
      onBack: ""
  },
  published: {
      url: ""
  },
  components: [
      {kind: "PageHeader", components: [
          {name: "headerText", kind: enyo.VFlexBox,
              content: "Enyo FeedReader", flex: 1},
          {name: "backButton", kind: "Button", content: "Back",
              onclick: "backClick"}
      ]},
      {kind: "Scroller", flex: 1, components: [
          {name: "webView", kind: "WebView", className: "enyo-view"}
      ]}
  ],
  backClick: function() {
      this.doBack();
  },
  showingChanged: function() {
      if (!this.showing) {
          this.$.webView.setUrl("");
      }
  },
  urlChanged: function() {
      this.$.webView.setUrl(this.url);
  }
});

The most important things to note here are the published event, onBack, and the published property, "url".

The onBack event is fired by the call to this.doBack() in backClick, which in turn is triggered by a click of the Back button. (Notice that the PageHeader with Back button has been moved here from FeedReader.js, since this is the only view that makes use of the button.)

Ultimately, the onBack event is handled by FeedReader, whose goBack method does the actual work of navigating back to the previous view. As you may recall, we explicitly specified FeedReader.goBack as the handler for onBack events originating from the detail view when declaring the view in the components block of FeedReader.js.

Now let's turn our attention to the published property url and its associated method, urlChanged.

By definition, the published properties of an object are accessible by outside objects. Published properties are declared in a special "published" block, with each property specifying its name, followed by a colon, followed by the default value in quotes.

If you look back at FeedReader.linkClicked, you'll notice that it includes a call to Detail.setUrl. As you might recall from Part 3 of the tutorial, when a property is declared as published, Enyo automatically generates getter and setter methods for it. In this example, those methods are named getUrl and setUrl, respectively.

In addition, if you want certain side effects to be triggered automatically when the value of a published property is changed (i.e., when its setter method is called), Enyo lets you do this by implementing a property changed method. Our example includes just such a method, urlChanged. So when FeedReader.linkClicked calls Detail.setUrl, the following happens in the Detail object:

  1. The value of this.url is set to the value passed in from FeedReader.
  2. Because we've implemented a urlChanged method, this.urlChanged is called automatically.

Also worth mentioning is the showingChanged method. We saw the implementation of a showingChanged method earlier (in Preferences.js) and noted that Enyo calls the method automatically when the visibility of the object changes. Here in Detail.showingChanged we make a related discovery--that an object's visibility state can be determined from the value of its showing property. This means we can move the conditional call to this.$.webView.setUrl("") out of the (now-deleted) FeedReader.viewSelected method, and into showingChanged.

depends.js

Before we forget, because we've added new files (Detail.js and Preferences.js) to our application, we need to update the contents of depends.js:

enyo.depends(
  "source/FeedReader.js",
  "source/Search.js",
  "source/Detail.js",
  "source/Preferences.js",
  "css/FeedReader.css"
);

Search.js

Finally, we've made a couple of minor changes to Search.js.

Like Preferences.js and Detail.js, Search.js now contains a PageHeader at the top of its components block:

{kind: "PageHeader", content: "Enyo FeedReader"}

We've also added a simple new method for setting the contents of this.$.feedUrl:

setFeedUrl: function(inUrl) {
  if (inUrl) {
      this.$.feedUrl.setValue(inUrl);
  }
}

Using Mock Data

You'll recall that Enyo allows us to do much of our application development within a WebKit-based desktop browser, such as Safari or Chrome. Indeed, we've continued to use the browser as our development environment throughout the course of this tutorial. We've seen that this environment is well-suited to UI development, of course, but we've also been able to perform many lower-level tasks, such as communicating with Web-based services.

Still, there are certain resources that are only available on the device. One key example is the ability to access services of type PalmService--i.e., to access a device-side database of live user data. The storage system on the device simply doesn't exist in the browser. If the user generates new content (say, by setting a default RSS feed URL) in an Enyo app running in the browser, there is no mechanism for storing that content for later use.

To help compensate for this, Enyo provides a way to simulate the data returned from a PalmService request. This is done by utilizing a MockPalmService, which reads in static data from a file of your own creation. The file lives in a subdirectory called "mock" under the application directory, and its name is of the form GRANDPARENT_PARENT_SERVICE.json. Whenever a PalmService call is made by an application running in the browser, Enyo will automatically look for mock service data in this location.

(Note: It's also possible to test service calls from an Enyo app running in the browser by attaching a device to your computer via USB and routing calls to services running on the device. However, a detailed discussion of this approach lies outside the scope of the present tutorial.)

In our tutorial app, in order to test the Preferences functionality, we've created mock data for getPreferencesCall and setPreferencesCall, the two service calls found in Preferences.js. Our mock data resides in files named feedReader_preferences_getPreferencesCall.json and feedReader_preferences_setPreferencesCall.json, located in the FeedReader/mock/ directory.

Here are the contents of the two files:

feedReader_preferences_getPreferencesCall.json

{
  "defaultFeed": "Your default feed here",
  "returnValue": true
}

feedReader_preferences_setPreferencesCall.json

{
  "returnValue": true
}

Note that returnValue must be set to true in order for the data to be returned.

Note also that data can only be read from mock data files; it cannot be written to the files.

Finally, we'll need to make a slight change to the <script> tag in index.html, which will now look like this:

<script src="../../enyo/enyo.js" launch="nobridge"
  type="text/javascript"></script>

The addition of launch="nobridge" tells an Enyo app running in the browser to look for mock data locally, rather than directing its service calls to a device attached via USB.

Enyo FeedReader - preferences view showing mock data

(Continue to Epilogue.)