Factory Functions in Ext Extensions (Abstract Classes)

7. August 2010 – 15:37

I have recently run across one of the the Jay Garcia’s excellent screencasts Abstract classes with Ext JS. (Thank you Jay for your effort of educating Ext JS community.)

I’ve been consulting in a company at that time – developers of the client, after seeing the screencast, immediately asked me: “So what should we use? The ‘xtype’ style or Abstract classes?”

The curt answer would be: “Use whatever you prefer.” or “Use both”.

However, decisions based on mere “preference” or on “I use it because it is available” are often not fully rational and can lead to troubles as the application grows. Thus, let’s take a deeper look to see if there are any pitfalls and to find how can we get most of both methods.

Note: Before you continue, read “Writing a big application in Ext”, watch the Jay’s screencast and understand both, otherwise, the following text won’t make any sense to you.

Abstract ExtJS Class

Abstract classes are not to be used directly but they serve more like templates that specify methods and properties to be implemented in classes derived from them. They are very useful in object oriented programming because they force developers to implement all mandatory methods with same signature (signature = arguments the method or function accepts) and return values so there is a greater chance to develop a bug free code.

However, in ExtJS (and JavaScript that is scripting language where source is not compiled but directly interpreted), there is no mechanism to warn developer “you haven’t implemented method XY” or “your implementation has wrong signature” so we need to take care ourselves.

Nevertheless, the idea of abstract classes is useful also in JavaScript/ExtJS environment because:

  1. logic and configuration common to all expected descendants can be put in the abstract base class that eliminates the code duplication, speeds up development and debugging, and increases future code maintainability greatly
  2. abstract classes tell us which methods to implement
  3. it increases the human readability of the code (Remember, we always try to write the code to be readable and understandable by us and others in the future.)

The Basic Idea

The basic idea Jay presents in his great screencast is to extend an Ext Component, FormPanel for example, and add stub methods to it:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     submitUrl:null
    ,initComponent:function() {
        Ext.apply(this, {
             items:this.buildItems()
            ,buttons:this.buildButtons()
        });
 
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildItems:function() {
        return [];
    } // eo function buildItems
 
    ,buildButtons:function() {
        return [];
    } // eo function buildButtons
 
}); // eo extend

The empty “buildXxx” methods will be implemented in the abstract class extension.

Tweaking the Abstract Class example

While perfect for the educational purposes, we need to tweak this code if want to use it in the production quality application. I shall walk you through recommended improvements.

initialConfig

Ext.Component takes the config object passed to its constructor and saves it in the object variable initialConfig when it instantiates. Although items and buttons in combination with the FormPanel in the above example do not rely on it, so the example runs as expected, initialConfig is used on the Ext.Component level so it may be used by any of Component descendants.

