views:

549

answers:

2

We are using ExtJS for a webapplication. In that application, we use the standard Ext.form.ComboBox control when a simple dropdown is required, and the Ext.us.Andrie.Select control when we need a dropdown where you can select multiple values and/or clear the value. Creating either of these always requires a bunch of boilerplate code for the config options, so I wanted a class that reduced boilerplate code and while I was at it, I wanted this class to be able to produce either the simple dropdown, or the more advanced one, depending on a config option (multi: true or clearable: true), but this turned out to be a lot harder than expected.

This is the closest I came to a working result:

MyComboBox = (function() {

    var singleDefaults = {
        typeAhead: false,
        triggerAction: 'all',
        selectOnFocus: false,
        allowBlank: true,
        editable: false,
        delay: 700
    };

    var multiDefaults = {
        typeAhead: false,
        triggerAction: 'all',
        selectOnFocus: false,
        allowBlank: true,
        editable: false,
        delay: 700
    };

    var constructor = function(config) {

            if (config.multi || config.clearable) {
                config = Ext.apply(this, config, multiDefaults);
                Ext.apply(this, Ext.ux.Andrie.Select.prototype);
                Ext.apply(this, Ext.ux.Andrie.Select(config));
                Ext.ux.Andrie.Select.prototype.constructor.call(this, config);
            } else {
                config = Ext.apply(this, config, singleDefaults);
                Ext.apply(this, Ext.form.ComboBox.prototype);
                Ext.apply(this, Ext.form.ComboBox(config));
                Ext.form.ComboBox.prototype.constructor.call(this, config);
            }
    };

    return function(config) {
        this.constructor = constructor;
        this.constructor(config);
    };

})();

Well, it doesn't crash, but it doesn't really work either. When set to behave like Ext.ux.Andrie.Select, it wants to load the store even when it's loaded, doesn't expand the dropdown unless you start typing in the field.

Another approach that was tried was something like:

MyComboBox = Ext.extend(Ext.form.ComboBox, {
 constructor: function(config){
   if (config.multi || config.clearable) {
     Ext.form.ComboBox.prototype.constructor.call(this, config);
   } else {
     Ext.ux.Andrie.Select.prototype.constructor.call(this, config);
   }
 }
});

That doesn't work because the Andrie dropdown doesn't define a constructor function of its own so it ends up calling the constructor function of Ext.form.ComboBox, which it inherits from, which results in a normal dropdown, not the multiselect dropdown.

I suppose this is ExtJS specific, but if you have a framework agnostic approach to doing this, I can probably translate it to ExtJS.

+2  A: 

When building medium to large scale JavaScript applications, I find it convenient to move all control creation/instantiation into a single class were each method is a factory for that control (i.e.: combo box, text area, buttons, tool tips, check boxes, etc). Since only your factory class has the knowledge of how to create controls, this has the added benefit of decoupling your view logic from control instantiation themselves (that is, of course, as long as the control constructors and interfaces stay the same).

WidgetFactory = {
    comboBox: function(config) {
        return config.multi || config.clearable
            ? this.comboBoxMultiple(config)
            : this.comboBoxSingle(config)
    },

    comboBoxSingle: function(config) {
        // ... boiler plate goes here ...
    },

    comboBoxMultiple: function(config) {
        // ... boiler plate goes here ...
    },

    textArea    : function(config) {},
    textBox     : function(config) {},
    checkbox    : function(config) {},
    radioButton : function(config) {},
    button      : function(config) {},
    slider      : function(config) {},
    colorPicker : function(config) {}

    // etc
};

Decoupling control creation from the view allows you quickly and easily swap out on implementation of a control for another without having to hunt through the whole of your application to find/replace all instances of that control's creation boilerplate.


Edit:
I didn't mean to imply that this would replace the use of objects for creating/instantiating controls, but rather, to be used in addition to. Using the WidgetFactory as a single gateway to all controls allows more flexibility than just being able to change super classes. You can swap out the entire class without altering the original class, allowing you to have multiple, different implementations if necessary.

For example, if your single combo box is defined by some class, namespace.ui.combobox.single, then you can do

comboBoxSingle: function(config) {
    return new namespace.ui.combobox.single(config);
},

However, if you need to suddenly use a different class, for testing perhaps, you can change it once in the factory

comboBoxSingle: function(config) {
    return new namespace.ui.combobox.single2(config);
},

and easily switch back and forth without having to alter the actual widget classes.

Justin Johnson
Whilst certainly a valid and good suggestion, creating classes also reduces coupling. I could change the implementation of MyComboBox at any point, to inherit from `Ext.form.TextField` for example (wrong, but entirely possible). The only real difference is semantics, I use `new` and you use a factory method.
JulianR
@JulianR See my edit.
Justin Johnson
A: 

Almost forgot about this question. I solved this and if anyone is wondering, here's how to do it:

var constructor = function(config) {
    if (config && (config.multi || config.clearable)) {
        config = Ext.apply(this, config, multiDefaults);
        Ext.applyIf(this, Ext.ux.Andrie.Select.prototype);
        Ext.ux.Andrie.Select.createDelegate(this)(config);
    } else {
        config = Ext.apply(this, config, singleDefaults);
        Ext.applyIf(this, Ext.form.ComboBox.prototype);
        Ext.form.ComboBox.createDelegate(this)(config);
    }
};
JulianR