jQuery Mobile - Listview

Tweet Like

This is one of several jQuery Mobile samples. Please refer to the jQuery Mobile and jQuery overview articles for related background information.

The source code for this sample can be found here: https://github.com/gomobile/sample-jqm-list-view, or download the Intel(R) XDK to check out all the HTML5 Samples.

This sample uses the Rotten Tomatoes* API to create a paginated list of current DVD rentals. It demonstrates how to populate a listview with live data from a web API, and how to dynamically generate pages.

Web API Basics

This sample is an example of how to utilize the abundance of interesting 3rd-party data from open web APIs. It uses the Rotten Tomatoes API to obtain information on current release DVDs. Like many other web APIs, the Rotten Tomatoes API is a RESTful service that returns data in a JSON format. The API specifies various URLs for various service functionalities. For example, current release DVD data is provided via this URL:

http://api.rottentomatoes.com/api/public/v1.0/lists/dvds/current_releases.json?parameters

To invoke this functionality, an app would issue an HTTP GET request to this URL. Function parameters are specified as URL query parameters. The Rotten Tomatoes API requires an apikey parameter to identify the accessing app, so that it can analyze and control API access. It also accepts parameters specifying how many DVDs make up a "page" of response data, and which page of DVDs to return, so that it can partition the large amount of current releases data into multiple page-chunked responses.

Parameter Required Default Value Description
apikey yes N/A key for identifying the app accessing the API
page_limit no 16 number of DVDs per page
page no 1 page of current DVD releases
country no us country for localization

The API then returns JSON data in an HTTP response, which contains the total number of DVDs available, and a page-length list of movie details.

{
    "total": 50,
    "movies": [{
        "id": "771203390",
        "title": "Even the Rain (Meme La Pluie)",
        "year": 2011,
        "mpaa_rating": "Unrated",
        "runtime": 103,
        "ratings": {
            "critics_rating": "Certified Fresh",
            "critics_score": 89,
            :
        },
        "synopsis": "...",
        "posters": {
            "thumbnail": "http://content8.flixster.com/.../11155586_mob.jpg",
            :
        },
        "abridged_cast": [
            {"name": "Gael Garcia Bernal"},
            {"name": "Luis Tosar"},
            {"name": "Najwa Nimri"}
        ],
        :
    }, ... , { ...
    }],
    :
}

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Calling Web APIs via AJAX

This sample calls the Rotten Tomatoes API using an AJAX request. Asynchronous-style communication is used to ensure that the app remains responsive while the request is being processed. Rather than having the only JavaScript* thread in an app wait for a response from the remote service, the application thread can just specify a callback for processing that future response, and return from the request function call to do useful work in the meantime.

Additionally, this sample exploits the popular JSONP hack so that its code can work properly as a hosted web app in browsers, not just as a packaged web app on devices. (In general, an HTML5 CORS solution can be used instead of JSONP, but only with supporting client devices and service providers.) Using JSONP enables a hosted version of this sample to bypass the browser's same origin policy and communicate with the Rotten Tomatoes service. This sample utilizes a jQuery JSONP plugin to abstract away the implementation logistics of a JSONP-based AJAX request. Underneath the hood, the Rotten Tomatoes API is effectively called by injecting a script tag with the request URL as its source into the DOM, and tricking the browser into believing that it is just downloading a 3rd-party script from the Rotten Tomatoes domain. The Rotten Tomatoes API then returns the desired data as a parameter to a sham callback function specified by the callback parameter in the request. The jQuery JSONP plugin provides the sham callback, defaultly named _jqjsp, whose sole purpose is to make the response data available for the app's actual callback functions.

/* code to issue the JSONP-based AJAX request */
$.jsonp({
    /* request URL to obtain current releases data from Rotten Tomatoes API */
    url: baseurl + 'lists/dvds/current_releases.json',
    /* parameters to send as part of request */
    data: {
        page: page,
        page_limit: nItemsPerPage,
        apikey: key
    },
    /* parameter name for appending callback name to request */
    callbackParameter: 'callback'
})

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

<!-- script tag generated and injected into the DOM by above code -->
<!-- (assuming page = 1, nItemsPerPage = 12, key = KEY) -->
<script async 
    src="http://api.rottentomatoes.com/api/public/v1.0/lists/dvd/
    current_releases.json?page=1&page_limit=12&apikey=KEY&callback=_jqjsp></script>

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

/* data returned by the API, hidden as the parameter in sham callback script */
_jqjsp( { "total": 50, "movies": [ {...} ], ... } );

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

This sample specifies the actual callbacks for handling the data response from the AJAX request using jQuery's Promise interface. jQuery-based AJAX requests implement the Promise interface to enable multiple callbacks to be chained to the original request. The Promise interface provides special callback hooks for handling different completion statuses — done() for handling the response to a successful request, fail() for handling errors, and always() for handling the response regardless of the status. Note that jQuery's default AJAX implementation fails silently when using JSONP, which is the motivation behind using the JSONP plugin. The Promise interface also provides a pipe() callback hook, which can be used to process the response data before passing the filtered data value as part of a new Promise to the subsequent callback in the chain. This sample registers a pipe callback to clean up the formatting of the data, cache the data for future queries, and return the formatted movies array portion of the original data. It then registers a donecallback to generate the appropriate UI to display that piped data (see next section).