Let’s take care of it and modify initComponent as follows:

    ,initComponent:function() {
        // create config object
        var config = {};
 
        // build config properties
        Ext.apply(config, {
             items:this.buildItems()
            ,buttons:this.buildButtons()
        });
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent

Return Values

Abstract methods in the example return empty arrays. That is no problem for items or buttons but if we want to have also buildTbar or buildBbar factory functions that return empty arrays [] then top and bottom toolbars are always created, but empty, if methods are not implemented in derived classes. (Empty toolbars are quite ugly when rendered.)

Therefore, always return undefined from the abstract methods. For now, the class could look like this:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     submitUrl:null
    ,initComponent:function() {
        // create config object
        var config = {};
 
        // build config properties
        Ext.apply(config, {
             items:this.buildItems()
            ,buttons:this.buildButtons()
            ,tbar:this.buildTbar()
            ,bbar:this.buildBbar()
        });
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildItems:function() {
        return undefined;
    } // eo function buildItems
 
    ,buildButtons:function() {
        return undefined;
    } // eo function buildButtons
 
    ,buildTbar:function() {
        return undefined;
    } // eo function buildTbar
 
    ,buildBbar:function() {
        return undefined;
    } // eo function buildBbar

Passing config to Abstract Factory Methods

If we pass config object to abstract methods then we can apply the created items, buttons, toolbar items directly to it. That has no great benefit in itself, however, if we create a sequence or interceptor of these methods we can use the config object directly.

So now we have:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     submitUrl:null
    ,initComponent:function() {
        // create config object
        var config = {};
 
        // build config properties
        this.buildItems(config);
        this.buildButtons(config);
        this.buildTbar(config);
        this.buildBbar(config);
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildItems:function(config) {
        config.items = undefined;
    } // eo function buildItems
 
    ,buildButtons:function(config) {
        config.buttons = undefined;
    } // eo function buildButtons
 
    ,buildTbar:function(config) {
        config.tbar = undefined;
    } // eo function buildTbar
 
    ,buildBbar:function(config) {
        config.bbar = undefined;
    } // eo function buildBbar
 
}); // eo extend

The Final Touch

If you look in the initComponent now you see that we call a series of “build” functions in it. Let’s move it into another factory method:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     submitUrl:null
    ,initComponent:function() {
        // create config object
        var config = {};
 
        // build config
        this.buildConfig(config);
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildConfig:function(config) {
        this.buildItems(config);
        this.buildButtons(config);
        this.buildTbar(config);
        this.buildBbar(config);
    } // eo function buildConfig
 
    ,buildItems:function(config) {
        config.items = undefined;
    } // eo function buildItems
 
    ,buildButtons:function(config) {
        config.buttons = undefined;
    } // eo function buildButtons
 
    ,buildTbar:function(config) {
        config.tbar = undefined;
    } // eo function buildTbar
 
    ,buildBbar:function(config) {
        config.bbar = undefined;
    } // eo function buildBbar
 
}); // eo extend

The above gives you the flexibility of implementing or overriding of the whole buildConfig function or individual items, buttons, bars functions. It also takes care of initialConfig problem.

It’s a kind of a “pattern” and I will publish it in my “file patterns” series on this blog with more code comments.

Example

If we build upon Jay’s example, using the pattern above, we get:

AbstractFormPanel.js:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     defaultType:'textfield'
    ,frame:true
    ,width:300
    ,height:200
    ,labelWidth:75
    ,submitUrl:null
    ,submitT:'Submit'
    ,cancelT:'Cancel'
    ,initComponent:function() {
 
        // create config object
        var config = {
            defaults:{anchor:'-10'}
        };
 
        // build config
        this.buildConfig(config);
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildConfig:function(config) {
        this.buildItems(config);
        this.buildButtons(config);
        this.buildTbar(config);
        this.buildBbar(config);
    } // eo function buildConfig
 
    ,buildItems:function(config) {
        config.items = undefined;
    } // eo function buildItems
 
    ,buildButtons:function(config) {
        config.buttons = [{
             text:this.submitT
            ,scope:this
            ,handler:this.onSubmit
            ,iconCls:'icon-disk'
        },{
             text:this.cancelT
            ,scope:this
            ,handler:this.onCancel
            ,iconCls:'icon-undo'
        }];
    } // eo function buildButtons
 
    ,buildTbar:function(config) {
        config.tbar = undefined;
    } // eo function buildTbar
 
    ,buildBbar:function(config) {
        config.bbar = undefined;
    } // eo function buildBbar
 
    ,onSubmit:function() {
        Ext.MessageBox.alert('Submit', this.submitUrl);
    } // eo function onSubmit
 
    ,onCancel:function() {
        this.el.mask('This form is canceled');
    } // eo function onCancel
 
}); // eo extend

Mention please that I moved hard-wired button texts to class properties. The reason is that this way we can easily localize (translate) these texts to another languages. I didn’t take care of messages of handlers though. You will do in your localizable applications, won’t you?

AddressFormPanel.js:

Ext.ns('MyApp');
 
