Month: <span>September 2012</span>

As I was putting the final touches on my Bookmark browser project, I came across this:

blog.mozilla.org/services/2012/08/31/retiring-firefox-home

That’s delicious, because I strongly suspected Mozilla wasn’t interested in improving Home or fixing any issues with it. Yet another reason I wanted to build a replacement. They have published the iOS Sync client source code that Home was built upon at GitHub. It would be fun to use that code and learn Objective-C while building a new app, but first I need to get a Mac. Maybe next year…

Update:
It seems a developer has taken that ball and run with it. He posted an app in the App Store that is essentially the same as Home. I’ll be giving it a try.

Later update:
OK, this is lame. He basically took the Firefox Home source code, removed all references to Mozilla, slapped on a new name, and submitted it to the App Store. All the same flaws are there, including the re-arrangement bug that I hate.

This is part 4 in a series of posts about creating a web-based replacement for the Firefox Home iOS app. Part 3 can be found here.

Now that I had an ironclad way of getting bookmark data, I needed to display it and provide a clean method of navigating through it. I looked around the web for a suitable client-side data binding solution, and KnockoutJS seemed like a good way to go. I wasn’t familiar with the MVVM pattern but thought it would be a good learning experience. In a nutshell, the way I understand MVVM is you have a model of your data that is kept strictly separate from the client-side view of it. A view model lives on the client that takes in the data itself and knows where to put it in the UI (admittedly, I may be simplifying or leaving something out).

A quick note about how Firefox organizes bookmarks: it stores them in two high-level containers, the bookmarks toolbar and the bookmarks menu. No big mystery where these are: it’s the built-in toolbar for single-click access to bookmarks, and the Bookmarks menu in the main menu bar at the top of the window.

I set up an unordered list for the bookmarks that looked like this:

<div id="bookmarkContainer">
    <ul id="bmMain" class="bookmarkList" data-role="listview" data-divider-theme="e">
        <li id="toolbarDivider" data-role="list-divider">Bookmarks Toolbar</li>

        <!-- ko foreach: BookmarksToolbar -->
        <li data-bind="bookmarkItemType: ItemType" data-icon="false">
            <div class="imageBlock"><img class="ui-li-icon" alt="Item icon" width="16px" height="16px" /></div>
            <div class="nameAndLocationBlock">
                <div data-bind="text: Name"></div>
                <div class="locationBlock" data-bind="text: Location"></div>
            </div>
        </li>
        <!-- /ko -->

        <li id="menuDivider" data-role="list-divider">Bookmarks Menu</li>

        <!-- ko foreach: BookmarksMenu -->
        <li data-bind="bookmarkItemType: ItemType" data-icon="false">
            <div class="imageBlock"><img class="ui-li-icon" alt="Item icon" width="16px" height="16px" /></div>
            <div class="nameAndLocationBlock">
                <div data-bind="text: Name"></div>
                <div class="locationBlock" data-bind="text: Location"></div>
            </div>
        </li>
        <!-- /ko -->
    </ul>
</div>

I went with the foreach binding in Knockout, since it made the most sense for what I was doing. I briefly tried the template binding but it turned out to be more involved than necessary. I initially tried to have a single unordered list that held everything, including the dividers for toolbar bookmarks and menu bookmarks. But I had a hard time changing the theme for the divider items on the fly, which I wanted to be a different color. Knockout lets you define binding event handlers, like this one:

ko.bindingHandlers.bookmarkItemType = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        if (viewModel.ItemType === 0) {
            $(element).jqmData('icon', 'arrow-r');
            $(element).find("a").attr("href", "#");
            $(element).find("a").attr("onclick", "doNavigation(this);");
            $(element).find("img").attr("src", "Images/folder.png");
            $(element).find(".locationBlock").css("display", "none");
        }
        else if (viewModel.ItemType === 1) {
            $(element).find("a").attr("href", viewModel.Location);
            $(element).find("a").attr("target", "_blank");
            $(element).find("img").attr("src", "Images/bookmark.png");
            $(element).find(".locationBlock").css("display", "block");
        }
    }
};

Here I’m basically setting attributes on the elements in each list item based on whether it’s a directory or an actual bookmark. I tried modifying the data-theme attribute used by JQM to indicate a list divider, but the change never seemed to get applied. Fortunately Knockout lets you apply the binding logic to a subset of items in a ul via containerless control flow syntax. There was some duplication of markup, but not enough to worry about.

