Search code examples
javascriptextjsextjs4

Ext.bind does not see function in ExtJS


I am trying to mod the Portlet example and port it into our program. The code looks like this:

Ext.create('Ext.container.Viewport',{
            id: 'app-viewport', //creates viewport
            layout: {
                type: 'border',
                padding: '0 5 5 5' // pad the layout from the window edges
            },

            onPortletClose: function(portlet) {
                this.showMsg('"' + portlet.title + '" was removed');
            },

            showMsg: function(msg) {
                var el = Ext.get('app-msg'),
                    msgId = Ext.id();

                this.msgId = msgId;
                el.update(msg).show();

                Ext.defer(this.clearMsg, 3000, this, [msgId]);
            },

            clearMsg: function(msgId) {
                if (msgId === this.msgId) {
                    Ext.get('app-msg').hide();
                }
            },

            items: [{
                id: 'app-header',
                xtype: 'box',
                region: 'north',
                height: 40,
                html: 'Ext Welcome'
            },{
                xtype: 'container',
                region: 'center',
                layout: 'border',
                items: [{
                    id: 'app-options', //Creates the Options panel on the left
                    title: 'Options',
                    region: 'west',
                    animCollapse: true,
                    width: 200,
                    minWidth: 150,
                    maxWidth: 400,
                    split: true,
                    collapsible: true,
                    layout:{
                        type: 'accordion',
                        animate: true
                    },
                    items: [{
                        html: content,
                        title:'Navigation',
                        autoScroll: true,
                        border: false,
                        iconCls: 'nav'
                    },{
                        title:'Settings',
                        html: content,
                        border: false,
                        autoScroll: true,
                        iconCls: 'settings'
                    }]
                },{
                    id: 'app-portal', //Creates the panel where the portal drop zones are.
                    xtype: 'mainpanel',
                    region: 'center',
                    items: [{
                        id: 'col-1', //Each column represents a drop zone column. If we add more there are more created and width gets adjusted accordingly
                        items: [{
                            id: 'portlet-1',
                            title: 'Portlet 1',
                            html: content,
                            listeners: {
                                'close': Ext.bind(this.onPortletClose, this)
                        },{
                            id: 'portlet-2',
                            title: 'Portlet 2',
                            html: content,
                            listeners: {
                                'close': Ext.bind(this.onPortletClose, this)
                            }
                        }]
                    },{
                        id: 'col-2',
                        items: [{
                            id: 'portlet-3',
                            title: 'Portlet 3',
                            html: content,
                            listeners: {
                                'close': Ext.bind(this.onPortletClose, this)
                            }
                        }]
                    },{
                        id: 'col-3',
                        items: [{
                            id: 'portlet-4',
                            title: 'Portlet 4',
                            html: content,
                            listeners: {
                                'close': Ext.bind(this.onPortletClose, this)
                            }
                        }]
                    }]
                }]
            }]
        });

The problem is that Ext.bind cannot read the onPortletClose function and the browser gives me a:

Uncaught TypeError: Cannot read property 'apply' of undefined error. I checked the stack and essentially in the Ext.bind(fn,scope) the fn is undefined and thus it cannot read the handler function. The difference between my application and in the example is that I add this directly into a JSP's Ext.onReady() whereas in the example all of this is added through a Ext.apply(this, {...}). I'm really confused. I tried all kinds of gimmicks to force the scope on the viewport but it seems that whatever is inside Ext.bind() loses contact with outside or something. I've used Ext.bind() before and it went fine although it was inside an initComponent configuration. Is that mandatory? If no then what is the problem?


Solution

  • It is important to understand the meaning of this here (or in general when working with JavaScript or ExtJS).

    In a global context, this refers to the global object, also known as the global window variable. This is the case inside your Ext.onReady function:

    Ext.onReady(function() {
        console.log(this === window);
        // -> true
        // because this is the global object
    });
    

    In an object context, by default this refers to the owner object of the function (there are ways around it and actually this is exactly want you want do achieve using Ext.bind):

    var obj = {
       prop: 'My text property',
       fn: function() {
           console.log(this.prop);
           // -> My text property
           // because "this" refers to our object "obj"
       }
    };
    

    Now, this is exactly what makes the difference between your case and the one from the examples.

    When using the approach with Ext.onReady this - as pointed out above - will refer to the global object, which of course does not have a function onPortletClose.

    When using the approach from the examples, this is accessed from inside of initComponent, which is a member function of your derived viewport class, and therefore this refers to the component instance (= the owning object), which allows you to access your function.

    Ext.define('MyViewport', {
        extend: 'Ext.container.Viewport',
    
        onPortletClose: function(portlet) {
            this.showMsg('"' + portlet.title + '" was removed');
        },
    
        initComponent: function() {
            console.log(this);
            // -> the instance of your viewport class
    
            Ext.apply(this, {
                items:[{
                    xtype: 'panel',
                    title: 'Portlet 1',
                    listeners: {
                        'close': Ext.bind(this.onPortletClose, this)
                    }
                }]
            });
    
            this.callParent();
        }
    });
    

    Side notes

    Ext.apply

    Ext.apply really just copies all properties from one object to another, so in this case it is just a convenient way of applying all attributes of that object to this with one function call. You could as well just use:

    this.items = [...];
    

    Changing the scope of your listeners

    You do not need to use Ext.bind when adding listeners to a component, you can simply provide a scope configuration in your listeners object:

    listeners: {
        close: function() {...},
        scope: this
    }
    

    External sources

    If you want to dive in a little deeper (which I would recommend), you might want to read more about this at the MDN (Mozilla Developer Network).