MyApp.AddressFormPanel = Ext.extend(MyApp.AbstractFormPanel, {
     title:'Edit address data'
    ,submitUrl:'addressAction.asp'
    ,buildItems:function(config) {
        config.items = [{
             name:'address1'
            ,fieldLabel:'Address 1'
        },{
             name:'address2'
            ,fieldLabel:'Address 2'
        },{
             name:'city'
            ,fieldLabel:'city'
        },{
             xtype:'combo'
            ,name:'state'
            ,fieldLabel:'State'
            ,store:['MD', 'VA', 'DC']
        },{
             xtype:'numberfield'
            ,name:'zip'
            ,fieldLabel:'Zip Code'
        }];
    } // eo function buildItems
 
});
 
// eof

NameFormPanel.js:

Ext.ns('MyApp');
 
MyApp.NameFormPanel = Ext.extend(MyApp.AbstractFormPanel, {
     title:'Edit name data'
    ,submitUrl:'nameAction.asp'
    ,okT:'OK'
 
    ,buildItems:function(config) {
        config.items = [{
             name:'firstName'
            ,fieldLabel:'First Name'
        },{
             name:'lastName'
            ,fieldLabel:'Last Name'
        },{
             name:'middleName'
            ,fieldLabel:'Middle Name'
        },{
             xtype:'datefield'
            ,name:'dob'
            ,fieldLabel:'DOB'
        }];
    } // eo function buildItems
 
    //Extension
    ,buildButtons:function(config) {
 
        // let parent build buttons first
        MyApp.NameFormPanel.superclass.buildButtons.apply(this, arguments);
 
        // tweak the submit button
        config.buttons[0].text = this.okT;
        config.buttons[0].handler = this.onOkBtn;
 
    } // eo function buildButtons
 
    //Override
    ,onOkBtn:function() {
        console.info('OK btn pressed');
    } // eo function onOkBtn
 
}); // eo extend
 
// eof

With this setup, we can even create instance of AbstractForm panel directly passing some/all build functions inline:

var nameForm = new MyApp.AbstractFormPanel({
     title:'Name Form Panel configured inline'
    ,width:300
    ,height:200
    ,renderTo:Ext.getBody()
    ,buildItems:function(config) {
        config.items = [{
             name:'firstName'
            ,fieldLabel:'First Name'
        },{
             name:'lastName'
            ,fieldLabel:'Last Name'
        },{
             name:'middleName'
            ,fieldLabel:'Middle Name'
        },{
             xtype:'datefield'
            ,name:'dob'
            ,fieldLabel:'DOB'
        }];
    } // eo function buildItems
});

Note: Although the above works, it violates the rule of not instantiating an abstract class directly. It’s up to you if you will do it or not.

Combining with “xtypes”

Now, do not succumb to the temptation of “putting everything” in factory functions. Abstract classes are just a programming style, not an universal solvent.

Imagine, you have your own carefully crafted Submit and Cancel buttons so if you would “put everything” in the factory functions you would need to copy buttons configurations many times because also toolbars, grids, data views, etc can contain them.

Create extensions (xtypes) for those buttons instead. The following illustrates the approach:

SubmitButton.js

Ext.ns('MyApp');
 
MyApp.SubmitButton = Ext.extend(Ext.Button, {
     text:'Submit'
    ,iconCls:'icon-disk'
    ,initComponent:function() {
        MyApp.SubmitButton.superclass.initComponent.apply(this, arguments);
    } // eo function initComponent
}); // eo extend
 
Ext.reg('submitbutton', MyApp.SubmitButton);
 
// eof

CancelButton.js:

Ext.ns('MyApp');
 
MyApp.CancelButton = Ext.extend(Ext.Button, {
     text:'Cancel'
    ,iconCls:'icon-undo'
    ,initComponent:function() {
        MyApp.CancelButton.superclass.initComponent.apply(this, arguments);
    } // eo function initComponent
}); // eo extend
 
Ext.reg('cancelbutton', MyApp.CancelButton);
 
