Yesterday, I was helping someone figure out how to add a custom, complex editor for use in a ExtJS Property Grid.

The Anatomy of a Property Grid

For some context, a Property Grid is like an editable grid, but instead of editing rows of data, you use a grid like interface to edit the properties of a single object. Think of it like the interface you’d use when defining database properties in SSMS…

By default, the Property Grid has several editors for simple types like strings, dates, booleans and numbers. Better yet, ExtJS will automatically try to detect the correct type, and specify the appropriate editor for you.

Out of the Box is Never Enough

But of course, out of the box never gets you 100% of the way. Imagine that we have a cluster of properties, one property of which is a complex data type. Consider this example:

source: {
    name: "My Object",
    created: Ext.Date.parse('10/15/2006', 'm/d/Y'),
    timeofday: "12:00 PM",
    available: false,
    version: 0.01,
    description: Ext.encode({
    product: 'Clorox',
    tagline: 'The Shinyest White!'            
    })
}

As you can see, most of the properties are very simple. However, the last one (description) is complex: in this example, it’s a serialized object. If we were heartless, we could just use a regular textfield editor; however, it’s unlikely that the people using the form would really like having to edit a serialized object. So to avoid angry looks from the end-user, a better alternative is to create a new field altogether that will enable us to edit this complex data in a friendlier manner.

A New Field Type

Since our complex data type has two properties (product and tagline…both simple text values), the ideal way to edit this would be by providing a window of some kind that would itself contain a form which would contain textfields for the properties of the object we want to edit. Before you reply that this sounds crazy (a form of fields as a fieldtype?), think about some of the other field types that ExtJS has. For example, when you use a datefield, the actual field component has not only internal awareness of its data type (e.g., it has methods for formatting text input into a date), but also a “picker” which, although technically not a part of a field that is submitted, is nonetheless integral for managing the data that will be input into the datefield component. If datefield can do this, why can’t our custom field? Well, it can, and it will :)

To give away the ending, here’s what we’re ultimately going to build (try editing the “Product Description” field):

View this on JSFiddle

First, let’s create a new field. Since we want to have some of the goodness of the “windowed” functionality that datefield has, we’ll extend Ext.form.field.Picker. By extending this class, we will have some out-of-the-box goodness (like creation of the “window”, management of focus between the cell editor and the picker, etc.) upon which we can build out our custom requirements.

Ext.define('CustomEditorField', {
    extend: 'Ext.form.field.Picker',
    alias: 'widget.customeditorfield',
    editable: false,
    hideTrigger: true,
    pickerOffset: [ 0, -20 ],
    listeners: {
        focus: function( fld, e, opts ) {
            fld.expand();
        }
    }
})

Pretty simple. Beyond extending the picker class, we’re marking the field as NOT editable, hiding the trigger, and setting a focus listener to automatically show the picker when editing begins. Now for the good stuff.

When extending the Ext.form.field.Picker class, you are required to implement your own createPicker() method. It is in this expected method that we can create whatever we need for our custom field. I’ll share the code, and then describe what we’re doing:

Ext.define('CustomEditorField', {
    ...,
    createPicker: function() {
        var me = this,
            format = Ext.String.format;
        // return a component that encapsulates what we want to appear in the picker
        return Ext.create('Ext.form.Panel', {
            title: 'Enter Product Details',
            bodypadding:5,
            pickerField: me,
            ownerCt: me.ownerCt,
            renderTo: document.body,
            floating: true,
            bodyPadding:8,
            items: [
                {
                    xtype: 'textfield',
                    fieldLabel: 'Product',
                    labelAlign: 'top',
                    anchor: '100%',
                    name: 'product'
                },
                {
                    xtype: 'textfield',
                    fieldLabel: 'Tagline',
                    labelAlign: 'top',
                    anchor: '100%',
                    name: 'tagline'
                }                           
            ],
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'bottom',
                    items: [
                        {
                            xtype: 'button',
                            name:'cancel',
                            text:'Cancel',
                            iconCls: 'cancelicon',
                            handler: function( btn, e, opts ) {
                                me.cancelEdit();
                            }                                
                        },
                        '->',
                        {
                            xtype: 'button',
                            name:'save',
                            text:'Save',
                            iconCls: 'accepticon',
                            handler: function( btn, e, opts ) {
                                me.applyValues();
                            }                                
                        }
                    ]                    
                }
            ],
            listeners: {
                afterrender: function( panel, opts ) {
                    panel.getForm().setValues( 
                        Ext.decode( me.getValue() ) 
                    );                      
                }
            }
        })            
    }
});