$.jsonp( /* issue ajax request */ )
    .pipe(function(data) {
        /* clean up data from remote source to apply app-specific data format */
        cleanupData(data);
        /* cache results for future queries */
        cachedData[page] = data.movies;
        /* return movie data */
        return data.movies;
    })
    .done(function(data) {
        /* generate UI to display the processed data */
    });

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Note that Promise chaining via pipe enables modularity. In the actual code, the two callbacks reside in functions of two different components, dataSource.getMovies() andviewController.createPrimaryPage(), as to create a clean separation of concerns between data management and UI management.

Dynamically Generating a Listview

This sample uses a jQuery Mobile listview to display the array of movies returned by the pipe callback above. It defines an HTML template for the listview, delineating the various components of the listview based on the expected JSON data format. It uses the Mustache template syntax to iterate through the JSON array and pull out relevant fields from each array element. A Mustache tag is delineated by double brackets ( { { } } ), and is dynamically replaced by a value based on the given JSON data. A Mustache section begins with a pound (#) prefix and ends with a slash (/) prefix, and delineates a template that should be replicated for each item in the named JSON array; unnamed arrays are referred to as a dot (.). The template is embedded within the main HTML code as a script tag of the unknown type "text/html". This enables the template to be formatted and maintained with the rest of the HTML code, while staying invisible to the HTML5 runtime and accessible to the JavaScript code.

<!-- listview template (hide in unknown script tag type) -->
<script type="text/html" id="primaryPageContentTmpl">
    <ul data-role="listview" class="movies">
        <!-- mustache section -->
        {{#.}}
            <!-- template for each item in given json data -->
            <li><a href="#secondaryPage" class="movie" data-index={{index}}>
                <!-- movie thumbnail image -->
                <img src={{posters.thumbnail}} />
                <!--title of the movie -->
                <h3>{{title}}</h3>
                <!-- Rotten Tomatoes critics rating, numeric w/ corresp icon -->
                <p>
                    TOMATOMETER<sup>&reg;</sup> {{ratings.critics_score}}
                    <i class="icon tiny {{&ratings.critics_rating}}"></i>
                </p>
            </a></li>
        {{/.}}
    </ul>
</script>

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

The done callback for the AJAX request copies the template from the DOM, tells the Mustache library to render the template with the received data, and inserts the rendered listview into the DOM. Then, it invokes the listview() method to inform the jQuery Mobile framework of the listview's existence, so that the listview widget can be initialized with the appropriate jQuery Mobile styling and behavior.

.done(function(data) {
    /* get template for creating listview */
    var tmpl = $('#primaryPageContentTmpl').html();
    /* render listview template with data */
    $(Mustache.render(tmpl, data))
    /* insert into page in dom */
    .prependTo($page.find(':jqmData(role=content)'))
    /* tell jQuery Mobile to process newly inserted listview widget */
    .listview();
});

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Dynamically Generating Primary Pages

This sample consists of multiple "primary" pages, each containing a listview of its corresponding allocation of movies, and linked with a previous and next page through navbar buttons in the footer. (These pages are labeled as "primary" because they are at the first level of app navigation.) At its start, this sample queries the Rotten Tomatoes API for the total number of movies in order to compute the number of primary pages. It then creates the skeleton for that many primary pages based on a pre-defined HTML template.

<!-- template for primary page skeleton -->
<script type="text/html" id="primaryPageSkeletonTmpl">
    <div data-role="page" class="primaryPage">

        <!-- header -->
        <div data-role="header" data-id="app-header">
            :
        </div>

        <!-- content -->
        <!-- to contain listview dynamically created with data from web API -->
        <div data-role="content">
        </div>

        <!-- footer -->
        <!-- navbar to link previous and next pages of movie data -->
        <div data-role="footer" data-theme="a">
            <div data-role="navbar">
                <ul>
                    <li><a href="#" class="prev">Previous</a></li>
                    <li><a href="#" class="next">Next</a></li>
                </ul>
            </div>
        </div>

    </div>
</script>

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

For each page, the template needs to be customized with the page number as the id and jQuery Mobile data-url navigation attribute, as well as the correct links to previous and next pages. The previous (next) navigation button for the first (last) page is disabled by applying jQuery Mobile's ui-disabled class.

/* create page skeleton html node */
var $page = $($('#primaryPageSkeletonTmpl').html())
    .attr('id', pagenum)
    .attr('data-url', '#' + pagenum);

/* link to previous page (if any) */
if (pagenum == 1) {
    $page.find('.prev').addClass('ui-disabled');
} else {
    $page.find('.prev').attr('href', '#' + (pagenum - 1));
}

/* link to next page (if any) */
if (pagenum == N) {
    $page.find('.next').addClass('ui-disabled');
} else {
    $page.find('.next').attr('href', '#' + (pagenum + 1))
}

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

This sample creates a jQuery object, $pages, to hold all the newly created page skeletons, so that they can be inserted into the DOM all at once. In general, it is better to minimize the number of DOM node insertions to achieve better performance. Each insertion usually triggers an expensive reflow, in which the web runtime recalculates the position for all affected elements, and re-renders part or all of the app. The new pages are inserted before the existing pages, so that the first primary page becomes the top page in the document, thus recognized by the jQuery Mobile framework as the first page to load for the app.

/* create pages and insert into DOM in bulk */
var $pages = $();
for (var pagenum = 1; pagenum <= N; pagenum++) {
    $pages = $pages.add( /* created $page (see above) */ );
}
$pages.insertBefore( /* 1st of existing pages */ );

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

This sample also registers an event handler on the initial loading of all primary pages, so that the listview content for each page can be generated on demand.

/* create pages on demand */
$(document).on('pageinit', '.primaryPage', /* generate listview content */);

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Finally, $.mobile.initializePage() is invoked to inform the jQuery Mobile framework to process all the pages and load the first page.

/* tell jQuery Mobile to process pages */
$.mobile.initializePage();

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Dynamically Generating a Secondary Details Page

Selecting a movie on a primary page causes this sample app to navigate to a "secondary" details page, which contains additional information for that movie. Each secondary details page is created on demand. There is only one secondary page in the DOM at all times. The page skeleton for the secondary page is defined in the initial HTML code. Each request for movie details will cause the content and title of that page to be filled in dynamically.

<!-- 1 copy of secondary page skeleton, content & title to be filled on demand -->    
<div data-role="page" id="secondaryPage">
    :
</div>

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

To enable on-demand page creation, this sample listens for all click events on a child movie item (<li>) of each primary page's listview (class = "movies"). By utilizing a hierarchical jQuery selector, this sample can then retrieve the index of the selected movie with respect to its parent listview, which can then be used with the current page number to identify the movie.

/* create secondary page on demand */
$(document).on('click', '.movies > li', createSecondaryPage);

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

The secondary page content is dynamically created using a Mustache-based HTML template, rendered with data cached from the initial request that generated the corresponding primary page.

<!-- template for content of secondary pages -->
<script type="text/html" id="secondaryPageContentTmpl">
    :
</script>

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

/* update secondary page (override content with details for given movie) */
createSecondaryPage() {
    /* get current page */
    var pagenum = $.mobile.activePage.attr('id');
    /* get cached detailed data for selected movie */
    var data = getMovie(pagenum, $(this).index());
    $('#secondaryPage')
        /* update header toolbar with movie title */
        .find('#title').html(data.title).end()
        /* render movie details template with data & insert into DOM as content */
        .find(':jqmData(role=content)')
            .html($(Mustache.render($('#secondaryPageContentTmpl').html(), data)));
}

See the Pen %= penName %> by Intel IDZ (@intelidz) on CodePen

Refreshing the Data

This sample provides a refresh button at the top of all primary pages to force the app to request the latest data from the Rotten Tomatoes web API. The refresh functionality essentially clears the content of all primary pages, flushes the cached data, and reloads the app from the first primary page. All previously created primary pages are tracked, as to manually trigger the pageinit event upon revisiting those pages so that the corresponding listview is dynamically regenerated. If the total number of pages has changed, additional page skeletons may be generated, and the affected page navigation button is updated.

Initial Login Screen

The first page shown in the sample is a login screen to input your developer key to access the Rotten Tomatoes web API. If you wish to run this sample app, you can register for a Mashery user account, then request a key. This sample will test the validity of the key entered by making a single request to the Rotten Tomatoes API and evaluating the status of the response. With a valid key, the main sample code starts up to create all the primary page skeletons and load the first primary page. Otherwise, an error message is displayed, returning you to the login page to try again. The login page uses a jQuery Mobile form to solicit your developer key, and jQuery Mobile dialog pages to display informational messages.

Miscellaneous

To emulate or preview this sample in the Intel HTML5 Development Environment, you need to start Chrome with the --allow-running-insecure-content flag. This is because the IDE is running in a secure https environment, while the Rotten Tomatoes API only supports http traffic.


Devices Tested:

  • Samsung Galaxy S* II smartphone (Google Android* 2.3.5, 480 x 800 pixels, hdpi)
  • Lava Xolo X900* smartphone (Android 2.3.7, 600 x 1024 pixels, hdpi)
  • Motorola Droid* Razr M smartphone (Android 4.0.4, 540 x 960 pixels, hdpi)
  • Asus Google Nexus* 7 tablet (Android 4.1.1; display: 7 inches, 1280 x 800 pixels, tvdpi)
  • Amazon Kindle Fire* 2 tablet (v.10.1.3 based on Android 4.0; display: 7 inches, 1024 x 600 pixels, mdpi)
  • Apple iPod Touch* 4th gen mobile device (Apple iOS* 4.3.1, 640 x 960 pixels, retina)
  • Apple iPod Touch 4th gen mobile device (iOS 6.0, 640 x 960 pixels, retina)
  • Apple iPad* 2 tablet (iOS 5.1.1, 1024 x 768 pixels, non-retina)

Download project assets on github


Download Now