Durandal
-
Building a Dynamic Application Menu with Durandal.js, Knockout, and Bootstrap (Pt. 3)
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
Categories: JavaScript
Tags: Bootstrap, Durandal, Knockoutjs, KoLite
-
Building a Dynamic Application Menu with Durandal.js, Knockout, and Bootstrap (Pt. 2)
In the previous post, I laid out a design for creating a dynamic menu system, specifically the object model that will be used in data binding. In this post, we’ll look at the Knockout bindings and HTML structure to render the menus.
This will be pretty straightforward as far as taking our object model and applying markup to it, however, the one complicated part is dealing with sub-menus. Because menu items can themselves contain other menu items, we need the ability to render a menu within a menu and so on. We can do this with Durandal’s compose binding handler for KO. It allows recursive composition that is perfect for hierarchical things like menus.
Here’s the contents of views/menu.html which is the view component for a single Menu rendering:
<ul class="dropdown-menu" data-bind="foreach: items"> <!-- ko if: $data.text !== undefined --> <li data-bind="css: { disabled: !command.canExecute(), 'dropdown-submenu': hasSubMenu }"> <!-- ko ifnot: $data.hasSubMenu --> <a href="#" tabindex="-1" data-bind="command: command"> <span data-bind="text: text"></span> </a> <!-- /ko --> <!-- ko if: $data.hasSubMenu --> <a tabindex="-1" data-bind="text: text"></a> <!-- ko compose: { view: 'views/menu.html', model: $data } --> <!-- /ko --> <!-- /ko --> </li> <!-- /ko --> <!-- ko if: $data.divider !== undefined --> <li class="divider"></li> <!-- /ko --> </ul>
This markup is a bit complicated so let’s go through it:
- In Bootstrap, applying the dropdown-menu class to a <ul> will style it as a drop down menu container. The data-bind here is also set to loop over each item in the Menu object.
- Within the <ul>, we need to decide if the MenuItem is a text-based menu item or a divider. We do this by detecting if the divider property exists and/or there is text to display. If the MenuItem is a divider, the special divider class is applied to a <li> and Bootstrap renders it as a thin gray line.
- If the MenuItem is actually a text-based item, we style it appropriately in its <li> element. Notice the binding to command.canExecute(). In KoLite, if you provide a canExecute() function on a command (with is a computed observable), it can determine if the command can be executed or not. In this case, we want the UI to gray-out or disable if the command cannot execute. Once the command can execute, it will immediately synchronize the UI element to be a clickable command.
- Inside the <li>, we check to see if the MenuItem is a sub-menu or not. If it is not, we create an <a> element as desired by Bootstrap to create the link to click on, binding the appropriate command to it. We also bind the text in a <span> element inside the <a>.
- If the MenuItem does have a sub-menu, we assume that the MenuItem can’t be clickable, but instead, simply groups other elements. In that case, we call upon the Durandal compose binding handler to recursively call this view sending the MenuItem as the view model to bind to in that context.
Now, to render the top-level menu bar, in our main view we’d add the following markup (I’m using nav-pills in Bootstrap to represent the top-level menu items, but you don’t need to do that):
<ul class="nav nav-pills menu-bar" data-bind="foreach: menus"> <li class="dropdown"> <a class="dropdown-toggle" href="#" data-toggle="dropdown" role="button" data-bind="text: text"></a> <!-- ko compose: { view: 'views/menu.html', model: $data } --> <!-- /ko --> </li> </ul>
This assumes that the view model for the main view has a collection of Menu objects in an observableArray called menus.
It renders a <li> element for each Menu giving it a dropdown class. The <a> element will trigger Bootstrap to open the <ul> element that immediately follows the <a>. That <ul> is generated by a compose binding calling onto the Menu view to render that one Menu and all of its children recursively.
Categories: JavaScript
Tags: Durandal, Knockoutjs, KoLite
-
Building a Dynamic Application Menu with Durandal.js, Knockout, and Bootstrap (Pt. 1)
I’m going to do a longer series here about how to create a dynamic menu bar system with Durandal, Knockout, and Twitter Bootstrap. This menu bar is going to emulate the traditional desktop application menu bar you find in apps (like the File, Edit, or View menus, for example). The special thing here is that it will be 100% dynamic. This will allow interesting scenarios such as dynamically altering the application menu when the application is in a different mode or allow something like plug-ins to alter the menu structure adding new commands.
We will use the following libraries/frameworks to perform this task:
- We’ll use Bootstrap to style the menus and get basic drop down menu support. The menus in Bootstrap look very pretty and work very well using just one .js file for behavior and a little bit of markup.
- We’ll use Durandal to structure the application and to take advantage of the composition engine it has. I assume you know how to get a basic Durandal project up and running so I’m not going to spend a lot of time discussing how Durandal works.
- We’ll use Knockout to do all of the data binding. Our menu items will have observable properties in them so that menus will dynamically change when you change things about them in code.
- We’ll also make use of KoLite by John Papa which provides a simple KO extension (the command) to abstract the idea of a UI command. A single menu item will wrap a ko.command(). If a command does not allow execution, the corresponding menu item(s) will not allow it and will appear disabled. Also, when clicking on a menu item, the command will execute. This will all happen through data binding and will not
For this first part, let’s build the JavaScript object model for the menu system. A Menu object (defined in ui/menu.js in my project) represents one menu on a menu bar (such as a File menu). It will contain zero or more MenuItems which are the individual menu selections in the menu. There is also a special menu item that acts as a divider or separator. These dividers draw thin lines between menu items to help visually group commands. They are not clickable.
Here’s the code for the MenuItem class (defined in ui/menuItem.js as a Durandal module):
define(function (require) { var MenuItem = function (itemText, command, items) { this.text = ko.observable(itemText); this.command = command || ko.command({ execute: function () { } }); this.items = ko.observableArray(items || []); this.hasSubMenu = ko.computed(function () { return this.items().length > 0; }, this); }; MenuItem.prototype.addMenuItem = function (menuItem, position) { if (position) { this.items.splice(position, 0, menuItem); } else { this.items.push(menuItem); } }; MenuItem.prototype.addDivider = function (position) { var item = { divider: true }; if (position) { this.items.splice(position, 0, item); } else { this.items.push(item); } }; return MenuItem; });
A menu item takes a menu item text (the text to appear in the menu), an optional KoLite command, and an optional set of child sub-items. The sub-items are used for when the menu item is actually a menu within a menu and will be rendered with a an arrow to the right of the menu item using Bootstrap.
A Menu class also exists as a top-level container for MenuItems. Think of this as the File menu or Edit menu. It is defined in ui/menu.js as a Durandal module:
define(function (require) { var MenuItem = require('ui/menuItem'); var Menu = function (text, items) { this.text = ko.observable(text); this.items = ko.observableArray(items || []); }; Menu.prototype.addMenuItem = function (menuItem, position) { if (position) { this.items.splice(position, 0, menuItem); } else { this.items.push(menuItem); } return menuItem; }; return Menu; });
This class takes the text of the menu item (“File”) and the collection of MenuItems to add to the menu. You can call addMenuItem() to add a menu item after the initial creation of the menu. The position parameter will add the menu in the specified position. If you don’t specify a position, it will be added to the end of the menu.
In the next part of the series, we’ll look at the HTML and KO data-bindings that will render the menus.
Categories: JavaScript
Tags: Bootstrap, Durandal, Knockoutjs, KoLite
- 1