A lot of lines, but really not much going on here. I’ve simply created a new form component that contains two fields (one for each of the properties I want to edit) and a menu with some buttons to handle saving/canceling. FInally, I added a listener on afterrender to apply the underlying field’s value to the picker’s form fields.  Now, when I edit the property on the Property Grid, a new little window will popup and display the two property-editing fields. Nice!.

To wrap this up, I’ve also added some methods to help with saving/canceling edits made to the property fields in the picker:

Ext.define('CustomEditorField', {
    ...,
    cancelEdit: function() {
        var me = this;
        me.fireEvent( 'blur' );
        me.collapse();       
    },
    applyValues: function() {
        var me = this,
            form = me.picker,
            vals = form.getForm().getValues();    
        // set the value of the editable field        
        me.setValue( Ext.encode( vals ) );
        me.fireEvent( 'blur' );
        me.collapse();        
    },
    createPicker: function() {
        ...
    }
});

Putting it All Together

Awesome, so now we have a new custom, complex field type that we can use in our Property Grid. Let’s now configure the Property Grid to use it:

Ext.create('Ext.grid.property.Grid', {
    title: 'Properties Grid',
    width: 400,
    renderTo: Ext.getBody(),
    source: {
        name: "My Object",
        created: Ext.Date.parse('10/15/2006', 'm/d/Y'),
        timeofday: "12:00 PM",
        available: false,
        version: 0.01,
        description: Ext.encode({
            product: 'Clorox',
            tagline: 'The Shinyest White!'            
        })
    },
    customEditors: {
        timeofday: Ext.create('Ext.form.TimeField', {selectOnFocus: true}),
        description: {
            xtype: 'customeditorfield'  
        }
    },
    customRenderers: {
        description: function( v ) {
            var value = Ext.decode( v ),
                product = value.product,
                tagline = value.tagline,
                description='';
            description += '<b>' + product + '</b>: ';
            description += '<i>' + tagline + '</i>';
            return description;
        },
        timeofday: function( v ) {
            return Ext.isDate( v ) ? Ext.Date.format( v, 'g:i A' ) : v;
        }            
    },
    propertyNames: {
        name: '(name)',
        created: 'Created Date',
        timeofday: 'Time of Day',
        available: 'Available?',
        version: 'Version',
        description: 'Product Description'        
    },
    listeners: {
        beforeedit: function( editor, e, opts ) {
            if( e.record.get( 'name' )=='name' || e.record.get( 'name' )=='version' ) {
                return false;            
            }                
        }            
    }
});

Pretty straightforward. First, in source, I’ve defined all the properties (and their values) that I want to manage in my Property Grid. Next, in customEditors, I’ve defined custom editors to be used by particular fields in the property definition. As you can see, for description I’ve specified that I want to use customeditorfield, the xtype of the new complex field type that we created earlier. Finally, I’ve specified some customRenderers to help make the rendering of the complex data a bit more meaningful.

NOTE: The approach with customRenderers, customEditors, and propertyNames is pre-ExtJS 4.1.3 and is deprecated for future versions. For ExtJS 4.1.3 and on, you would do something more like this:

Ext.create('Ext.grid.property.Grid', {
    ...
    source: {
        ...
        description: Ext.encode({
            product: 'Clorox',
            tagline: 'The Shinyest White!'            
        })
    },
    sourceConfig: {
        ...
        description: {
            renderer: function( v ) {
               ...
            },
            displayName: 'Description',
            editor: Ext.create( 'CustomEditorField', {...} )
        }
    }
});

I like the new approach better, but the deprecated approach works just fine for older versions…

Wrapping Up

So, there we have it. Creating a custom, complex field type is not only possible, but pretty easy too! Now your Property Grids can deal with kind of data you can throw at it. Have fun!

View this on JSFiddle