Lists were introduced in Chapter 3 with several extensive examples. Even so, some major list features weren’t touched on. We’ll take a look at some more advanced features here.
With all list widgets, you can intervene in the middle of the list rendering to provide some intermediate formatting to list items or to insert dividers between rows. After a brief review of those features, we will add a Filter List widget to News to implement a search feature. This is a good example of a dynamic list, something you can use in many different types of applications.
The formatters
property is
a hash of property names to formatter functions, like this:
{timeValue: this.myTimeFormatter, dayOfWeek: this.dayIndexToString, ... }
Before rendering the relevant HTML templates, the formatter
functions 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 modified new names so that the unformatted value is
still accessible from inside the HTML template. Formatted values have
the text “Formatted” appended to their names. In the example above, the
HTML template could refer to #{timeValueFormatted}
in order to render the
output from the myTimeFormatter()
function. Formatter functions receive the relevant property value as the
first argument, and the appropriate model object or items element as the
second.
You can add dividers to your lists; they are particularly useful for long lists. You will
specify the function name to the dividerFunction
property and a template to dividerTemplate
. If no template is specified,
the framework will use the default, a single-letter alpha divider
(list-divider.html, styled with the
palm-alpha-divider
class).
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.
Use a Filter List widget when your list is best navigated with a search field, particularly one where you would like to instantly filter the list as each character is typed into the field. It is intended to display a variable length list of objects, built by a special callback function.
The widget includes a text field displayed above a list, where the
list is the result of applying the contents of the text field through an
application-specific callback function against some off-screen data
source. The text field is hidden when empty, but it is given focus as
soon as any key input is received. At the first keystroke, the field is
displayed with the key input (after a delay; specified via the
delay
attribute—default is 300ms), and the framework
calls the function specified by filterFunction
.
The framework calls the filterFunction
, which is similar to the
itemsCallback
function, in the base
List widget (see Chapter 3) when data is needed for
displaying list entries. You provide the filterFunction
, with arguments for the list
widget element, offset, and count, similar to itemsCallback
, plus an additional argument,
filterString
.
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.
The filter list will display a spinner in the text field while the
list is being built, and it is replaced with an entry count when done.
To set the count properly, call the widget’s setCount(totalSubsetSize)
, where totalSubsetSize
is the number of entries in
the list. To set the list length, call setLength(totalSubsetSize)
; the length is a
dependency of some internal widget functions and needs to be set
accurately.
Search is one of the best applications for the Filter List
widget. You can start typing on the feedList
scene and quickly search all feeds
for a keyword, viewing the results in the same list format used for
the individual feed lists. It’s simple to access and powerful.
The Filter List is declared and set up conventionally, but
requires a filter function called to process the keyword entries and
returns a list for display. In the News design, we’re going to put the
search results into a temporary list that is structured like the feed
list. We will display it using the storyList
assistant.
Because the Filter List search field is going into the feedList
view, it will be necessary to hide
the feed list and feature story when in search mode. Start by adding
the Filter List declaration at the top of feedList-scene.html and wrap the rest of
the scene in a div with feedListMain
as its ID. This will be used
later to hide the rest of the scene:
<!-- Search Field --> <div id="searchFieldContainer"> <div x-mojo-element="FilterList" id="startSearchField"></div> </div> <div id="feedListMain"> <!-- Rotating Feature Story --> . . .
Now set up the widget in feedList-assistant.js, reusing the storyList
templates, preparing to format the
search results in a storyList
scene. Identify the filter function as this.searchList
, and use a standard delay of
300 milliseconds. This is the default, so this step can be omitted,
but you may need to tune the behavior, so it’s not a bad idea to add
it at the beginning:
// Setup the search filterlist and handlers; this.controller.setupWidget("startSearchField", { itemTemplate: "storyList/storyRowTemplate", listTemplate: "storyList/storyListTemplate", filterFunction: this.searchList.bind(this), renderLimit: 70, delay: 300 }, this.searchFieldModel = { disabled: false }); this.viewSearchStoryHandler = this.viewSearchStory.bindAsEventListener(this); this.controller.listen("startSearchField", Mojo.Event.listTap, this.viewSearchStoryHandler); this.searchFilterHandler = this.searchFilter.bindAsEventListener(this); this.controller.listen("startSearchField", Mojo.Event.filter, this.searchFilterHandler, true);
Add two listeners, one for the tap event and the other for a filter event. The filter event is used on Filter Field and Filter List only. It is sent after the defined delay once the filter field is activated, on the first character entry, and immediately when the field is cleared. Listening for this event allows you to do some pre- and postprocessing with the widget or the scene.
With News, we want to hide the rest of the scene when the first
character is typed, and restore it when the field is cleared. To do
this, add this new method in the feedList
assistant; it simply calls the
Prototype methods hide()
and
show()
of the div element, feedListMain
, in
feedlist-scene.html:
// searchFilter - triggered by entry into search field. First entry will // hide the main feedList scene - clearing the entry will restore the scene. // FeedListAssistant.prototype.searchFilter = function(event) { Mojo.Log.info("Got search filter: ", event.filterString); var feedListMainElement = this.controller.get("feedListMain"); if (event.filterString !== "") { // Hide rest of feedList scene to make room for search results feedListMainElement.hide(); } else { // Restore scene when search string is null feedListMainElement.show(); } };
After the filter event is sent, the framework calls the function
assigned to the filterFunction
property in the Filter List widget’s attributes; in this case,
this.searchList()
, which is added also
to feedList-assistant.js and shown
here:
// searchList - filter function called from search field widget to update the // results list. This function will build results list by matching the // filterstring to the story titles and text content, and then return the // subset of the list based on offset and size requested by the widget. // FeedListAssistant.prototype.searchList = function(filterString, listWidget, offset, count) { var subset = []; var totalSubsetSize = 0; this.filter = filterString; // If search string is null, return empty list, otherwise build results list if (filterString !== "") { // Search database for stories with the search string; push matches var items = []; // Comparison function for matching strings in next for loop var hasString = function(query, s) { if(s.text.toUpperCase().indexOf(query.toUpperCase())>=0) { return true; } if(s.title.toUpperCase().indexOf(query.toUpperCase())>=0) { return true; } return false; }; for (var i=0; i<this.feeds.list.length; i++) { for (var j=0; j<this.feeds.list[i].stories.length; j++) { if(hasString(filterString, this.feeds.list[i].stories[j])) { var sty = this.feeds.list[i].stories[j]; items.push(sty); } } } this.entireList = items; Mojo.Log.info("Search list asked for items: filter=", filterString, " offset=", offset, " limit=", count); // Cut down list results to just the window asked for by the widget var cursor = 0; while (true) { if (cursor >= this.entireList.length) { break; } if (subset.length < count && totalSubsetSize >= offset) { subset.push(this.entireList[cursor]); } totalSubsetSize++; cursor++; } } // Update List listWidget.mojo.noticeUpdatedItems(offset, subset); // Update filter field count of items found listWidget.mojo.setLength(totalSubsetSize); listWidget.mojo.setCount(totalSubsetSize); };
The function definition is filterFunction (filterString,
listWidget, offset, limit)
, using the arguments shown in
Table 5-1.
Table 5-1. FilterFunction arguments
Argument | Type | Description |
---|---|---|
| String | The contents of the filter field or the search string to be used |
| Object | The DOM node for the list widget requesting the items |
| Integer | Index in the list of the first desired item model object (zero-based) |
| Integer | Count of the number of item model objects requested |
Assuming the filterString
isn’t empty, which it shouldn’t be, the first part of the method will
do a primitive match against all the story titles and text content
across all feeds. The results are pushed into the items array and
assigned to this.entireList
when
complete.
The list is cut down to just the portion of the list that was
requested by the offset
and the
count
, and is assigned to subset
, which is returned with the offset by
calling listWidget.mojo.noticeUpdatedItems(offset,
items)
, using the arguments shown in Table 5-2. This is a method of
listWidget
, an argument passed by
the framework.
Table 5-2. NoticeUpdatedItems arguments
Argument | Type | Description |
---|---|---|
| Integer | Index in the list of
the first object in |
| Array | An array of the list item model objects that have been loaded for the list |
Finish up with calls to listWidget.mojo.setLength(totalSubsetSize)
and listWidget.mojo.setCount(totalSubsetSize)
to
set the list length and the results count for the counter displayed in
the filter field.
With these changes, users can type at any time into the feedList
scene to see the filter field
display and the results presented in list form below, similar to what
is shown in Figure 5-11.
After the list is built, the tap event indicates a user
selection of a list entry, just as it does for a conventional list.
Since the search list is built as a stories array, News responds to a
tap by creating a temporary feed and pushing the storyView
scene with that feed. Here’s the
viewSearchStory()
method:
// viewSearchStory - triggered by tapping on an entry in the search // results list will push the storyView scene with the tapped story. // FeedListAssistant.prototype.viewSearchStory = function(event) { var searchList = { title: "Search for: "+this.filter, stories: this.entireList }; var storyIndex = this.entireList.indexOf(event.item); Mojo.Log.info("Search display selected story with title = ", searchList.title, "; Story index - ", storyIndex); Mojo.Controller.stageController.pushScene("storyView", searchList, storyIndex); };
As shown in Figure 5-12, as well as viewing the selected story, users can tap Next and Previous to view each story in the results list.
18.117.145.11