The various samples at knockoutjs.com and the web at large had two different ways of setting up the view model: as a JavaScript variable or a function. I went the function route since I needed to do a bit of data manipulation before assigning the bookmark data. Knockout has the capability of updating the UI automatically when the bound data changes, such as in response to a user clicking on something. That sort of fit my usage pattern, though I would be updating the data via code. The mechanism Knockout uses is observables, and you simply declare them in your view model. Mine looked like this:

var bookmarksViewModel = function () {
    var self = this;
    var currentBookmarks = JSON.parse(localStorage.getItem("CurrentBookmarks"));
    self.BookmarksToolbar = ko.observableArray(currentBookmarks.BookmarkItems[0].BookmarkItems);
    self.BookmarksMenu = ko.observableArray(currentBookmarks.BookmarkItems[1].BookmarkItems);

    self.setBookmarks = function (node) {
        if (node) {
            self.BookmarksToolbar(node.BookmarkItems);
            self.BookmarksMenu([]);

            $("#toolbarDivider").hide();
            $("#menuDivider").hide();
        }
        else {
            // We are back at the topmost level, so show the toolbar and menu bookmarks
            self.BookmarksToolbar(currentBookmarks.BookmarkItems[0].BookmarkItems);
            self.BookmarksMenu(currentBookmarks.BookmarkItems[1].BookmarkItems);

            $("#toolbarDivider").show();
            $("#menuDivider").show();
        }
    }
};

The initial state of the app would always be to show everything in the bookmarks toolbar followed by everything in the bookmarks menu, which matches how Firefox Home shows things. I set up two observable arrays holding each of those sets. The trickiest part was how to go about updating that data when the user wanted to navigate into a directory. The rule in Knockout is you only apply the bindings once, then let the framework update the UI for you when things change. And since references to the two observable arrays will be maintained by the framework, I would need to replace their contents rather than assign completely new arrays.

Several failed attempts ensued. I tried using the removeAll() method that is available on observable arrays, then the push() method which is also available on observable arrays but is slightly different than the native JavaScript one (it turns out to only take single elements rather than an array of elements). I finally found the best way to do it via a blog post from Ryan Niemeyer, who totally has the title of Knockout expert locked up.

So the setBookmarks() function in my view model allows me to update the bound data at any time. I wrote the following function to handle the navigation when the user selected a directory rather than an actual bookmark:

function doNavigation(sender) {
    var nodePath = $(sender).attr("nodePath").split("\\");
    var newHeader;

    if (sender.id === "backButton") {
        var newPath;
        nodePath.pop();

        if (nodePath.length === 0) {
            return;
        }

        if (nodePath.length === 2) {
            newPath = "Root";
            newHeader = "Bookmarks";
        }
        else {
            newPath = nodePath.join("\\");
            nodePath.shift();
            newHeader = nodePath[nodePath.length - 1];
        }

        $("#backButton").attr("nodePath", newPath);
    }
    else {
        nodePath.shift();
        newHeader = nodePath[nodePath.length - 1];
        $("#backButton").attr("nodePath", $(sender).attr("nodePath"));
    }

    var currentBookmarks = JSON.parse(localStorage.getItem("CurrentBookmarks"));
    var curNode = getNode(currentBookmarks.BookmarkItems, nodePath);
    ko.dataFor($("#bookmarkContainer")[0]).setBookmarks(curNode);
    $("#bmHeader").html(newHeader);

    // For some reason the 'slide' transition doesn't work right when using it in a call
    // to changePage. It does the animation, but then doesn't show the page content
    $.mobile.changePage($("#Bookmarks"), { allowSamePageTransition: true, transition: "slidefade" });
}

The bookmark data itself was an object that contained a list of directories and bookmarks. Each directory had a list of subdirectories and bookmarks, while bookmarks had just a name and URL. The list was stored as an array on the client, so it was just a matter of finding the right one to use in my Knockout view model. The idea in doNavigation() was to take the path to the node the user just pressed and get the bookmark items assigned to it, or if Back was pressed get the bookmark items for its parent.

Each time the user selected a bookmark a new page would be opened, and if they selected a directory I needed to bind to the proper array in my main data object. I wrote the following function to search that object to find the exact items to display:

