PDA

View Full Version : Best Practice for Handling Events in Subcomponents



mminnie
21 Jan 2012, 8:50 AM
I want to once a gain thank the developers and contributors to ExtJS. There is much great documentation on the framework and many great forum posts, but I do see some (including some ExtJS 3.x experienced users) taking a bit to grasp the MVC of ExtJS 4.x. I think my question is fairly simple, but I didn't find a forum post or other documentation that answered my question. This may just be that the MVC structure of 4.x hasn't had time to generate as big of a knowledge base as earlier versions.

I see many ways to handle events, but I'm interested in what is the best ExtJS 4.x MVC way to handle the events of a view. I have a user grid panel that is contained in a tab panel. I want to handle a double click on the user record in the grid to open up another tab for that specified user. My question is: Is the BEST PLACE in the ExtJS MVC design to "bubble-up" the grid itemdblclick event (or any other event from a view controlled by a controller) to the controller (User controller in my case)?

30882


Ext.define('LB.controller.Users', {
extend: 'Ext.app.Controller',

views: ['user.List','user.Tabs'],
stores: ['Users'],
models: ['User'],
init: function() {
this.control({
'userlist *': {
itemdblclick: function(dataview, record, item, index, e) {
// Is this where YOU would put the handling of a itemdblclick for the grid?
console.log(record.get('username')+ ' was double clicked.');
}
}
})
}
});


Ext.define('LB.view.user.Tabs' ,{
extend: 'Ext.tab.Panel',
alias : 'widget.usertabs',
resizeTabs: true,
enableTabScroll: true,
defaults: {
autoScroll:true
},
items: [{
title: 'User List',
items: [{
xtype:'userlist',
id: 'userlist'
}],
closable: false


}]
});


Ext.define('LB.view.Viewport', {
extend: 'Ext.Viewport',
layout: 'fit',

initComponent: function() {
this.items = {
items: [{
xtype: 'usertabs'
}]

};

this.callParent();
}
});

nkezhaya
21 Jan 2012, 12:41 PM
Absolutely. All of that logic should be in the controller, but you should use "itemId", or a more specific selector.

This is a better way to do it:


Ext.define('Controller', {
extend: 'Ext.app.Controller',

refs: [
// refs here
],

init: function() {
this.control({
'#gridPanel': {
itemdblclick: this.onGridPanelItemDblClick
}
});
},

onGridPanelItemDblClick: function() {
// code here
}
});

You shouldn't actually write functions in the controller's initialization. That can get hard to read, really quickly.

skirtle
22 Jan 2012, 12:16 AM
Personally I wouldn't use an itemId without putting it in the context of an owner container. I also try to avoid control queries that don't include a custom xtype. So assuming the grid doesn't have its own alias the selector might be something like 'userlist #gridPanel'.

A couple of other things I would change...

You should avoid using static ids, e.g. id: 'userlist'.

Though it's difficult to be sure, it looks like your code might be suffering from excessive panel nesting. For example, here:


items: [{
title: 'User List',
items: [{
xtype:'userlist',
id: 'userlist'
}],
closable: false
}]

Why not just this?


items: [{
closable: false,
title: 'User List',
xtype: 'userlist'
}]

Implicit in the selector in your original question is that your grid is wrapped in a yet another panel with xtype userlist. From the screenshot it isn't clear why. Shouldn't userlist be the grid?

mminnie
22 Jan 2012, 9:01 AM
Personally I wouldn't use an itemId without putting it in the context of an owner container. I also try to avoid control queries that don't include a custom xtype. So assuming the grid doesn't have its own alias the selector might be something like 'userlist #gridPanel'.


Being new to ExtJS, I didn't realize the power of the component query. I looked through the generated HTML to determine how I would pinpoint the grid. Now I realize the component query can do this through xtypes (how....it can only be magic!). So my selector simply can be 'userlist gridview'. This is different from 'userlist #gridview'. The query didn't work with the hash mark before gridview. Am I missing something or was this an oversight on your post?

And yes, I had excessive panel nesting. One reason, being a newbie and just getting it working. The other reason, I hadn't yet refactored my code.

As for the userlist, it is a grid panel. I forgot to include the code in the original post.


Ext.define('LB.view.user.List' ,{
extend: 'Ext.grid.Panel',
alias : 'widget.userlist',
store : 'Users',
initComponent: function() {
this.columns = [
{header: 'User ID', dataIndex: 'id', resizable: false, width:55},
{header: 'User Name', dataIndex: 'username', flex: 4},
{header: 'First Name', dataIndex: 'firstname', flex: 6},
{header: 'Last Name', dataIndex: 'lastname', flex: 6}
];

this.callParent(arguments);
}
});



@skirtle: Thanks for the clarification on everything, and thanks for going the extra mile in helping by pointing out places to improve the code above and beyond the original question. When I get enough knowledge, I will return the favor to forum users. For now, I'm feverishly learning!

