jQuery Mobile and client generated pages

Posted on 23.01.12

3



Normally you would use jQuery Mobile for server generated pages. But you can also create the pages dynamically with JavaScript in the browser. To make this work we need to do a little jQuery Mobile configuration and call some utility methods our self.

In this sample todo list web app we will have this data in local storage:

var todos = [
    { title: "Buy milk", desc: "Remember to buy milk tomorrow." },
    { title: "Call Will", desc: "Will expects a call at 8." },
    { title: "Pay the bill", desc: "Pay the electric bill today." }
];
localStorage.setItem("todos", JSON.stringify(todos));

We will have this HTML5 page to start with:

<!doctype html>
<html><head><meta charset="utf-8" />
    <title>Todo list app</title>
    <link
        rel="stylesheet"
        href="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css" />
    <script src="http://code.jquery.com/jquery-1.6.4.min.js">
    </script>
    <script type="text/javascript">

        // jQuery Mobile initialization …

    </script>
    <script src="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.js">
    </script>
    <script type="text/javascript">

        // Page generation …

    </script>
</head><body></body></html>

Note that we do not have any content at all. The body is completely empty.

jQuery Mobile will do a lot of stuff for us when loaded. This is how the document looks like when done:

<!doctype html>
<html class="ui-mobile"><head>
    <base href="http://host/">
    <meta charset="utf-8">
    <title>Todo list app</title>
    <link
        rel="stylesheet"
        href="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css">
    <script src="http://code.jquery.com/jquery-1.6.4.min.js">
    </script>
    <script type="text/javascript">

        // jQuery Mobile initialization …

    </script>
    <script src="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.js">
    </script>
    <script type="text/javascript">

        // Page generation …

    </script>
</head>
<body class="ui-mobile-viewport">
    <div
        data-role="page"
        data-url="/pages.html"
        tabindex="0"
        class="ui-page ui-body-c ui-page-active"
        style="min-height: 1071px; ">
    </div>
    <div class="ui-loader ui-body-a ui-corner-all" style="top: 535.5px; ">
        <span class="ui-icon ui-icon-loading spin"></span>
        <h1>loading</h1>
    </div>
</body>
</html>

Most importantly a page has been created. When jQuery Mobile loads a document without a page, a page will be created. We can also see that this has been made the active page by the ui-page-active CSS class. We need to stop jqm from doing this. We want to create our own page, and make this generated page the active one. The autoInitializePage option has to be set to false on the mobileinit event:

        // jQuery Mobile initialization …
        $(document).bind("mobileinit", function () {
            $.mobile.autoInitializePage = false;
        });

Now the only thing that happens is that the head base elements gets added like in the previous case. Now we need to create our first dynamic page, which is going to be the page with the list of todos, and make it current:

        // Page generation …
        function createPage (title, content) {
            return $([
                '<section data-role="page">',
                    '<header data-role="header">',
                        "<h1>", title, "</h1>",
                    "</header>",
                    '<div data-role="content">',
                        content,
                    "</div>",
                "</section>"
            ].join("")).appendTo("body");
        }

        var items = JSON.parse(localStorage.getItem("todos")) || [];
        function createList () {
            var list = [ '<ul data-role="listview">' ];
            $.each(items, function (i) {
                list.push(
                    "<li>",
                        '<a href="#todos/', i, '">', this.title, "</a>",
                    "</li>"
                );
            });
            list.push("</ul>");
            return list.join("");
        }

        $(function () {
            createPage("Todos", createList()).attr("id", "list");
            $.mobile.initializePage();
        });

Here we create our first page with an id of list that we append to the body element. The page contains a list view with all the todo items. Each item has a URL with a hash. We will use this hash to figure out what dynamic page to generate. The hash looks like #todos/{itemId}. The last thing we do is to initalize the page. The page looks like this:

When we click a todo we get this error message:

jQuery Mobile is trying to load our page from http://host/todos/0. This is clearly not our intention. We want to generate the details pages for our items dynamically, not fetch them using AJAX. We also want to use a hash, but jQuery Mobile has changed our hash into pointing to a real document on the server side. Lets take some control of page changing by subscribing to the pagebeforechange event:

If $.mobile.activePage is undefined we know that it is the first page we are going to change to. We let jQuery Mobile do its thing in this case. Lets click an item in the todos list:

Here we can see that this is not the first page. We also look for the flag byUs that we will use later to indicate that the page has been changed by us. If none of these are present we stop jQuery Mobile from doing anything. Now our first page works as expected, but nothing happens if we click a todo item in the todo list.