function getNode(items, nodePath) {
    var curDir = nodePath.shift();
    var node = null;

    for (var i = 0; i < items.length; i++) {
        if (items[i].Name === curDir && items[i].ItemType === 0) {
            // We know to stop when we've found the final directory in the node's path
            if (nodePath.length === 0) {
                node = items[i];
                break;
            }
            else {
                node = getNode(items[i].BookmarkItems, nodePath);
                break;
            }
        }
    }

    return node;
}

Putting it all together gave me a reasonable imitation of the navigation in Firefox Home. The speed of the navigation wasn’t too bad, though I have an idea of how I might improve it (Update: the ‘idea’ was a CSS rule to reduce the animation duration. It didn’t work, but I suspect it might be because my rule is incomplete). I’d like to get as close to native app response time as possible. The full source is posted at GitHub.

I feel I have a fairly good grasp on jQuery Mobile now. It would be interesting to try to incorporate PhoneGap into the project and see how it works.

This is part 3 in a series of posts about creating a web-based replacement for the Firefox Home iOS app. Part 2 can be found here.

What I wanted to do next was write enough code to save Sync settings and load bookmarks, log out as the current Sync user and wipe out the locally stored bookmarks and credentials in the process, and refresh bookmark data, all without any problems of any kind. Basically everything the user could do on the settings page. I planned to do as much work on the client as possible, since that is where the bookmark data would be stored. When the user saved new credentials I wanted them to be directed to the bookmark page, and if they were simply refreshing they would stay on the settings page but would see an updated count of their bookmarks.

The biggest issue was doing the form submissions and getting the UI to behave accordingly, including when an error occurred on the server. What was tricky was the fact that to get bookmark data from the Sync servers I made an Ajax call to a server-side function, and JQuery Mobile relies mainly on Ajax calls to do its thing. That coupled with the fact that my Ajax call was non-blocking made the UI flow have a bunch of niggling problems. Mostly it wouldn’t show the right things at the right time.

The best way I found to make it all work correctly was to disable the JQuery Ajax handling of the button clicks. I added data-ajax=”false” to the form element, then canceled the posting to the server and did everything manually. Essentially I took JQuery out of the equation. In keeping with the credo of the framework that you work with pure HTML elements and not server-based ones, I replaced the asp:Button controls with input elements and made the form element a regular client element, like so:

<div id="Settings" data-role="page">
    <div data-role="header" data-theme="b">
        <h5>Settings</h5>
    </div>

    <div class="smallText" data-role="content" data-theme="b">
        <div class="msgPanel">&nbsp;</div>
        <form id="settingsForm" action="Default.aspx" method="post" data-ajax="false">
            <div id="LoggedOut">
                <div class="bottomPadding20"><input id="txtUserName" type="text" data-mini="true"></div>
                <div class="bottomPadding20"><input id="txtPassword" type="password" data-mini="true"></div>
                <div class="bottomPadding20"><input id="txtSyncKey" type="password" data-mini="true"></div>
                <div class="alignCenter"><input id="Save" type="button" value="Save" data-icon="check" data-mini="true"></div>
            </div>

            <div id="LoggedIn" class="alignCenter hideMe">
                <table class="statsTable">
                    <tbody>
                        <tr>
                            <td class="labelColumn">&nbsp;</td>
                            <td class="fieldColumn">&nbsp;</td>
                        </tr>
                        <tr>
                            <td class="labelColumn">&nbsp;</td>
                            <td class="fieldColumn">&nbsp;</td>
                        </tr>
                        <tr>
                            <td class="labelColumn">&nbsp;</td>
                            <td class="fieldColumn">&nbsp;</td>
                        </tr>
                    </tbody>
                </table>

                <input id="Logout" type="button" value="Logout" data-icon="delete" data-mini="true"> <br><input id="Refresh" type="button" value="Refresh" data-icon="refresh" data-mini="true">
            </div>
        </form>
    </div>

    <div data-role="footer" data-theme="b">
        <div data-role="navbar">
            <ul>
                <li><a href="#Home" data-role="button" data-iconpos="bottom" data-icon="home">Home</a></li>
                <li><a href="#Bookmarks" data-role="button" data-iconpos="bottom" data-icon="star">Bookmarks</a></li>
                <li><a href="#Settings" data-role="button" data-iconpos="bottom" data-icon="gear">Settings</a></li>
            </ul>
        </div>
    </div>
</div>

The Save onclick handler looked like this:

