to initComponent() or not to initComponent()
If you do not have a deep understanding of how ExtJS class system work, you may want to follow this:
Declare all non-primitive types in
initComponent()
.
Terminology
- Primitive types - strings, booleans, integers, etc.
- Non-Primitives - arrays & objects.
Explanation
If the component you extend is to be created more than once, any non-primitive configs declared as a config option (outside initComponent
) will be shared between all instances.
Because of this, many people experienced issues when an extended component (typically an extended grid) is created on more than one tab.
This behaviour is explained in sra's answer below and in this Skirtle's Den article. You may also want to read this SO question.
First I will take a stand on my Comments:
@AlexanderTokarev Don't get me wrong. I don't talk about configurations of components, or much worse of instances and moving them to initComponent()
, that is not my point.
Now what I think about this.
initComponent() should resolve anything required the time an instance of this class is created. No more, no less.
You can mess up a load when defining classes and most of it happens because people don't understand how the ExtJS class-system works. As this is about components, the following will focus on those. It will also be a simplified example which should only show a sort of error that I've seen a lot of times.
Let's start: We have a custom panel which does a lot of nifty things. That brings up the need of a custom configuration, we call it foo
. We add it along with our default config option to the class definition so we can access it:
Ext.define('Custom', {
extend: 'Ext.panel.Panel',
alias: 'widget.custpanel',
foo: {
bar: null
},
initComponent: function() {
this.callParent(arguments);
}
});
But things get weird after testing. The values of our configurations seems to change magically. Here's a JSFiddle
What happened is that all created instances are referring to the same foo
instance. But lately I've done that
store: {
fields: [ ... ],
proxy: {
type: 'direct',
directFn: 'Direct.Store.getData'
}
}
with a store and that worked. So why doesn't foo
work?
Most people don't see any difference between this little foo
object and an (ExtJS) config which is basically correct because both are objects (instances). But the difference is that all classes shipped by sencha know perfectly well which configuration properties they expect and take care of them.
For example the store property of a grid is resolved by the StoreManager
and can therefore be:
-
storeId
string, or - store instance or
- store configuration object.
During the initialization of the grid either of these get resolved and overwritten by an actual store instance. A store is just one example. I guess the more known one is the items array. This is an array at definition time and it gets overridden for each instance with a MixedCollection (if I am not mistaken).
Yes there is a difference between a class definition and the instance created from it. But we need to take care of any new property which contains a reference like the foo
from above and that is not that complicated. Here is what we need to do to fix it for the foo
example
Ext.define('Custom', {
extend: 'Ext.panel.Panel',
alias: 'widget.custpanel',
foo: {
bar: null
},
initComponent: function() {
this.foo = Ext.apply({}, this.foo);
this.callParent(arguments);
}
});
Here's the JSFiddle
Now we take care of the foo
config when an instance get created. Now this foo
example is simplified and it will not always be that easy to resolve a configuration.
Conclusion
Always write your class definition as configurations! They must not contain any referred instances except for plain configuration and must take care of these to resolve them when a instance get created.
Disclaimer
I do not claim to cover all with this really short writing!
I usually advocate for having as much configuration as possible in class config options, because it reads better and is easier to override in subclasses. Besides that, there is a strong possibility that in future Sencha Cmd will have optimizing compiler so if you keep your code declarative, it could benefit from optimizations.
Compare:
Ext.define('MyPanel', {
extend: 'Ext.grid.Panel',
initComponent: function() {
this.callParent();
this.store = new Ext.data.Store({
fields: [ ... ],
proxy: {
type: 'direct',
directFn: Direct.Store.getData
}
});
this.foo = 'bar';
}
});
...
var panel = new MyPanel();
And:
Ext.define('MyPanel', {
extend: 'Ext.grid.Panel',
alias: 'widget.mypanel',
foo: 'bar',
store: {
fields: [ ... ],
proxy: {
type: 'direct',
directFn: 'Direct.Store.getData'
}
}
});
...
var panel = Ext.widget({
xtype: 'mypanel',
foo: 'baz'
});
Note how these approaches are very different. In the first example, we're hardcoding a lot: object property values, store configuration, MyPanel class name when it's used; we're practically killing the idea of a class because it becomes inextensible. In the second example, we're creating a template that can be reused many times with possibly different configuration - basically, that's what the whole class system is about.
However, the actual difference lies deeper. In the first case, we're effectively deferring class configuration until runtime, whereas in the second case we're defining class configuration and applying it at very distinctively different phases. In fact, we can easily say that the second approach introduces something JavaScript lacks natively: compile time phase. And it gives us a plethora of possibilities that are exploited in the framework code itself; if you want some examples, take a look at Ext.app.Controller
and Ext.app.Application
in latest 4.2 beta.
From more practical perspective, the second approach is better because it's easier to read and deal with. Once you grasp the idea, you will find yourself writing all your code like that, because it's just easier this way.
Look at it this way: if you would write an old style Web application, generating HTML and stuff on the server side, you would try not to have any HTML mixed with the code, would you? Templates to the left, code to the right. That's practically the same as hardcoding data in initComponent
: sure it works, up to a point. Then it becomes a bowl of spaghetti, hard to maintain and extend. Oh, and testing all that! Yuck.
Now, there are times when you need to do something with an instance at runtime, as opposed to a class definition time - the classical example is applying event listeners, or calling control
in Controllers. You will have to take actual function references from the object instance, and you have to do that in initComponent
or init
. However, we're working on easing this problem - there should be no hard requirement to hardcode all this; Observable.on()
already supports string listener names and MVC stuff will too, shortly.
As I said in the comments above, I'll have to write an article or guide for the docs, explaining things. That would probably have to wait until 4.2 is released; meanwhile this answer should shed some light on the matter, hopefully.