19

Is there a way to use the Ext.app.Controller control() method, but pass in a DOM query? I have a page that contains standard links and would like to add a click handler to them even though they were not created as Ext Buttons.

I've tried

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

    init: function() {
        console.log("init");
        this.control({
            'a': {
                click: this.changeTab
            }   
        });
    },

    changeTab: function() {
        alert("new tab!");
    }   
});

But clicking on links does not fire the alert.

Is there a way to specify a CSS selector with this.control? Or does it only work with components?

Nathan Voxland
  • 13,759
  • 1
  • 44
  • 60
  • I'm sure you have long since come up with some other work around, but I did find a workable solution to the problem above (answered below). Maybe it will help in the future? – LittleTreeX Sep 23 '11 at 21:37

5 Answers5

39

I asked this question at SenchaCon this year, the Sencha developers stated that their intent is that DOM listeners should be attached within your view, and the view should abstract them into more meaningful component events and refire them.

For example, suppose you're creating a view called UserGallery that shows a grid of people's faces. Within your UserGallery view class, you would listen for the DOM click event on the <img> tag to receive event and target, and then the view might fire a component event called "userselected" and pass the model instance for the clicked user instead of the DOM target.

The end goal is that only your views should be concerned with things like interface events and DOM elements while the application-level controller only deals with meaningful user intents. Your application and controller code shouldn't be coupled to your markup structure or interface implementation at all.

Sample View

Ext.define('MyApp.view.UserGallery', {
    extend: 'Ext.Component'
    ,xtype: 'usergallery'

    ,tpl: '<tpl for="users"><img src="{avatar_src}" data-ID="{id}"></tpl>'

    ,initComponent: function() {
        this.addEvents('userselected');

        this.callParent(arguments);
    }

    ,afterRender: function() {
        this.mon(this.el, 'click', this.onUserClick, this, {delegate: 'img'});

        this.callParent(arguments);
    }

    ,onUserClick: function(ev, t) {
        ev.stopEvent();

        var userId = Ext.fly(t).getAttribute('data-ID');

        this.fireEvent('userselected', this, userId, ev);
    }
});

Notes on views

  • Extend "Ext.Component" when all you want is a managed <div>, Ext.Panel is a lot heavier to support things like titlebars, toolbars, collapsing, etc.
  • Use "managed" listeners when attaching listeners to DOM elements from a component (see Component.mon). Listeners managed by a components will be automatically released when that component gets destroyed
  • When listening for the same event from multiple DOM elements, use the "delegate" event option and attach the listener to their common parent rather than to individual elements. This performs better and lets you create / destroy child elements arbitrarily without worrying about continuously attaching/removing event listeners to each child. Avoid using something like .select('img').on('click', handler)
  • When firing an event from a view, Sencha's convention is that the first parameter to the event be scope -- a reference to the view that fired the event. This is convenient when the event is being handled from a controller where you'll need the actual scope of the event handler to be the controller.

Sample Controller

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

    ,init: function() {
        this.control({
            'usergallery': {
                userselected: function(galleryView, userId, ev) {
                    this.openUserProfile(userID);
                }
            }   
        });
    }

    ,openUserProfile: function(userId) {
        alert('load another view here');
    }
});
The Mighty Chris
  • 1,265
  • 10
  • 16
3

I have found a work around for this problem. It isn't as direct as one may hope, but it leaves all of your "action" code in the controller.

requirment: Wrap the html section of your page in an actual Ext.Component. This will likely be the case either way. So for instance, you may have a simple view that contains your HTML as follows:

Ext.define('app.view.myView', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.myView',
    title: 'My Cool Panel',
    html: '<div><a href="#">This link will open a window</a></div><br /> <label for="myInput">Type here: </label><input name="myInput" type="text" value="" />',
    initComponent: function(){
        var me = this;
        me.callParent(arguments);
    }
});

Then in the controller you use the afterrender event to apply listeners to your DOM elements. In the example below I illustrate both links (a element) and input elements:

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

    init: function() {
        this.control({
            'myView': {
               afterrender: function(cmp){
                    var me = this; //the controller
                    var inputs = cmp.getEl().select('input'); // will grab all DOM inputs
                    inputs.on('keyup', function(evt, el, o){
                        me.testFunction(el); //you can call a function here
                    });

                    var links = cmp.getEl().select('a'); //will grab all DOM a elements (links)
                    links.on('click', function(evt, el, o){ 
                        //or you can write your code inline here
                        Ext.Msg.show({
                            title: 'OMG!',
                            msg: 'The controller handled the "a" element! OMG!'
                        });
                    });
                }
            }   
        });
    },
    testFunction: function(el) {
        var str = 'You typed ' + el.value;
        Ext.Msg.show({
            title: 'WOW!',
            msg: str
        });
    }   
});

And there you have it, DOM elements handled within the controller and adhering to the MVC architecture!

LittleTreeX
  • 1,229
  • 2
  • 12
  • 27
1

No, this seems not to be possible. The Ext.EventBus listens to events fired by ExtJS components. Your standard DOM elements do not fire those events. Additionally the query is checked with the ExtJS componets is( String selector ) method, wich can't be called by DOM elements. Someone might correct me if i'm wrong, but so i'm quite sure it's not possible, unfortunately.

aveltens
  • 303
  • 1
  • 9
0

I also have a solution that works around this problem. I use this technique regardless though as it has other benefits: I created an application wide messaging bus. Its an object in my application that extends Observable, and defines a few events. I can then trigger those events from anywere in my app, including html a links. Any component that wants to listen to those events can relay them and they'll fire as if fired from that component.

Ext.define('Lib.MessageBus', {
    extend: 'Ext.util.Observable',

    constructor: function() {
        this.addEvents( 
                  "event1",
                  "event2"
                 );
        this.callParent(arguments);
    }
});

Then, each other compnent can add this after initialisation:

this.relayEvents('Lib.MessageBus', ['event1','event2']);

and then listen to those events. You can trigger the events from anything by doing:

Lib.MessageBus.fireEvent('event1', 'param a', 'param b')

and you can do that from anything including html links.

Very handy to fire off events from one part of the app to another.

Harel
  • 1,751
  • 2
  • 22
  • 39
0

I had the same problem recently (mixing mvc with some non components). Just thought I'd throw this in as an answer as it seems pretty simple and works for me :)

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

    init: function() {
        console.log("init");
        this.control({
           /* 'a': {
                click: this.changeTab
            } */  
        });

    var link = Ext.dom.Query.selectNode('a');

    Ext.get(link).on('click', this.changeTab);

    },

    changeTab: function() {
        alert("new tab!");
    }   
});
TConere
  • 61
  • 1
  • 6