function Save_OnClick() {
    var userName = $("#txtUserName").val();
    var password = $("#txtPassword").val();
    var syncKey = $("#txtSyncKey").val();

    if (!userName) {
        displayMessage("User name cannot be blank", "Settings");
        return false;
    }
    if (!password) {
        displayMessage("Password cannot be blank", "Settings");
        return false;
    }
    if (!syncKey) {
        displayMessage("Sync key cannot be blank", "Settings");
        return false;
    }

    clearMessagePanel("Settings");
    $.mobile.showPageLoadingMsg();
    loadBookmarks(userName, password, syncKey, "Save");

    return false;
}

It called this common function for getting bookmark data:

function loadBookmarks(userName, password, syncKey, action) {
    $.ajax({
        type: "POST",
        url: "Default.aspx/LoadBookmarks",
        contentType: "application/json; charset=utf-8",
        data: "{'userName':'" + userName + "', 'password':'" + password + "', 'syncKey':'" + syncKey + "'}",
        dataType: "json",
        headers: {"LoadAction":action},
        error: function (error) {
            var resp = JSON.parse(error.responseText);
            displayMessage(resp.Message, "Settings");
        },
        success: function (data) {
            localStorage.setItem("CurrentBookmarks", JSON.stringify(data.d));

            if (action === "Refresh") {
                localStorage.setItem("BookmarkCount", data.d.Tag);
            }
            else if (action === "Save") {
                localStorage.setItem("UserName", userName);
                localStorage.setItem("Password", password);
                localStorage.setItem("SyncKey", syncKey);
            }
        }
    });
}

I added a handler for the jQuery ajaxCompleted event and used that to handle the post-retrieval stuff, like hiding the input controls or updating the bookmark count.

function ajaxCompleted(e, xhr, settings) {
    if (settings.url === "Default.aspx/LoadBookmarks") {
        if (xhr.status < 400) {
            switch (settings.headers["LoadAction"]) {
                case "Save":
                    $.mobile.changePage("#Bookmarks");
                    break;
                case "Refresh":
                    loadSettingsPage();
                    break;
            }
        }

        $.mobile.hidePageLoadingMsg();
    }
}

At this point I tried everything out in Electric Plum to get an idea of how it might look on an actual iPhone. It was fairly good, only a couple of cosmetic issue came up that might not even be issues on a real phone.

Next, listviews and data binding.

This is part 2 in a series of posts about creating a web-based replacement for the Firefox Home iOS app. Part 1 can be found here.

I set up my bookmark browser web app project and started reading through the excellent jQuery Mobile site. I planned to have a home page, a page for settings, an About page, and one to show the actual bookmarks. So I created separate .aspx pages, each set to use a single master page, and added a page template to each one. The master page had a link to a CSS file I created and links to all the jQuery scripts hosted at code.jquery.com. It also had a single JQuery Mobile header and footer since I didn’t want to have to duplicate markup over several pages. I put in a few text boxes on the settings page and added a button for saving Sync credentials to local storage. I made it so when your credentials were saved, it would hide the input controls via JavaScript and show a button for refreshing the data.

When I ran it and tried out the navigation, things were not working as expected. The page state wasn’t being saved when I would browse from the settings page to the home page and back, meaning my hiding and showing of elements was failing. I had put links to each page in the main footer, and they would take you to the respective page, but old content was being shown. I was adjusting styles between each build, and those changes weren’t being reflected properly. In short, it was a mess.

After more reading through the JQuery Mobile site, I realized I was going to have to change my approach. Probably the most unique thing about JQuery Mobile is the navigation paradigm. It’s very different from what I was used to. Each time it needs to access content, it will make an AJAX call behind the scenes and insert that content into the DOM. There are ‘transitions’ between pages, but they are not pages in the sense of separate files in your project. They can be, but everything ends up in one big container anyway.

I ended up using multi-page templates, all housed in default.aspx. I ditched my master page and set up separate div elements for each page. I had to duplicate the header and footer markup, which I’m not thrilled about, but it isn’t much and I might find a better solution in the future. I experimented with using true single-page templates, but they just didn’t work how I wanted, whereas in the multi-page scenario everything flowed as I expected. DOM size wasn’t too much of a concern because apart from the page for bookmarks, the other pages have little markup in them. It may be possible to incorporate a master page into the navigation model, but I’d rather press on with the actual functionality.

Next, how to handle transitions to the current page, and others in the DOM.