Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Steps to overriding Sencha ExtJS standard component functionality (Ext.tree.Panel & Ext.data.TreeStore as two examples)

Suppose I am extending a standard Sencha ExtJS 4 widget/component, and I found a bunch of things that don't work the way I want them to, or perhaps they are just broken and Sencha hasn't gotten around to fixing the issues with the component yet. I'm just going to use the Sencha ExtJS Ext.tree.Panel and Ext.tree.Store as two example components. What are the most basic steps to overriding the constructor, configs, properties, methods and events so I can find and fix the issues with that component without modifying the core ExtJS 4 framework JS file I'm currently using?

I realize that sometimes there is so much functionality in the framework, that one might overlook a config somewhere and not realize they can fix the issue with a standard implementation. And that's something that can be corrected with more experience with the framework. Putting that aside here, what would be these most basic steps?

Suppose we start with these two implementations and start with the very basics.

FYI: I got the core features of these two components working without too much effort really using the Ext.Direct server side stack, and I could explain all of the cross browser compatible issues with the Sencha ExtJS Ext.tree.Panel component with IE, Mozilla Firefox and Google Chrome, but I would probably spend too much time asking those other questions. And I'm not saying IE first to be stereotypical, because all of these browsers have their issues with the Ext.tree.Panel component. I'd rather learn how to fish here, so I can catch my own fish. Once I understand these tree related classes better, I will ask more specific questions.

http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.TreeStore

Custom Ext.data.TreeStore implementation:

Ext.define('MyApp.store.TreeNodes', {
    extend: 'Ext.data.TreeStore',
    xtype: 'store-tree-nodes',
    model : 'MyApp.model.TreeNode',
    proxy: {
        type: 'direct',
        directFn: Tree_Node_CRUD.read,
        reader: {
            root: 'data'
        }
    },
    nodeParam: 'node',
    parentField: 'parentId',
    root: {
        text: 'root',
        id: '0',
        expanded: true
    },
    autoLoad: false,
    single: true,
    listeners: {
        beforeload: function(store, operation, options) {
        },
        append: function( thisNode, newChildNode, index, eOpts ) {
        }    
    }
});

http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.tree.Panel

Custom Ext.tree.Panel implementation:

Ext.define('MyApp.view.MainTree', {
    extend: 'Ext.tree.TreePanel',
    xtype: 'view-main-tree',
    requires: [
        'MyApp.store.TreeNodes'
    ],
    initComponent: function() 
    {        
        this.store = 'TreeNodes';
        this.superclass.initComponent.call(this);
    },
    animate: false,
    title: 'Tree',
    rootVisible: true,
    collapsible: true,   
    dockedItems: [{
        xtype: 'toolbar',
        items: [{
            text: 'Open Node'
        }, {
            text: 'Create Node'
        }, {
            text: 'Delete Node'
        }, {
            text: 'Expand All'
        }, {
            text: 'Collapse All'
        }]
    }], 
    listeners: {
        afterrender: function() {
        },
        itemclick: function(view, node, item, index, e) {
        },
        afteritemexpand: function() {  //node, index, item, eOpts) {
        },
        afteritemcollapse: function() { //node, index, item, eOpts) {
        }
    }
});
like image 938
MacGyver Avatar asked Aug 15 '13 09:08

MacGyver


1 Answers

Overview

There are three ways of augmenting the stock classes behavior in Ext JS 4.x without changing the framework source: subclassing, class overriding, and instance configuration.

Subclassing

Subclassing is what you do when you need to create a custom component tailored for your application. This in fact is what you are doing in the code above: you're taking a stock component, changing its behavior to suit your needs, and using it as a new component. The important point is that by subclassing you are not changing the stock component's behavior so you can use both custom and stock components.

Overriding

Overriding is another approach that will change the behavior of the stock class:

Ext.define('MyApp.tree.TreePanel', {
    override: 'Ext.tree.Panel',

    // Stock fooMethod has a bug, so we are
    // replacing it with a fixed method
    fooMethod: function() {
        ...
    }
});

This way you can apply the changes that are going to affect all instances of the TreePanel, both stock and custom. This approach is mostly used for patches and fixes; it can be used for adding new features to the stock components but you will find it harder to maintain down the road.

Instance configuration

That said, the most popular approach so far is to instantiate the stock classes and change the behavior of the instances by setting config options and overriding methods:

var tree = new Ext.tree.Panel({
    fooConfig: 'bar', // override the default config option

    fooMethod: function() {
        // Nothing wrong with this method in the stock class,
        // we just want it to behave differently
    }
});

This way of doing things was popularized in earlier Ext JS versions and is still heavily used. I do not recommend this approach for new 4.x applications, because it does not allow you to modularize your code properly and is harder to maintain in the long run.

Declarative classes

Another benefit of going the subclassing way is that it allows you to keep your code declarative instead of imperative:

Ext.define('MyApp.view.Panel', {
    extend: 'Ext.panel.Panel',

    store: 'FooStore',

    // Note the difference with your code: 
    // the actual function reference
    // will be resolved from the *object instance*
    // at the object instantiation time
    // and may as well be overridden in subclasses
    // without changing it here
    listeners: {
        itemclick: 'onItemClick'
    },

    initComponent: function() {
        var store = this.store;

        if (!Ext.isObject(store) || !store.isStore) {
            // The store is not initialized yet
            this.store = Ext.StoreManager.lookup(store);
        }

        // You don't need to address the superclass directly here.
        // In the class method scope, callParent will resolve
        // the superclass method and call it.
        this.callParent();
    },

    // Return all items in the store
    getItems: function() {
        return this.store.getRange();
    },

    onItemClick: function() {
        this.doSomething();
    }
});

The above class declaration is shared by all instances of the MyApp.view.Panel, including both the store config option and the initComponent method override, but when you instantiate this class or its subclasses, initComponent method will operate on whatever configuration is current for the particular class.

So when using such class, you will have a choice of either overriding the store config for the instance:

var panel = new MyApp.view.Panel({
    store: 'BarStore'
});

var items = panel.getItems(); // Return all items from BarStore

Or just falling back to the default configuration provided by the class:

var panel = new MyApp.view.Panel();

var items = panel.getItems(); // Return all items from FooStore

You can also subclass it, overriding part of the config or behavior, but not everything:

Ext.define('MyApp.view.NewPanel', {
    extend: 'MyApp.view.Panel',

    // For this Panel, we only want to return first 10 items
    getItems: function() {
        return this.store.getRange(0, 9);
    },

    onItemClick: function() {
        this.doSomethingElse();
    }
});

var panel = new MyApp.view.NewPanel();

var items = panel.getItems(); // Return first 10 items from FooStore

Declarative vs Imperative

Compare that to the imperative approach in which you will have to specify the full configuration for the stock class instance every time:

var panelFoo = new Ext.panel.Panel({
    initComponent: function() {
        this.store = Ext.StoreManager.lookup('FooStore');

        // Note that we can't use this.callParent() here
        this.superclass.initComponent.call(this);
    }
});

var panelBar = new Ext.panel.Panel({
    initComponent: function() {
        this.store = Ext.StoreManager.lookup('BarStore');
        this.superclass.initComponent.call(this);
    }
});

The biggest disadvantage of the code above is that everything happens to the class instance when it is already halfway initialized (initComponent is called by the constructor). You can't generalize this approach, and you can't easily make instances share most of the behavior but differ in details -- you will have to repeat the code for every instance.

Subclassing pitfalls

This brings us to the most common mistake people make with subclassing as well: going just half way. If you take a look at your code above, you may notice this exact mistake:

Ext.define('MyApp.view.MainTree', {
    extend: 'Ext.tree.TreePanel', // You're using subclassing

    initComponent: function() {

        // But here you are assigning the config options
        // to the the *class instance* that has been
        // instantiated and half way initialized already
        this.store = 'TreeNodes';
        ...
    }
});

Compare your code with the declarative example above. The difference is that in your class, configuration happens at instantiation time while in the example it happens at the class declaration time.

Suppose that down the road you will need to reuse your MainTree class elsewhere in the application, but now with a different store, or behavior. With the code above you can't do that easily and you will have to create another class and override the initComponent method:

Ext.define('MyApp.view.AnotherMainTree', {
    extend: 'MyApp.view.MainTree',

    initComponent: function() {
        this.store = 'AnotherTreeNodes';
        ...
    }
});

Compare that to the instance config override above. Not only the declarative approach is easier to write and maintain, but it is infinitely more testable as well.

like image 56
Alex Tokarev Avatar answered Dec 29 '22 00:12

Alex Tokarev