// eof

and AbstractFormPanel.js:

Ext.ns('MyApp');
 
MyApp.AbstractFormPanel = Ext.extend(Ext.form.FormPanel, {
     defaultType:'textfield'
    ,frame:true
    ,width:300
    ,height:200
    ,labelWidth:75
    ,submitUrl:null
    ,initComponent:function() {
 
        // create config object
        var config = {
            defaults:{anchor:'-10'}
        };
 
        // build config
        this.buildConfig(config);
 
        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
 
        // call parent
        MyApp.AbstractFormPanel.superclass.initComponent.call(this);
 
    } // eo function initComponent
 
    ,buildConfig:function(config) {
        this.buildItems(config);
        this.buildButtons(config);
        this.buildTbar(config);
        this.buildBbar(config);
    } // eo function buildConfig
 
    ,buildItems:function(config) {
        config.items = undefined;
    } // eo function buildItems
 
    ,buildButtons:function(config) {
        config.buttons = [{
             xtype:'submitbutton'
            ,scope:this
            ,handler:this.onSubmit
        },{
             xtype:'cancelbutton'
            ,scope:this
            ,handler:this.onCancel
        }];
    } // eo function buildButtons
 
    ,buildTbar:function(config) {
        config.tbar = undefined;
    } // eo function buildTbar
 
    ,buildBbar:function(config) {
        config.bbar = undefined;
    } // eo function buildBbar
 
    ,onSubmit:function() {
        Ext.MessageBox.alert('Submit', this.submitUrl);
    } // eo function onSubmit
 
    ,onCancel:function() {
        this.el.mask('This form is canceled');
    } // eo function onCancel
 
}); // eo extend
 
// eof

Note: The above buttons are far too simple to be extended in the real world but you get the point, right?

Conclusion

Abstract classes, as Jay Garcia defines and presents them, are a very useful ExtJS programming technique and if you use them in the information provided in this post your code will be clean, flexible and maintainable. That is what all we developers want.

Happy coding!

Further reading:

  1. Writing a Big Application in Ext
  2. Abstract classes with Ext JS