We want full control of page changing and use hash changes to figure out what to do. jQuery Mobile includes Ben Alman’s hashchange special event plugin. First lets do some jqm configuration:

        // jQuery Mobile initialization …
        $(document)
            .bind("mobileinit", function () {
                $.extend($.mobile, {
                    autoInitializePage: false,
                    linkBindingEnabled: false,
                    pushStateEnabled: false,
                    hashListeningEnabled: false
                });
            })
            .bind("pagebeforechange", function (e, data) {
                var firstPage = !$.mobile.activePage,
                    changedByUs = !!data.options.byUs,
                    firstPageOrChangedByUs = firstPage || changedByUs;
                if (!firstPageOrChangedByUs) {
                    e.preventDefault();
                }
            });

Here we make sure to stop jQuery Mobile from:

  • Auto initialize our page
  • Add custom behaviour to our list items
  • Rewrite our hashes into real server documents
  • Listen to hash changes

Next thing to do is to subscribe to the hashchange event and change to dynamically generated pages:

        // Page generation …
        function createPage (title, content) { /* … */ }

        var items = JSON.parse(localStorage.getItem("todos")) || [];
        function createList () { /* … */ }

        $(function () {
            createPage("Todos", createList()).attr("id", "list");
            $.mobile.initializePage();
            $(window).bind("hashchange", function () {
                var url = window.location.href,
                    hash = $.mobile.path.parseUrl(url).hash;
                route(hash);
            });
        });

        var pattern = /\/(\d+)/;
        function route (route) {
            var match = pattern.exec(route);
            var showList = match === null;
            if (showList) {
                changePage($("#list"));
            } else {
                var itemId = match[1];
                changePage(getItemPage(itemId));
            }
        }

        function changePage (page) {
            $.mobile.changePage(page, {
                byUs: true,
                transition: "none",
                changeHash: false
            });
            $.mobile.urlHistory.stack = [];
        }

        function getItemPage (id) {
            var existingItemPage = $("#item" + id);
            if (existingItemPage.size() === 1) {
                return existingItemPage;
            }
            var item = items[id];
            return createPage(item.title, item.desc)
                .attr("id", "item" + id);
        }

Here we:

  • Search for an todo list item id in the hash
  • If we do not find an item we show the todo list page
  • If we find an item id we create an item page
  • Make sure not to recreate the list page and item pages by searching for pages with specific ids
  • Use jQuery Mobile’s $.mobile.changePage method to change to the found page

When we change the page we make sure to:

  • Set the byUs flag to true so that we know not to prevent default in the pagebeforechange event
  • Prevent transitions because the URL history of jQuery Mobile does not match our custom routing
  • Make sure that jQuery Mobile does not change our hash

Our first item page looks like this:

Todo item page.

Note the URL with the hash. There is still an issue to take care of. If we refresh the web app with this URL it will be staying. The problem is that we render the list page as the first page without checking the URL. Lets fix this last problem and then we are done:

        // jQuery Mobile initialization …
        $(document)
            .bind("mobileinit", function () { /* … */ })
            .bind("pagebeforechange", function (e, data) {
                var changedByUs = !!data.options.byUs;
                if (!changedByUs) {
                    e.preventDefault();
                }
            });

Here we make sure to never change any page if not changed by us. But this also prevents us from changing into our first page. Fix:

        // Page generation …
        function createPage (title, content) { /* … */ }

        var items = JSON.parse(localStorage.getItem("todos")) || [];
        function createList () { /* … */ }

        $(function () {
            createPage("Todos", createList()).attr("id", "list");
            $.mobile.initializePage();
            $(window)
                .bind("hashchange", function () {
                    var url = window.location.href,
                        hash = $.mobile.path.parseUrl(url).hash;
                    route(hash);
                })
                <strong>.trigger("hashchange")</strong>;
        });

        var pattern = /\/(\d+)/;
        function route (route) { /* … */ }

        function changePage (page) { /* … */ }

        function getItemPage (id) { /* … */ }

To solve this last problem we just trigger a hash change when our web app gets loaded.

You can try the complete todo list HTML5 web app here:

http://www.kkj.no/apps/todo-list/index.htm.

The source for the demo can be found here:

https://github.com/knutkj/kkj/blob/master/src/apps/todo-list/index.htm.

Advertisements
Posted in: Uncategorized