Tag: <span>KnockoutJS</span>

It’s been over a year since I last touched my bookmark browser, and it needed some attention. There were a couple of outstanding bugs to deal with, plus it seemed like a good time to update the various libraries it uses.

One bug that occasionally cropped up is logging in for the first time and changing to the bookmark page, but having nothing show up. If I did a full refresh of the page then everything would be fine. I never quite figured out what was going on, until now. I assumed there was something I wasn’t doing right with Knockout, so I tried moving the code that does the bindings from the PageBeforeShow event for the bookmark page to right after the AJAX call that gets the data. But that didn’t help. Turns out it was something simple: the bookmark container is initially hidden, and I was never showing it after the first successful login, assuming the app was started in a logged-out state. The solution was showing the container right after the bindings were applied.

And speaking of content not showing right, it suddenly became necessary to add an explicit jQuery Mobile refresh call on the list that holds the bookmark data, like this:

$("#bmMain").listview("refresh");

Without such a call none of the jQuery Mobile styles would be applied unless I did a full page refresh. Not sure why, but whatever. The latest version did resolve a weird style issue with the bookmark menu list showing a double border around each list item, so that’s good.

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.