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
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 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 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.
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.
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
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.
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.
Override the functions which you think are not working correctly or the way you want it to work
Example
Ext.define('MyApp.store.TreeGridStore', {
extend: 'Ext.data.TreeStore',
getTotalCount : function() {
if(!this.proxy.reader.rawData) return 0;
this.totalCount = this.proxy.reader.rawData.recordCount;
return this.totalCount;
},
....
In the above example I want the getTotalCount function to calculate the count differently, so I extended the treestore and overrided the method.