StumbleUpon Toolbar
  1. 16 Responses to “Factory Functions in Ext Extensions (Abstract Classes)”

  2. About initialConfig:

    I have written ExtJS for quite some time now and have never applied configuration in initComponent additionally to initialConfig.

    Could you provide concrete examples of what could go wrong if you ignore initialConfig?

    About Passing config to Abstract Factory Methods:

    I don\’t buy the createSequence() argument for several reasons…

    1. I personally only ever use createSequence() when I need to add behaviour into third-party code that I don\’t want to modify. So for classes meant to be used only inside one project, I don\’t see a reason to cater for createSequence().

    2. I always prefer code that doesn\’t modify state over the one that does. The latter one tends to be lot more harder to understand and debug. I don\’t see a reason to sacrificing all the simplicity for some extensibility that I\’m not sure is even needed.

    3. When createSequence() doesn\’t handle return values well, then instead of catering for createSequence(), why not use something that can handle return values. For example:

    var oldBuildItems = form.buildItems;
    form.buildItems = function() {
    var items = oldBuildItems.call(this);
    items.push({name:\’city\’,fieldLabel:\’city\’});
    return items;
    }

    PS. Thanks for thought-provoking post.

    By Rene Saarsoo on Aug 16, 2010

  3. initialConfig caused problems with border layout with some earlier versions of 3.x series of Ext. You see, I want to write the code, especially when touching private methods and properties, that can survive Ext upgrades.

    If the initialConfig is used/saved in the Component class, I just consider it a good programming practice to take care of it in extensions of derived classes, regardless of existence of an actual problem at present. In other words, it is much safer to save it.

    1. That’s your way. I often use sequences and interceptors when I do not want to touch my own code that is already debugged.

    2. That’s also your way. Name of methods clearly say that they are “building” something and it is the config object that is being built. The “state” of the config object is changed also when you assign a new property to it elsewhere. Debugging is also a breeze as you can console.log(config); after each statement in buildConfig method.

    Anyway, everybody chooses his own style. If your way works for you, just stick on it.

    BTW, could you post a public link to any of Ext applications you have written?

    By Saki on Aug 17, 2010

  4. Excellent post. I’m fairly new at ExtJS, and this is by far the best pattern I’ve seen. I’m in the middle of building a “big” ExtJS application, and I can already see the benefits of this pattern (Abs Cls w/config init + xtype) as we have many forms which need to be extended for different areas of the app.

    Will post back for feedback to see how this pattern works for us. Thanks.

    By James on Aug 27, 2010

  5. Thank you James, looking forward to your feedback.

    By Saki on Aug 31, 2010

  6. Thanks Saki! But I have one question that how to Ext Designer apply this? I see that generated code by Ext Designer use the same same this method.

    By rualatngua on Sep 27, 2010

  7. No idea, I don’t use designer.

    By Saki on Sep 29, 2010

  8. Nice pattern. Tried it out yesterday with a AbstractToolbar class (for Sencha Touch).
    Added buildLeftButtons, buildCenterButtons and buildRightButtons.

    Though, instead of passing the config to every method and applying it at the end to the component, I directly pass the definition to the component by using this.items, this.buttons, etc.

    initComponent: function() {
      this.buildItems();
      this.buildButtons();
      this.buildTbar();
      this.buildBbar();
      MyApp.AbstractFormPanel.superclass.initComponent.call(this);
    },
    buildButtons:function() {
      this.buttons = [{
        xtype: \'submitbutton\',
        scope: this,
        handler: this.onSubmit
      },{
        xtype: \'cancelbutton\',
        scope: this,
        handler: this.onCancel
      }];
    }
    

    By Steffen Hiller on Nov 30, 2010

  9. I used the proposed patern to create a panel extension that hold an Ext StackedBarChart. When I added the extended panel to the center region of a panel having a boder layout I am getting the error message \’No center region defined in BorderLayout ext-comp\’ even when I have the region set through a config object. I had to hard code the region in the extended class to get ride of the error… I am new to ext and need your help to find a better way to set the config object.
    Many thanks

    By Ibra Partage on Jan 1, 2011

  10. Would it make sense to add buildEvents(config) and buildListeners(config) to the abstract?

    buildEvents : function (config) {
    config.events = undefined;
    }
    buildListeners : function (config) {
    config.listeners = undefined;
    }

    By btlife on Jan 17, 2011

  11. Add Events has to go between these two lines right?
    // apply config
    Ext.apply(this, Ext.apply(this.initialConfig, config));

    // call parent
    MyApp.AbstractFormPanel.superclass.initComponent.call(this);

    so it would be

    // apply config
    Ext.apply(this, Ext.apply(this.initialConfig, config));

    this.buildEvents(config);
    this.buildListeners(config);

    // call parent
    MyApp.AbstractFormPanel.superclass.initComponent.call(this);

    By btlife on Jan 17, 2011

  12. otherwise you would have to provide the initComponent function in order to use the AbstractFormPanel with events? Which I believe is one of the things trying to be abstracted with this. Is this right? or am I off base on this?

    By btlife on Jan 17, 2011

  13. How will Ext JS 4.0 affect the way the Abstract Classes are done?

    By JC on Feb 26, 2011

  14. how i Creating a master-details view with
    two grids such that one-to-many relationships with two grids?plz reply saki…..

    By rp111 on May 7, 2011

  15. Take a look at http://examples.extjs.eu

    By Saki on May 9, 2011

  16. Great, it worked, I have 3 charts that are quite the same, but different stores so I have now in my code a parent class and 3 instances, instead of 3 classes with the same configuration.

    By diana on Dec 5, 2011

  1. 1 Trackback(s)

  2. Dec 28, 2011: Abstract class pattern extended by Saki | Modus Create

Post a Comment