The Telligent Community Mobile API provides support for rendering of lists of items, including navigation, endless scrolling, and filtering helpers. This guide will explore the use of these patterns, re-implementing a portion of the bookmarks list widget as an example.
[toc]
Navigation
A common mobile list interaction pattern is that the entirety of a list item, and not just specific text in it, is tappable to navigate to details about the item. Instead of adding explicit tap handlers to list items, the data-targeturl
can be used instead.
For example, to list a user's bookmarks:
#set ($userId = $core_v2_user.Accessing.Id) #set ($bookmarks = $core_v2_bookmark.List("%{ UserId = $userId, PageSize = 10, PageIndex = 0 }")) #foreach ($bookmark in $bookmarks) #beforeall <ul class="post-list bookmarks"> #each <li class="post-list-item content-item" data-targeturl="$bookmark.Content.Url"> <div class="post"> <div class="content"> <a href="$bookmark.Content.Url">$bookmark.Content.HtmlName("Web")</a> </div> </div> </li> #afterall </ul> #end
After performing a widget API call to the $core_v2_bookmark extension, each bookmark is renderd in a containing element with a data-targeturl
attribute.
Endless Scrolling
Endless Scrolling refers to the ability to load further pages of content by scrolling down a page/screen. This can be achieved soley using the mobile.content.scrollBottom client message or mobile.navigation.scrollBottom client message, but this pattern can be more easily accomplished using the higher level $.telligent.evolution.mobile.scrollable()
method in the shell JavaScript API Module.
The API supports:
- Handling infinite scrolling in either content or navigation
- Handling the scroll events, while blocking during current loads
- Showing a loading indicator
- Tracking a current page index
- Pre-filling up to a maximum number requests to fill the vertical space when an initial page's content is not enough
- Handling context clearing when navigating to different pages or refreshing
Usage
Within a widget:
$.telligent.evolution.mobile.scrollable({
region: 'content',
load: function(pageIndex, success, error) {
// perform a request to load a page of content
$.telligent.evolution.get({
url: SOME_URL,
data: {
pageIndex: pageIndex
},
success: success,
error: error
});
},
complete: function(content) {
// render the content somewhere in the widget
}
});
Load
should only load data, but not process it or add it to the UI. Rendering should occur within thecomplete
function.- The URL would likely be a REST endpoint or a URL to a custom ajax endpoint in the widget provided by $core_v2_widget.GetExecutedFileUrl().
PageIndex
is tracked and incremented automatically by thescrollable
on each new page. By default, the first call to load will receive a pageIndex of1
, assuming that a page of0
was loaded by default in the widget. This can be overriden by providing an alternate value to theinitialPageIndex
option.- When the
load
's ajax request completes, it must always callsuccess
orerror
.Success
should be provided the content to be rendered. Even if the content is empty, success should be passed the content. Complete
is called with the value returned to load's success callback.Complete
is only called if the context which generated the ajax request is still viable (i.e. the shell hasn't navigated away to another page).- By default, a
scrollable
will attempt 5 tries to fill the container if the initial page doesn't fill the space. The tries will be halted if, at any point, the content fills the container, or the content returned is empty (indicating there is no data to load). The attempt count can be overriden with thepreFillAttempts
option.
Example
To add endless paging to the previous example of bookmark listing, the behavior should be split into separate files in the widget.
Assuming the widget has an ID of 488a4c59b7584e51b0574cbc80b2ab4f
and the widget is defined in a file named Bookmarks.xml
, add the following structure:
/
Bookmarks.xml
488a4c59b7584e51b0574cbc80b2ab4f/
list.vm
more.vm
list.vm
will contain the same bookmark loading and rendering code that used to live in the widget's content script. more.vm
will serve as an ajax callback for scroll-based paging.
In list.vm
:
## Load bookmarks. Assume a $pageIndex value to be in scope
#set ($userId = $core_v2_user.Accessing.Id)
#set ($bookmarks = $core_v2_bookmark.List("%{ UserId = $userId, PageSize = 10, PageIndex = $pageIndex }"))
#foreach ($bookmark in $bookmarks)
<li class="post-list-item content-item bookmark" data-targeturl="$bookmark.Content.Url">
<div class="post">
<div class="content">
<a href="$bookmark.Content.Url">$bookmark.Content.HtmlName("Web")</a>
</div>
</div>
</li>
#afterall
## Provide extra data about whether there are more bookmarks beyond what
## has been returned from the current page size/page index call
#set ($hasMore = false)
#if ($bookmarks.TotalCount > (($bookmarks.PageIndex + 1) * $bookmarks.PageSize))
#set($hasMore = true)
#end
<li class="data" #if ($hasMore) data-hasmore="true" #end></li>
#end
In more.vm
:
#set ($pageIndex = $core_v2_page.GetQueryStringValue('pageIndex'))
$core_v2_widget.ExecuteFile('list.vm')
As this is an ajax callback, load the requested page index out of the query string and set it as a $pageIndex variable in scope before executing a bookmark listing.
Adjust the widget's content script to:
## Set initial page index when widget loads
#set ($pageIndex = 0)
## load and render the first page of bookmarks synchronously in list.vm
<ul class="post-list bookmarks">
$core_v2_widget.ExecuteFile("list.vm")
</ul>
<script>
var loadMoreUrl = "$core_v2_encoding.JavascriptEncode($core_v2_widget.GetExecutedFileUrl('more.vm'))";
jQuery(function(){
jQuery.telligent.evolution.messaging.subscribe('mobile.content.rendered', function(){
jQuery.telligent.evolution.mobile.scrollable({
region: 'content',
load: function(pageIndex, success, error) {
if (jQuery('.bookmarks .data:last').data('hasmore')) {
jQuery.telligent.evolution.get({
url: loadMoreUrl,
data: {
pageIndex: pageIndex
},
success: success,
error: error
});
} else {
success();
}
},
complete: function(content) {
## Animate the rendering of these new results.
content = jQuery(content).css({ opacity: 0 });
jQuery('.post-list.bookmarks').append(content);
content.evolutionTransform({ opacity: 1 }, { duration: 400 });
}
});
});
});
</script>
Note:
- The registration of client behavior after the mobile.content.rendered message.
- The use of
$.telligent.evolution.mobile.scrollable()
- Within
load
, it's first detecting if it should even attempt to load more by inspecting the previous results. - Calling
more.vm
as an executed ajax callback via its ExecutedFileUrl. - Optional use of animation to render the new results within
complete
Filtering
Filtering refers to the ability for lists to include a (possibly horizontally scrolling) set of links above them which, when tapped, asynchronously reloads the list with a filter or sorting applied, pre-scrolling the filters to the currently selected filter, while retaining the filter state across subsequent (pulled) refreshes.
This can be achieved through a combined used of the following mobile shell APIs:
$.telligent.evolution.mobile.scrollable()
$.telligent.evolution.mobile.addRefreshParameter()
$.telligent.evolution.mobile.refresh()
Usage
The pattern includes:
- Determine the current (if any) filter from the query string
- Define a ui-links UI component with all the possible filters.
- For each link, if it represents the current filter, give it a
data-selected
attribute to highlight the link and have it pre-scrolled into view.
- For each link, if it represents the current filter, give it a
- On the
tap
event on each filter link,- Use $.telligent.evolution.mobile.addRefreshParameter() to apply the tapped filter's value to be included in the querystring on the page's next refresh.
- Refresh the page using $.telligent.evolution.mobile.refresh(), which refreshes the current content page via ajax
- After the refresh, since refresh parameters are intentionally volatile and only last across a single refresh, if there's a current filter applied, then re-apply it's value to the next refresh querysting using
$.telligent.evolution.mobile.addRefreshParameter()
. This effectively allows refresh parameters to be round-tripped.
Within a widget:
## Attempt to read a requested filter value out of the query string
#set ($myFilterValue = 'no-filter')
#set ($myFilterValue = $core_v2_page.GetQueryStringValue('my_filter'))
## Render a horizontally-scrolling ui-links containing filters
## For each link, apply a data-selected attribute if the item matches the current filter
<div class="ui-links filters" data-minlinks="50">
<ul>
<li>
<a href="#" data-value="a" #if($myFilterValue=='a') data-selected #end>
Filter A
</a>
</li>
<li>
<a href="#" data-value="b" #if($myFilterValue=='b') data-selected #end>
Filter B
</a>
</li>
<li>
<a href="#" data-value="c" #if($myFilterValue=='c') data-selected #end>
Filter C
</a>
</li>
<li>
<a href="#" data-value="d" #if($myFilterValue=='d') data-selected #end>
Filter D
</a>
</li>
<li>
<a href="#" data-value="e" #if($myFilterValue=='e') data-selected #end>
Filter E
</a>
</li>
<li>
<a href="#" data-value="f" #if($myFilterValue=='f') data-selected #end>
Filter F
</a>
</li>
<li>
<a href="#" data-value="g" #if($myFilterValue=='g') data-selected #end>
Filter G
</a>
</li>
</ul>
</div>
<p>
And here's the current filter: $myFilterValue
</p>
<script>
jQuery(function(){
// set a javascript variable to refer to the filter value
var myFilterValue = '$core_v2_encoding.JavascriptEncode($myFilterValue)';
// handle taps of filter links
jQuery('#$core_v2_widget.WrapperElementId a').on('tap', function(e){
e.preventDefault();
// get a value from the tapped link
var filterValue = jQuery(this).data('value');
// add that as a refresh parameter
j.telligent.evolution.mobile.addRefreshParameter('my_filter', filterValue);
// now refresh the content page, including the filter
j.telligent.evolution.mobile.refresh();
})
// if there's already a filter value currently, then re-apply it so that it
// in case the current page is refreshed
if(myFilterValue) {
j.telligent.evolution.mobile.addRefreshParameter('my_filter', myFilterValue);
}
});
</script>
Example
This example will add content type filtering to the already endless paging bookmark list created above.
Update list.vm
:
Note the change to assume a $filter
variable in scope, and building a list of content types based on the value of the filter. The content type list is then provided to the api call.
#set ($userId = $core_v2_user.Accessing.Id) #set ($contentTypes = '') #if ($filter == 'content') #set ($contentTypes = "$core_v2_forumThread.ContentTypeId,$core_v2_wikiPage.ContentTypeId,$core_v2_blogPost.ContentTypeId,$core_v2_media.ContentTypeId") #elseif ($filter == 'applications') #set ($contentTypes = "$core_v2_forum.ApplicationTypeId,$core_v2_wiki.ApplicationTypeId,$core_v2_blog.ApplicationTypeId,$core_v2_gallery.ApplicationTypeId") #elseif ($filter == 'groups') #set ($contentTypes = "$core_v2_group.ContainerTypeId") #elseif ($filter == 'people') #set ($contentTypes = "$core_v2_user.ContentTypeId") #end #set ($bookmarks = $core_v2_bookmark.List("%{ UserId = $userId, PageSize = 3, PageIndex = $pageIndex, ContentTypeIds = $contentTypes }")) #foreach ($bookmark in $bookmarks) <li class="post-list-item content-item bookmark" data-targeturl="$bookmark.Content.Url"> <div class="post"> <div class="content"> <a href="$bookmark.Content.Url">$bookmark.Content.HtmlName("Web")</a> </div> </div> </li> #afterall ## Provide extra data about whether there are more bookmarks beyond what ## has been returned from the current page size/page index call #set ($hasMore = false) #if ($bookmarks.TotalCount > (($bookmarks.PageIndex + 1) * $bookmarks.PageSize)) #set($hasMore = true) #end <li class="data" #if ($hasMore) data-hasmore="true" #end></li> #end
Update more.vm
:
Note that the filter value is now read from the query string on paging callbacks
#set ($pageIndex = $core_v2_page.GetQueryStringValue('pageIndex')) #set($filter = false) #set($filter = $core_v2_page.GetQueryStringValue('filter')) #if (!$filter) #set($filter = 'content') #end $core_v2_widget.ExecuteFile('list.vm')
Update the widget content script:
## Set initial page index when widget loads
#set ($pageIndex = 0)
## Read the currently-applied filter, if any. Default to 'content'
#set($filter = false)
#set($filter = $core_v2_page.GetQueryStringValue('filter'))
#if (!$filter)
#set($filter = 'content')
#end
## Render a list of filters, applying data-selected if it matches the current filter
<div class="ui-links filters" data-minlinks="50" data-animate="true">
<ul class="activity-filters">
<li>
<a href="#" data-filter="content" #if($filter == 'content') data-selected #end>
Content
</a>
</li>
<li>
<a href="#" data-filter="applications" #if($filter == 'applications') data-selected #end>
Applications
</a>
</li>
<li>
<a href="#" data-filter="groups" #if($filter == 'groups') data-selected #end>
Groups
</a>
</li>
<li>
<a href="#" data-filter="people" #if($filter == 'people') data-selected #end>
People
</a>
</li>
</ul>
</div>
## load and render bookmarks synchronously in list.vm
<ul class="post-list bookmarks">
$core_v2_widget.ExecuteFile("list.vm")
</ul>
<script>
var loadMoreUrl = "$core_v2_encoding.JavascriptEncode($core_v2_widget.GetExecutedFileUrl('more.vm'))";
var widgetWrapper = '#$core_v2_widget.WrapperElementId';
var currentFilter = '$filter';
jQuery(function(){
jQuery.telligent.evolution.messaging.subscribe('mobile.content.rendered', function(){
// re-apply the current filter as a refresh paramter to be round-tripped
// across any potential manual refreshes
jQuery.telligent.evolution.mobile.addRefreshParameter('filter', currentFilter);
// when a filter is selected, if it's different than the current,
// then apply it as a refresh parameter and immediately refresh the content
jQuery(widgetWrapper + ' .filters a').on('tap', function(e) {
var tappedFilter = $(this).data('filter');
if (tappedFilter != currentFilter) {
jQuery.telligent.evolution.mobile.addRefreshParameter('filter', tappedFilter);
jQuery.telligent.evolution.mobile.refresh();
}
return false;
});
jQuery.telligent.evolution.mobile.scrollable({
region: 'content',
load: function(pageIndex, success, error) {
if (jQuery('.bookmarks .data:last').data('hasmore')) {
jQuery.telligent.evolution.get({
url: loadMoreUrl,
data: {
pageIndex: pageIndex
},
success: success,
error: error
});
} else {
success();
}
},
complete: function(content) {
content = jQuery(content).css({ opacity: 0 });
jQuery('.post-list.bookmarks').append(content);
content.evolutionTransform({ opacity: 1 }, { duration: 400 });
}
});
});
});
</script>
Note:
- The current filter is read from the query string
- A horizontal list of filters is rendered via ui-links, with the current filter given a data-selected attribute to pre-scroll to its position
- The current filter is applied as a refresh parameter to round-trip it across refreshes
- When filters are tapped, a new filter value is set as a refresh parameter before refreshing