Building a Dynamic Application Menu with Durandal.js, Knockout, and Bootstrap (Pt. 3)

Categories: JavaScript

Tags: Bootstrap, Durandal, Knockoutjs, KoLite

In the last two posts of this series, we built a dynamic menu system.  Now it is time to wrap it up with a discussion on how to actually populate and use these menus.

One idea is to create the concept of a workspace which represents the UI that the user sees for the application.  The workspace is like a top-level window in a desktop application.  The following module defines a workspace that contains a list of menus and defines a routine to take arbitrary menu layout objects and convert them to Menu and MenuItem instances:

define(function (require) {
    var Menu = require('ui/menu'),
        MenuItem = require('ui/menuItem'),
        menus = ko.observableArray([]);

    function setupWorkspace(cmds) {
        menus([]);

        var menus = {
            "File": [
                { text: "New", command: cmds.new },
                { text: "Open", command: cmds.open },
                { divider: true },
                { text: "Save", command: cmds.save },
                { text: "Save As", command: cmds.saveas },
                { divider: true },
                { text: "Sign out", command: cmds.signout }
            ],
            "Edit": [
                { text: "Cut", command: cmds.cut },
                { text: "Copy", command: cmds.copy },
                { text: "Paste", command: cmds.paste }
            ],
            "View": [
                { text: "View Mode", subItems: [
                    { text: "Simple", command: cmds.toggleSimpleView },
                    { text: "Advanced" command: cmds.toggleAdvancedView }
                ]}
            ],
            "Help": [
                { text: "Contents", command: cmds.helpcontents },
                { divider: true },
                { text: "About", command: cmds.about }
            ]
        };

        loadMenus(menus);
    }

    function loadMenus(menuDefinitions) {
        var menuText, menu;
        for (menuText in menuDefinitions) {
            menu = addMenu(menuText);
            addMenuItems(menu, menuDefinitions[menuText]);
        }
    }

    function addMenuItems(menuOrMenuItem, itemDefinitions) {
        for (var i = 0; i < itemDefinitions.length; i++) {
            var definitionItem = itemDefinitions[i];
            if (definitionItem.hasOwnProperty("divider")) {
                menuOrMenuItem.addDivider();
            }
            else {
                var menuItem = new MenuItem(definitionItem.text, definitionItem.command);
                menuOrMenuItem.addMenuItem(menuItem);
                if (definition.hasOwnProperty("subItems")) {
                    addMenuItems(menuItem, definitionItem.subItems);
                }
            }
        }
    }

    function addMenu(text, position) {
        var menu = new Menu(text);
        if (position) {
            menus.splice(position, 0, menu);
        }
        else {
            menus.push(menu);
        }

        return menu;
    }

    var workspace = {
        menus: menus,
        addMenu: addMenu,
        setupWorkspace: setupWorkspace
    };

    return workspace;
});

 

The main application shell should call the workspace singleton’s setupWorkspace() function and pass in an object that contains references to the desired ko.commands that will get attached to the menu items.  It can also use the menus property in its data-binding to automatically create the UI (as seen in part 2 of this series).

The setupWorkspace() function creates a menu definition which is just an inline object literal.  The source for this could actually come from the server as JSON, or be in another file, or loaded by a plugin.  The point is that there is a definition format that gets fed into the loadMenus() function that builds the menus by converting the definition into real Menu and MenuItem instances and adding them to the collection.

The workspace module also exports the addMenu() function which allows someone to add a menu to the menu bar after the initial setup has taken place.  I think more functions (like remove) could be added if you really want to make this robust as far as configuration of menus is concerned (I’m just demoing this to illustrate a point).  And obviously, the commands aren’t built and this is very demo-specific, but you can just swap that out for whatever you want.  You could even send the menu definitions to the setupWorkspace() function instead of embedding it directly in the function.

You can view a live demo of this series at: http://tblabonne.github.io/DynamicMenus/

The complete source to the demo can be found at: http://github.com/tblabonne/DynamicMenus

No Comments