skirtle
22 Jan 2012, 11:30 AM
I suggest having a read of this page in the docs, it doesn't cover everything but it may fill in some of the blanks:

http://docs.sencha.com/ext-js/4-0/#!/api/Ext.ComponentQuery

It includes an explanation of the # syntax.

The itemdblclick for a gridview is relayed by the grid itself, so there's no need to target the view specifically. Just listen for this event on the grid instead. You should end up with a CQ selector of just userlist.

mminnie
22 Jan 2012, 11:52 AM
The # makes sense. It is just like a CSS definition. Thanks for the link.

Can I assume the component query userlist with the itemdblclick does NOT fire when the grid header, which is part of the userlist component extended from grid panel, is double clicked because the grid header catches the event and prevents the event from bubbling up to the userlist component?

Thanks again for taking the time to explain. Little bits of help like this keeps us newbies going. :D

mattgoldspink
22 Jan 2012, 11:55 AM
Maybe this is just my preference, but I wouldn't even use the itemdblclick event in the controller. The way I like to look at my views and controllers is a little more higher level. A view is a representation of a model (or set of models) being displayed and the a view can allow a user to interact and perform actions. These actions are normally more high level, for example a double click on user might mean 'Edit this user', in that case I would get my view to relay an 'editUser' event with all the relevant details the controller might need.

By taking this approach you can set yourself up to changing the view more easily without having to touch the controllers. For example if you decide you need a mobile interface then itemdblclick doesn't make sense any more and you'd need to either rewrite a new set of controllers to handle this, or add more complexity to the controller to handle 'itemdbltap' for example. However if you get all views to relay the same editUser event then the controller need not be changed. Another good example is re-use across multiple views, if your component query matches multiple views then you can relay this same event from anywhere, similarly if the editUser event in one view needs to be in a grid and in another view it comes from a button (perhaps a detailed user view) then there's no need to touch the controller.

This is at least how we have been working with MVC in Sencha Touch and it has helped us to produce a phone ui first and then on tablets load a different set of views but re-use exactly the same controllers. We've also started doing the same with Ext 4.x and again it's helped a huge amount of complexity

HTH,
Matt

skirtle
22 Jan 2012, 3:53 PM
Events prefixed with item are events for the items in a view (that's an Ext.view.View, not a view in the MVC sense). For a grid view the items are the rows. So itemdblclick only applies to rows, not the header.

Regarding mapping events to more meaningful names like editUser. I can see the benefit of that but I haven't tried it myself so I can't speak from personal experience. Approaching it from a purely theoretically perspective, it feels like you're moving controller logic into the view. A view should understand concepts like clicks or keypresses and no more. Mapping to events such as editUser or deleteProduct is giving those events extra meaning at the view level. I think there's a clue in the names. itemdblclick describes an event that has occurred to the grid. editUser however does not - it describes how you want that event to be handled. That feels like the observable prejudging the observer to me. The decision to treat a double-click as an edit should be in the controller.

My instinct would be to keep the view events descriptive of the user interactions. It seems pretty trivial to wire up multiple events to the same handler at the controller level without building extra logic into the view to abstract it away.

Of course in practice I may be totally wrong... what's convenient in practice doesn't always tie up with theoretical idealism.

mminnie
22 Jan 2012, 7:26 PM
By taking this approach you can set yourself up to changing the view more easily without having to touch the controllers. For example if you decide you need a mobile interface then itemdblclick doesn't make sense any more and you'd need to either rewrite a new set of controllers to handle this, or add more complexity to the controller to handle 'itemdbltap' for example. However if you get all views to relay the same editUser event then the controller need not be changed. Another good example is re-use across multiple views, if your component query matches multiple views then you can relay this same event from anywhere, similarly if the editUser event in one view needs to be in a grid and in another view it comes from a button (perhaps a detailed user view) then there's no need to touch the controller.

Interesting approach. I see how having the same controller would have it's advantages. If the controller does not change, and the logic is to call a view EditUser, do you name the phone UI view and the tablet UI view the same and just reference the correct view in the app...such as appPhone.js and appTablet.js?

jay@moduscreate.com
22 Jan 2012, 7:29 PM
At all cost, do not use static ids. You will end up with an app that is brittle. Trust me on this.

mminnie
22 Jan 2012, 8:44 PM
No static IDs. Got it. I wasn't planning on keeping them, it was just part of a newbie's testing and figuring things out. Gone....and never again!;)

skirtle
23 Jan 2012, 3:27 AM
I've had a bit more of a think about my last post. I'm not convinced my logic actually holds up. If you have a button (a view) with the caption 'Edit User' then the view already implicitly has knowledge of how that view is controlled. My claim that having it fire an editUser event would be moving logic from the controller to the view is dubious at best.

What I would say instead is that mapping a click event to an editUser event is adding an extra layer of abstraction and you should think carefully about whether you need that extra abstraction before adding it to all your views.