We started using Ext JS about 3 years ago. Back then, we were new to Ext JS, and tasked with developing the next generation of components to revamp our UI. We were using Ext JS 2, and wanted infinite flexibility and customization in the components we built. This need, along with the desire to change some of the core Ext JS functionality led us to do some ugly things. We used private methods as extension points, straight copy-n-pasted methods to change one line, and introduced a slew of new config options. While the resulting components fit the bill, we were left with a maintenance and upgrade nightmare. Fast forward a few years and the nightmare got worse – Ext JS 3 was out, but we couldn’t separate the bug fixes from the behavior changes from the newly created features and options. So what did we do? We decided to start over (for the most part), with Ext JS 4, a clean slate, and a new approach to writing good maintainable JavaScript code.

We want to share some of our hard-learned lessons with you so hopefully you’ll avoid the same mistakes. In this post, we will discuss an approach to adding additional behavior to Ext JS that follows good Object Oriented (OO) design principles and leads to code that is more maintainable and much easier to upgrade when the next shiny new version of Ext JS is released. So lets get to it.

The Problem
Imagine you want to enhance the Ext.form.field.ComboBox to allow the dropdown list (an Ext.view.BoundList) to automatically size itself to it’s contents width (a feature that Ext JS should add to the ComboBox IMHO). Let’s call this enhancement the auto-sizing-feature. You might start out by creating a new subclass of ComboBox and adding the additional behavior:

Ext.define('Rally.field.ComboBox', {
    extend : 'Ext.form.field.ComboBox',

    defaultOffset : [0,-1],
    defaultAlign : 'tl-bl?',

    onExpand : function() {
        this.callParent(arguments);
        this.getPicker().on('viewready', this._sizeView, this);
    },

    _sizeView : function() {
        // code to customize BoundList dropdown
    },

    // helper methods used by _sizeView()
    _getMaxWidth : function() { },
    _reSizeBoundList : function() { },
    _reAlignBoundList : function() { }
});

While this will work, it sets us up for some potential issues. Imagine another developer opening up the Rally.field.ComboBox a few months down the road to add some additional functionality specified by another-really-important-feature. Lets say they need to do some size and position calculations. They might use one of the pseudo-private helper methods we’ve added to do this. Or they might see the defaultAlign property and decide to use it. So we’d then have something like this:

Ext.define('Rally.field.ComboBox', {
    ...
    defaultAlign : 'tl-bl?',
    ...
    // helper methods used by _sizeView()
    _getMaxWidth : function() {
        // original code to support auto-sizing-feature here
        this._lastCalcMaxWidth = maxWidth;  // added to support another-really-important-feature
        // more of the orignal code
    },
    _replaceWithShimWhileLoading : function(offset) {
        if (offset === this.defaultAlign) {
            // do stuff
        }
    }
});

Now let’s complicate the situation – a few months after another-really-important-feature was added, Ext JS 4.1 comes out and to our surprise, it provides the ability to auto-size the dropdown based on it’s contents width. So we want to remove the auto-sizing-feature, but we still need another-really-important-feature to work. Hmm, gets a little tricky.

A Better Approach
A better approach to prevent this situation is to use one of the great features provided by Ext JS – plugins. Plugins are by no means a silver bullet, but they do provide a more elegant way to encapsulate new functionality and promote better separation in your code.

Let’s see how we could have implemented auto-sizing-feature as a plugin:

Ext.define('Rally.field.plugin.BoundListAutoSizingPlugin', {
    extend : 'Ext.AbstractPlugin',
    alias : 'plugin.boundListAutoSizingPlugin',

    defaultOffset : [0,-1],
    defaultAlign : 'tl-bl?',

    init : function(comboBox) {
        this.comboBox = comboBox;
        this.comboBox.on('expand', this._onExpand, this);
    },

    destroy : function(comboBox) {
        this.comboBox.un('expand', this._onExpand, this);
        if (picker) {
            picker.un('viewready', this._sizeView, this);
        }
    },

    _onExpand : function() {
        var picker = this.comboBox.getPicker();
        if (picker) {
            picker.on('viewready', this._sizeView, this);
        }
    },

    _sizeView : function() {
        // code to customize BoundList dropdown
    },

    // helper methods used by _sizeView()
    _getMaxWidth : function() { },
    _reSizeBoundList : function() { },
    _reAlignBoundList : function() { }
});

Ext.define('Rally.field.ComboBox', {
    extend : 'Ext.form.field.ComboBox',
    plugins : [{
        ptype : 'boundListAutoSizingPlugin'
    }]
});

As you can see, our Rally.field.ComboBox is now much cleaner, and includes the boundListAutoSizingPlugin. The Rally.field.plugin.BoundListAutoSizingPlugin encapsulates all the code needed to auto-size the dropdown. Now, when Ext JS 4.1 comes out and includes this behavior by default, all we need to do is delete the plugin code and remove it from the list of plugins.

This will also make it much more unlikely that code for another-really-important-feature will be intermingled with the implementation of auto-sizing-feature. But what about code duplication? You might argue the reason that _getMaxWidth was leveraged later was because it already implemented the functionality needed. If you find there is common logic, calculations, or code needed by multiple plugins or other collaborating objects, refactor that code into a public method on Rally.field.ComboBox, and let the plugins or other collaborating object go through that method.

Conclusion
As we mentioned, plugins are not the ideal solution to every problem, but it is a very valuable tool as you being to write more Ext JS-based JavaScript. Look for ways to leverage it to help produce clean, well separated objects and interactions.