Ext JS 4 Fundamentals: Editing Database Tables with the Grid

Every app has “lookup” tables – those pesky little things that associate labels with numeric, autoincrementing primary keys. And those tables typically need to be editable by system administrators in order to maintain the flexibility of the system of which they are a part.

Using the following template-based approach. to quickly build these interfaces by leveraging the Ext JS 4 grid control as illustrated below:

Image

 

The first step in building this GUI is to define a data model and proxy. The model specifies the names of the fields that will be available as a JSON or XML feed. The proxy specifies the url from where the data feed will be generated. Most lookup table models will only specify two or three fields. Ext JS 4 considers the field named “id” to contain the primary key of the record. At Fig Leaf Software, we prefer to use a RESTful data services implementation. Ext automatically transmits a GET/POST/PUT/DELETE request header based on the type of operations that we’re going to perform, which can be a big time saver. All application servers (including Adobe ColdFusion) support the creation of REST-based services.

Ext.define('MyApp.model.PracticeCategory', {
    extend: 'Ext.data.Model',

    requires: [
        'Ext.data.Field',
        'Ext.data.proxy.Rest'
    ],

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'label',
            type: 'string'
        },
        {
            name: 'isActive',
            type: 'boolean'
        }
    ],

    proxy: {
        type: 'rest',
        url: 'http://[some domain]/[some app name]/category'
    }
});

The next step is to define a data Store which will hold multiple data model instances/records. This part is usually pretty boilerplate since most lookup tables typically contain fewer than 100 records. If you’re expecting more data points, you might want to look at setting the remoteFilter and remoteSort properties, as well as implementing data pagination.

Ext.define('MyApp.store.PracticeCategories', {
    extend: 'Ext.data.Store',

    requires: [
        'MyApp.model.PracticeCategory'
    ],

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            autoLoad: true,
            model: 'MyApp.model.PracticeCategory',
        }, cfg)]);
    }
});

Now that the data package plumbing has been completed, we’re ready to generate the editing GUI. In order to compartmentalize everything in a nice, neat package, I’ve opted to merge the event handling with the view. Some might argue that they’d prefer putting the event handling into a view controller, but for this specific limited use-case, I find that punching everything into a single file is slightly easier to maintain. And since it’s my code, it’s my rules.

Ext.define('MyApp.view.admin.PracticeCategoryEditor', {
    extend: 'Ext.window.Window',
    alias: 'widget.practicecategoryeditor',

    requires: [
        'Ext.grid.Panel',
        'Ext.form.field.Text',
        'Ext.grid.column.CheckColumn',
        'Ext.form.field.Checkbox',
        'Ext.grid.View',
        'Ext.grid.plugin.RowEditing',
        'Ext.panel.Tool'
    ],

    autoShow: true,
    height: 250,
    width: 400,
    layout: 'fit',
    title: 'Practice Categories',

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            items: [
                {
                    xtype: 'gridpanel',
                    header: false,
                    store: 'PracticeCategories',
                    columns: [
                        {
                            xtype: 'gridcolumn',
                            dataIndex: 'label',
                            text: 'Practice Category',
                            flex: 1,
                            editor: {
                                xtype: 'textfield',
                                allowBlank: false
                            }
                        },
                        {
                            xtype: 'checkcolumn',
                            width: 80,
                            dataIndex: 'isActive',
                            text: 'Active',
                            editor: {
                                xtype: 'checkboxfield',
                                inputValue: 'true',
                                uncheckedValue: 'false',
                                listeners: {
                                    change: {
                                        fn: me.onCheckboxfieldChange,
                                        scope: me
                                    }
                                }
                            },
                            listeners: {
                                beforecheckchange: {
                                    fn: me.onCheckcolumnBeforeCheckChange,
                                    scope: me
                                }
                            }
                        }
                    ],
                    plugins: [
                        Ext.create('Ext.grid.plugin.RowEditing', {
                            pluginId: 'roweditor',
                            autoCancel: false,
                            clicksToMoveEditor: 1,
                            listeners: {
                                edit: {
                                    fn: me.onRowEditingEdit,
                                    scope: me
                                },
                                canceledit: {
                                    fn: me.onRowEditingCanceledit,
                                    scope: me
                                }
                            }
                        })
                    ],
                    listeners: {
                        selectionchange: {
                            fn: me.onGridpanelSelectionChange,
                            scope: me
                        }
                    }
                }
            ],
            tools: [
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {

                        var grid = tool.up('window').down('grid');
                        var rowEditor=grid.getPlugin('roweditor');
                        var rec = Ext.create(grid.getStore().model, {label: 'New Practice Category'});

                        rowEditor.cancelEdit();
                        grid.getStore().insert(0,rec);
                        rowEditor.startEdit(0,0);

                    },
                    itemId: 'btnAdd',
                    tooltip: 'Add New Record',
                    type: 'plus'
                },
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {
                        var grid = tool.up('window').down('grid');
                        var rowEditor=grid.getPlugin('roweditor');
                        var sm = grid.getSelectionModel();


                        Ext.Msg.confirm(
                        "Delete Practice Category",
                        "Delete " + sm.getSelection()[0].get('label') + "?",
                        function(b) {

                            var store = grid.getStore();
                            rowEditor.cancelEdit();
                            store.remove(sm.getSelection());
                            if (store.getCount() > 0) {
                                sm.select(0);
                            }
                            store.sync();

                        }
                        );
                    },
                    disabled: true,
                    itemId: 'btnDelete',
                    tooltip: 'Delete selected record',
                    type: 'minus'
                },
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {
                        tool.up('window').down('grid').getStore().load();
                    },
                    tooltip: 'Refresh',
                    type: 'refresh'
                }
            ]
        });

        me.callParent(arguments);
    },

    onCheckboxfieldChange: function(field, newValue, oldValue, eOpts) {

        var rec = field.up('grid').getSelectionModel().getSelection()[0];
        rec.set('isActive',newValue);
    },

    onCheckcolumnBeforeCheckChange: function(checkcolumn, rowIndex, checked, eOpts) {
        return false;
    },

    onRowEditingEdit: function(editor, context, eOpts) {
        var rec = context.record;
        
        // this is a key technique:
        // The insert web service must return a primary key value, 
        // which is subsequently applied to the record in memory.
        // The record.commit() removes the grid's default indicator of modified fields
        rec.save({
            success: function(record,operation) {
                if (operation.action == 'create') {
                  var pk = Ext.decode(operation.response.responseText).id;
                  record.set('id',pk);
                }
                record.commit();
            },
            failure: function(record,operation) {
                Ext.Msg.alert('Operation failed',"Please try again later.");
                console.log(arguments);
            }
        });
    },

    onRowEditingCanceledit: function(editor, context, eOpts) {
        var rec = context.record;
        if (rec.phantom) {
         context.grid.getStore().remove(rec);
        }
    },

    onGridpanelSelectionChange: function(model, selected, eOpts) {

        var delBtn = this.down('#btnDelete');
        delBtn.setDisabled(!selected.length);
    }

});

That’s all folks! You should be able to integrate this solution into your apps with very few modifications other than tweaking the delete prompt text and changing the default text for a new record label.

Happy coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s