I did some interesting things with Sencha Touch over the last month, and I am going to share some of them in a series of blog posts over the next couple of days. But I am running really short of time. So I would let the code do the talking, and there would be minimal explanations (if any) in the posts themselves on what the code does and how.

Hopefully the live examples would help you understand most of the pieces involved (if you have a question, please feel free to use the comment form). If I get time later, I will come back to the posts adding explanations to make them more useful.

So much for the introduction, let’s switch to the topic of this blog post now.

So it happened that I needed menus in Sencha Touch 2 (that is still in its Developer Preview 2 when these lines are being written), and there was nothing available out-of-the-box (which came as a bit of surprise to me). Well necessity is the mother of invention, and so were born a couple of classes, Ext.ux.MenuButton and Ext.ux.Menu that work together to provide an extensive menu component for Touch 2, complete with support for any depth of nested sub-menus and ExtJs like config driven approach to create complex nested sub-menus.

Here’s the live example (click here to open in new window):

 

And following is the code which provides the menu component (contains both Ext.ux.MenuButton and Ext.ux.Menu classes). Please note that most of the time, you would directly use the Ext.ux.Menu class only, it automatically uses MenuButton internally to help it manage sub-menus and other aspects. Here’s the code:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.define(‘Ext.ux.MenuButton’, {
extend: ‘Ext.Button’,
xtype: ‘ext_ux_menubutton’,

config: {},

doTap: function(e) {
if (!this.getDisabled()) {
if (this.items) {
if (!this._menu) {
this._menu = new Ext.ux.Menu({
_ownerMenu: this.ownerCt,
items: this.items
});
this.ownerCt._childMenus.push(this._menu);
}

//Hide all other menus except those in active trail.
this._hideNonActiveTrail();

if (!this._menu.getHidden()) {
//Without this, the body gets masked on second click – Touch bug??
this._menu.hide();
}

this._menu.showBy(this, ‘slide’, ‘tl-tr’);
} else {
//Leaf item tap, hide all parents.
this.hideMenu();
}
}

this.callParent(arguments);
},

hideMenu: function(hideParents) {
this.ownerCt.hide();

if (hideParents !== false) {
var menu = this.ownerCt._ownerMenu;
while (menu) {
menu.hide();
menu = menu._ownerMenu;
}
}
},

_hideNonActiveTrail: function() {
var activeTrail = {};
var menu = this.ownerCt;
while (menu) {
activeTrail[menu.id] = true;
menu = menu._ownerMenu;
}

for (var id in Ext.ux.Menu.menuComponents) {
if (!activeTrail[id]) {
Ext.ux.Menu.menuComponents[id].hide();
}
}
}
})

Ext.define(‘Ext.ux.Menu’, {
extend: ‘Ext.Sheet’,
xtype: ‘ext.ux.menu’,

config: {
defaultType: ‘ext_ux_menubutton’,
childMenus: null
},

statics: {
menuComponents: {},
classInitialized: false,

initializeClass: function() {
Ext.getBody().on(‘tap’, Ext.ux.Menu._hideNonActiveTrail);
Ext.ux.Menu.classInitialized = true;
},

_hideNonActiveTrail: function(e, t, t2, o, dispatcher) {
for (var id in Ext.ux.Menu.menuComponents) {
var menu = Ext.ux.Menu.menuComponents[id];
var hide = true;
if (menu._childMenus.length == 0) {
//This is a leaf menu.
var current = menu;
while (current) {
if (current.el.contains(t)) {
hide = false;
break;
} else if (current._referenceElement) {
if (current._referenceElement.element.contains(t)) {
hide = false;
break;
}
}

current = current._ownerMenu;
}

if (hide) {
current = menu;
while (current) {
current.hide();

current = current._ownerMenu;
}
}
}
}
}
},

constructor: function(config) {
if (!Ext.ux.Menu.classInitialized) {
Ext.ux.Menu.initializeClass();
}

config = config || {};

Ext.applyIf(config, {
items: [],
childMenus: []
});

this._processItems(config.items);

this.callParent(arguments);
},

initialize: function() {
this.callParent(arguments);
Ext.ux.Menu.menuComponents[this.id] = this;
},

destroy: function() {
//Recursively destroy child menus.
//TODO: Need to ensure this works correctly.
var doRecurse = function(items) {
Ext.each(items, function(item) {
doRecurse(item._childMenus);
item.destroy();
});
}
doRecurse(this._childMenus);

delete Ext.ux.Menu.menuComponents[this.id];
this.callParent(arguments);
},

showBy: function(reference) {
this._referenceElement = reference;
this.callParent(arguments);
},

_processItems: function(items) {
Ext.each(items, function(item) {
if (item.items) {
Ext.applyIf(item, {
ui: ‘forward’
});
}
});
}
}); {/syntaxhighlighter}

 

As I said, I will try to explain what it does and how later as and when I get time. For the time-being, you should be more interested in how to use it. And here’s the code that produces the button in the toolbar together with the menu that pops-up when you click it:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }{
xtype: ‘button’,
text: ‘Click to open menu’,
handler: function() {
if (!this.menu) {
this.menu = new Ext.ux.Menu({
items: [
{
text: ‘Level 1 – Item 1’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 1 – Item 2’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 1 – Item 3 with sub-menu’,
items: [
{
text: ‘Level 2 – Item 1’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 2 – Item 2 with sub-menu’,
items: [
{
text: ‘Level 3 – Item 1’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 3 – Item 2’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 3 – Item 3’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
}
]
},
{
text: ‘Level 2 – Item 3 with sub-menu’,
items: [
{
text: ‘Level 3 – Item 4’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 3 – Item 5’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
},
{
text: ‘Level 3 – Item 6’,
handler: function() {
Ext.Msg.alert(”, this.getText() + ‘ clicked.’);
}
}
]
}
]
}
]
});
}

//Without this, the body gets masked on second click – Touch bug??
this.menu.hide();

this.menu.showBy(this);
}
}{/syntaxhighlighter}

I am going to explain the button code briefly. In the handler for the button, we first check if a menu has been created for this button. If not, we go ahead and create it (you can see, the if inside the handler would execute only once).

The menu itself is just a regular nested combination of items array and config objects for menu-items like you have in ExtJs. You can go into any level of nesting, but please ensure not to give items array for a menu item if that is not supposed to have a sub-menu.

Menu items with sub-menus are shown with Touch’s “forward” button ui by default, but you can easily override that in configuration for any menu-item.

And finally we show that menu item aligned to the button by using the showBy method available on all components. Although its not documented, but you can pass a third parameter to showBy method specifying a traditional Ext-like anchor configuration to control where the menu appears in relation to your button or any other reference element (please ensure to pass the second argument as null/undefined if you are passing the third argument to showBy as second argument is not supported by Touch 2 as I write this and would result in an exception).

That’s all I have for now. The example is produced by the sample code attached below.