List
From WebOS101
Contents |
Introduction
Mojo's list widget allows for the display of lists of information and widgets. List contents can be static or dynamic. Lists are also a common source of problems for programmers new to Mojo widgets. These problems are caused partly because the documentation available on Lists is somewhat incomplete or out of date and partly because Lists are so powerful.
Like most widgets List widgets require a fragment of HTML, a model and attributes. They must be instantiated before they can be displayed. Unlike most other widgets Lists also supports three template files: An optional list template, an item template, an optional dividerTemplate and an optional emptyTemplate.
List Setup
Attributes
To set up a list you must define the attributes that the list will have.
this.listAttributes =
{
itemTemplate: "sceneName/rowTemplate",
listTemplate: "scaneName/listTemplate",
swipeToDelete: false,
fixedHeightItems: true,
reorderable: false
};
In this code, sceneName refers to the name of the directory your view is in and rowTemplate/listTemplate are the names of your template files (see below) without the .html extension.
Model
A list's model contains the dynamic information about the list. If you know your list's items during setup you can set them into the model's items property. If this list will be dynamic, you can adjust the model later.
this.listModel =
{
listTitle: $L("Name List"),
items: [{"name":"Bob"},{"name":"George"}]
}
Note that anything you define in the model will be available to the list template for substitution.
List Template
A list template is not required for most applications. Two items are passed to a list template: listTitle and listElements. You can include styling information in your listTemplate if you don't wish to include it in your view's HTML or if you wanted to add additional classes to the palm-list div:
<div class="palm-group">
<div class="palm-group-title">
#{-listTitle}
</div>
<div class="palm-list">
#{-listElements}
</div>
</div>
The list template is placed in a file located in your scene's view directory and has a .html extension.
Item Template
An item template is required to display information in the list. This template specifies what a typical row looks like. When the list is displayed (and updated) data is automatically put into the template to display each row. The row data is pulled from the model's items property. Each row in the value should be a structure containing data with for each of the items in the template.
<div class="palm-row">
<div class="truncating-text">#{name}</div>
</div>
In the example above, model.items must have a "name" member. For example: model.items = [{"name":"Bob"},{"name":"George"}];
Note that since Mojo 1.1 HTML is automatically escaped in templates. If your model's items will contain HTML that you want rendered on the screen then you must either turn of HTML escaping in your entire app (not recommended) or disable it on an item-by-item basis. To disable HTML escaping for a field add a '-' before the property name:
#{-someHTML}
The item template is placed in a file located in your scene's view directory and has a .html extension.
Empty Template
The empty template is displayed when there are no items in a list. The documentation states that it cannot be used with static lists but it can be made to work by calling setLength() after the widget is created. The template can contain any HTML you wish to display when the list is empty.
View HTML
Like other widgets, you can define where in your scene a list appears in the view for your scene. The simplest way to include a list is as follows:
<div class="palm-list">
<div id="listWidget" x-mojo-element="List"></div>
</div>
Instantiating the List
Like all widgets, a list must be properly set up before it can be displayed. The normal way to set up a list is to create it during your scene assistant's setup function:
this.controller.setupWidget("listWidget", this.listAttributes, this.listModel);
Template Styles
Discuss the various stylings that can be applied to lists and their rows. x-mojo-tap-highlight?
Working With Lists
List Events
List events all have the following additional fields on the event argument to the callback: {model:this.controller.model, item:dataObj, index: index + this.renderOffset, originalEvent:event}
| Event | Parameters passed to the listener function |
|---|---|
| Mojo.Event.listChange | none |
| Mojo.Event.listTap | event.item |
| Mojo.Event.listAdd | event.item |
| Mojo.Event.listDelete | event.item, event.index |
| Mojo.Event.listReorder | event.toIndex, event.fromIndex |
Examples
Handling a tap on the list:
this.handleListTap = this.handleListTap.bindAsEventListener(this);
this.controller.listen('listWidget', Mojo.Event.listTap, this.handleListTap);
...
MyAssistant.prototype.handleListTap = function(event) {
this.tappedRowValue = event.item;
};
Reordering a list:
this.reorderItemHandler = this.reorderItem.bindAsEventListener(this);
this.controller.listen('myList', Mojo.Event.listReorder, this.reorderItemHandler);
...
FoldersAssistant.prototype.reorderItem = function (event) {
Mojo.Log.info("reorder event %j", event.item, event.toIndex, event.fromIndex);
this.myListModel.items.splice(event.fromIndex, 1);
this.myListModel.items.splice(event.toIndex, 0, event.item);
// do something with reordered items array
};
List Methods
| Method | Arguments | Description |
|---|---|---|
| focusItem | itemModel, focusSelector | Focus the item designated by the item model; optionally pass in the focusSelector to focus a specific element within the item. |
| getItemByNode | node | Returns the item model associated with the list item containing the given node, if any. Returns undefined if not. |
| getLength | Returns the current length of the list. | |
| getLoadedItemRange | Returns a hash with offset & limit properties indicating the range of currently loaded item models (or items which have been requested). This is sometimes used on the service side to optimize subscription data. | |
| getMaxLoadedItems | Returns the maximum number of loaded items the list will maintain in its local cache. Function has no affect when the list size != 0. | |
| getNodeByIndex | index | Return top level node for the list item of the given index. Returns undefined if the item does not exist or is not currently rendered. |
| invalidateItems | offset, limit | Causes the given items to be reloaded (if currently loaded). If limit is unspecified, causes all items after 'offset' to be invalidated. |
| noticeAddedItems | offset, items (array) | Inserts the given array of items into the list at the given offset.If list items are dynamically loaded, this may cause some to be kicked out of the cache. Calling this API will not cause a property-change event to be fired. |
| noticeRemovedItems | offset, limit | Removes items from the list beginning at the given offset, and continuing for 'limit' items. If list items are dynamically loaded, this may cause new ones to be requested. Calling this API will not cause a property-change event to be fired. |
| noticeUpdatedItems | offset, items (array) | Causes the given items to be replaced and re-rendered. Items provided past the current end of the list will cause the length to grow. MUST pass an array. |
| revealItem | index, animate (boolean) | Attempts to scroll the scene to reveal the item with the given index. May behave poorly when working with variable height list items which are not currently loaded, since we can't accurately predict the height of the final rendered content. |
| setLength | length | Call to set the overall length of the list. This function will set the limit on what range of items may be requested, but will generally not invalidate any existing items or request any new ones. It MAY request new items when the currently loaded items window is either not full, or the length change causes the items window to move. The latter case can occur if the length change causes the window to be "out of bounds", or if it would ideally be positioned past the end of the list. |
| setLengthAndInvalidate | length | Behaves like setLength, except that all currently loaded items are invalidated. For lazily loaded lists, this API will result in a request for a whole window of items. Note that items are invalidated even when the length of the list does not actually change. |
| showAddItem | boolean | Show the "add item" in the list. |
Dynamic Lists
Lists are very rarely static. Usually, the information changes in response to various events. A very common theme is requesting the list's contents via an ajax call. To change the contents of a list outside of the setup function you must change list's model's items property then call modelChanged:
MyAssistant.prototype.ajaxSuccess = function(response) {
this.listModel.items = response.responseJSON;
this.controller.modelChanged(this.listModel);
}
However, if the list exceeds 20 items, only the first 20 will be re-rendered after calling modelChanged. In other words if you are staring at items 25-35, tap on an item to update that item, you will see a white screen until you scroll (allowing the list to be re-rendered). The workaround for this is to use mojo.noticeUpdatedItems(offset, listModel). If this doesn't work, you can override the renderLimit attribute of your list widget, which is set to 20 by default.
MyAssistant.prototype.ajaxResults = function(indexOfListItemTapped, transport) {
//some code for parsing your requested data
var i = indexOfListItemTapped;
this.listModel.items[i].newInfo = someNewDataToUpdateIntoMyListItem;
$('myListId').mojo.noticeUpdatedItems(0, this.listModel.items);
}
// 'myListId' can also be passed as event.srcElement.id
// this is handy if you have multiple lists using the same listTapHandler
List formatters
In the attributes for the List widget, you can specify a formatters property. This property is an object which maps model properties to formatter functions.
List formatter example
This is a pretty good example of how you can use formatters in your application. The formatters property has many more advanced uses than what is shown here. The HTML for the item template should append the string Formatted to the original property name.
Also a formatter does not have to exist in the model as a property, you can create one with whatever name you choose. Its template variable will only exist with 'Formatted' appended to its name. You can see it below as 'example'.
JavaScript
this.list = {
'attributes': {
'itemTemplate': 'scene/item-template',
'formatters': {
// this becomes #{categoryFormatted} in the template
'category': function(value, model) {
return 'Category: <b>'+model.category.toUpperCase()+'</b>';
},
// this becomes #{titleFormatted} in the template
'title': function(value, model) {
return 'Title: <i>'+model.title.toUpperCase()+'</i>';
},
// this becomes #{commentsFormatted} in the template
'comments': function(value, model) {
// you dont always have to check against 'value'
// you can dig directly into the current instance in the model
if (model.comments == 1) {
return 'Only '+model.comments+' Comment.'
}
else if (model.comments > 1) {
return model.comments+' Comments.';
}
else {
return 'No comments yet.';
}
},
// this becomes #{exampleFormatted} in the template
// but #{example} will not exist since it is not in the model
'example': function(value, model) {
// since example is not a property of the model value == undefined
if (model.category == 'sports') {
return 'WAHOO!! SPORTS!!<br/>';
} else { return; }
}
}
},
'model': {
'listTitle': $L('Articles'),
'items': [
{
'title': 'this is a new article title',
'category': 'news',
'body': 'Gabba Gabba Hey! Gabba Gabba Hey!',
'comments': 12
}, {
'title': 'this is another news article title',
'category': 'sports',
'body':'zzzzzzzZZZzzzzzzzzzzzZzzzzzzz',
'comments':0
}]
}
}
};
item-template.html
<div class="palm-row">
<!-- unformatted -->
<div class='entry'>
<span class='title'>#{category}: #{title}</span>
<div class='entry_body'>#{example}#{body}</div>
<span class='comments'>#{comments}</span>
</div>
<!-- formatted -->
<div class='entry'>
<span class='title'>#{categoryFormatted}: #{titleFormatted}</span>
<div class='entry_body'>#{exampleFormatted}#{body}</div>
<span class='comments'>#{commentsFormatted}</span>
</div>
</div>
HTML Output
<div class="palm-row">
<!-- unformatted -->
<div class='entry'>
<span class='title'>news: this is a new article title</span>
<div class='entry_body'>Gabba Gabba Hey! Gabba Gabba Hey!</div>
<span class='comments'>12</span>
</div>
</div>
<div class="palm-row">
<!-- formatted -->
<div class='entry'>
<span class='title'><b>News</b>: <i>This is a new article title</i></span>
<div class='entry_body'>Gabba Gabba Hey! Gabba Gabba Hey!</div>
<span class='comments'>12 Comments.</span>
</div>
</div>
<div class="palm-row">
<!-- unformatted -->
<div class='entry'>
<span class='title'>sports: this is another news article title</span>
<div class='entry_body'>zzzzzzzZZZzzzzzzzzzzzZzzzzzzz</div>
<span class='comments'>0</span>
</div>
</div>
<div class="palm-row">
<!-- formatted -->
<div class='entry'>
<span class='title'><b>Sports</b>: <i>This is another news article title</i></span>
<div class='entry_body'>WAHOO!! SPORTS!!<br/>zzzzzzzZZZzzzzzzzzzzzZzzzzzzz</div>
<span class='comments'>No comments yet.</span>
</div>
</div>
Dividers
Dividers can be a little tricky to get right because you need two things to get them to work properly: 1) Divider Template, 2) Divider Function. The divider template is relative to the view folder and is basically an html file with #{dividerLabel} somewhere in the template and the divider function is a callback that accepts a list element model and returns a string that is substituted for #{dividerLabel}. For example if we have the template:
<table class="palm-divider labeled">
<tr>
<td class="left"></td>
<td class="label">
#{dividerLabel}
</td>
<td class="right"></td>
</tr>
</table>
and the divider function:
list2Dividerfunction: function(inSender, inItemModel) {
//return a divider label for the list item model inItemModel
return inItemModel.name;
}
then above every item you will get a label that includes inItemModel.name as it's string and it will be styled like a regular divider with a blue streak.
Adding Widgets to Lists
One of the most powerful features of lists is the ability to embed other widgets within the List widget. There are some limitations associated with doing so. You don't instantiate the widgets inside the list widget individually. After including the appropriate HTML markup within the list item template call instantiateChildWidgets on the List widget:
this.controller.instantiateChildWidgets(listWidget);
The model for the child widgets share the model of the list. For example, if you include a checkbox in your list widget you must specify a value property for each item in List.
For more information see the List Widget With Checkbox Example.
If you want drawers in a list, that gets a little tricky. The Pseudo-Drawers In A List example provides a nice way to simulate drawers using basic CSS and HTML.
Dynamic addItemLabel
One of the many attributes of a list is the addItemLabel. Specifying a value for the addItemLabel appends a special row to the end of your list. Currently, there's no built-in method to modify a list's addItemLabel once the list has been setup. However, you can modify the text within the addItemLabel div by using selectors.
// This appends the special 'add' row to the end of your list
$('id_of_your_list_widget').mojo.showAddItem(true);
var divAddItemLabel = $$('#id_of_your_list_widget [name=palm-add-item] .title').first();
var label = "modify this text to your liking";
if (divAddItemLabel) {
divAddItemLabel.update(label);
}
ARES
viewFile widget
Open a PDF file you have on your device from a listTap
- Drag List widget onto your Palm GUI. This can be placed inside of a scroller widget if you think you will have more than a couple of items. And is usually a good idea anyway, that way it will scroll if necessary when you change orientation.
- Drag the viewFile widget from the palette, under the File I/O section onto your list. You can rename this if you like so that you can remember it. Just remember that you did this because you reference it later and don't want to make that mistake.
- Under the styles section, look under ITEMS then itemHTML.
[ { item: 0, label: "Zero", value: "0" fileUrl: "/pathOfFile/fileName.pdf" }, { item: 1, label: "One", value: "1" fileUrl: "/pathOfFile/fileName.pdf" }, { item: 2, label: "Two", value: "2" fileUrl: "/pathOfFile/fileName.pdf" }, { item: 3, label: "Three", value: "3" fileUrl: "/pathOfFile/fileName.pdf" } ]
The item and value remain unchanged, however, the label is whatever you want to appear in the list and the fileUrl is where you have the pdf file stored in your app. That is the important thing for your list, otherwise it wont know where to look to open the file. Of course if you have more than 3 items in your list, enter all of them. Remember to put a comma at the end of every item set {} except for the last one (no comma for the last one).
Under the Events tab, click the icon in the listTap box. This will insert the code into the appropriate area for you. All you will have to do is tell it what to do.
- You will initally see this
MainAssistant.prototype = {
setup: function() {
Ares.setupSceneAssistant(this);
},
cleanup: function() {
Ares.cleanupSceneAssistant(this);
},
list1Listtap: function(inSender, event) {
}
};
Then just change the list1Listtap: function(inSender, event) { } to
list1Listtap: function(inSender, event) {
this.$.viewFile1.setFileUrl(Mojo.appPath+event.item.fileUrl);
this.$.viewFile1.viewFile();
}
be sure to replace the 'viewFile1' with what ever you called this widget when you placed it on your list. If you didnt rename it, then you shouldnt have a problem.
By doing all of this, you can now make a list, click on a list item, and have that item link to a PDF file and open that file. The PDF will open in a new window all by itself, you won't need to do anything for that.