sencha-logoAfter not one, but TWO diversions, we’re finally back on track! If you remember from our last installment, we built a handy editable grid with support for all our fancy CRUD actions. We also saw that we could build this in a very abstract way, thus accommodating a lot of separate but similar models, yet with very little code duplication.

While this approach definitely has its place, there are times when our data becomes a bit complex and no longer lends itself very well to the editable grid. In these cases, it’s probably best to go back to the ol’ web form…which is precisely what we’ll do.

 

NOTE: The code for this session can be found on GitHub

A Bit o’ Planning

Before we start hacking away at a form, let’s plan out what we’ll need. What we want to build in this installment is a management tool for Sales Staff. This has a few initial requirements:

  • Display sortable list of all Sales Staff
  • Allow for adding new Sales Staff members
  • Allow for editing existing Sales Staff members

And here is the data that management wants us to collect:

  • First Name
  • Last Name
  • Date of Birth
  • Address (Addy 1, Addy 2, City, State, Zip)
  • Phone
  • Hire Date
  • Position (a value from our “Position” table)

Now that we have our requirements, let’s think through the kinds of ExtJS assets we’ll need to create.

  • Sortable List: Obviously SCREAMS grid
  • Add/Edit Functionality: Since the data is a bit complex, probably better to do a form of some kind. I think popping up the form in a modal window will be best
  • New Data: Obviously we’ll need a Store and a Model
  • New Section: Our rule is that we should cordon off functionality for sections into separate controllers. So obviously we’ll need one of these.

This gives us:

  • Grid
  • Model
  • Store
  • Form
  • Window
  • Controller

While this exercise might seem a bit trivial, it does nicely illustrate a helpful way to approach our apps. If you simply plunge in and start coding, it’s easy to miss the forest for the trees. If this happens, you’ll eventually find your way out, but at the cost of a lot of unnecessary refactoring :)

Plus, now that we’ve briefly articulated the scope of what we’re going to be doing, we don’t need to spend a lot of time talking about the constituent parts. Below, I’ll include the code, and then we’ll talk once we get around to the controller, okay? Cool.

Staff Management

The Model (model/Staff.js)

/**
 * Model representing a Staff object
 */
Ext.define('CarTracker.model.Staff', {
      extend: 'CarTracker.model.Base',    
      idProperty: 'StaffID',
      fields: [
            // id field
            {
                  name: 'StaffID',
                  type: 'int',
                  useNull : true
            },
            // simple values
            {
                  name: 'FirstName',
                  type: 'string'
            },
            {
                  name: 'LastName',
                  type: 'string'
            },
            {
                  name: 'DOB',
                  type: 'date',
                  dateWriteFormat: 'Y-m-d'
            },
            {
                  name: 'Address1',
                  type: 'string'
            },
            {
                  name: 'Address2',
                  type: 'string'
            },
            {
                  name: 'City',
                  type: 'string'
            },
            {
                  name: 'State',
                  type: 'string'
            },
            {
                  name: 'PostalCode',
                  type: 'string'
            },
            {
                  name: 'Phone',
                  type: 'string'
            },
            {
                  name: 'HireDate',
                  type: 'date',
                  dateWriteFormat: 'Y-m-d'
            },
            // relational properties
            {
                  name: 'Position',
                  type: 'auto'
            },
            // decorated properties
            {
                  name: '_Position',
                  type: 'string',
                  persist: false
            }
      ] 
});

You’ll notice the section at the bottom for the _Position field. While I’ll talk about this more later, I often prefer to include the text value of a relational property (e.g., Position) in my data coming from the server. This prevents having to unnecessarily lookup the value from a store on the ExtJS side (which would require the store exists, has data, etc.). However, I have marked it as persist: false so that it isn’t submitted to the server.

The Store (store/Staff.js)

/**
 * Store for managing staff
 */
Ext.define('CarTracker.store.Staff', {
    extend: 'CarTracker.store.Base',
    alias: 'store.staff',
    requires: [
        'CarTracker.model.Staff'
    ],
    restPath: '/api/staff',
    storeId: 'Staff',
    model: 'CarTracker.model.Staff'
});

We’ve already seen this config a few times elsewhere…moving on…

The “Edit” Package

Early on, we discussed the importance and benefits of “packaging” related functionality together. One pattern I like to follow is that when I have a window/form type of editing mechanism for a section of functionality, I always create an edit package within the specific view package for that section.

So for example, in view/ we’ll create our staff package. Then, within that package, we can create another package named edit. Then, within that the edit package, we can create two new views, one named Form.js and one named Window.js. Ultimately, it looks like this:

  • view
    • staff
      • edit
        • Form.js (xtype: staff.edit.form)
        • Window.js (xtype: staff.edit.window)

What I really like about this is it creates a very repeatable (and therefore predictable) pattern across my application. So then, if another developer comes to the app and needs to adjust the edit form for Section X, all they have to do is follow the pattern and they’re all set.

Additionally, this also creates a very consistent and predictable xtype pattern. If all my edit forms live in the same packages (relative to their sections) and have the same xtype structure, it’s incredible easy to use them in controllers without having to constantly look them up.

The Form (view/staff/edit/Form.js)

/**
 * Form used for creating and editing Staff Members
 */
Ext.define('CarTracker.view.staff.edit.Form', {
    extend: 'Ext.form.Panel',
    alias: 'widget.staff.edit.form',
    requires: [
        'Ext.form.FieldContainer',
        'Ext.form.field.Date',
        'Ext.form.field.Text',
        'Ext.form.field.ComboBox',
        'CarTracker.ux.form.field.RemoteComboBox'
    ],
    bodyPadding: 5,
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            fieldDefaults: {
                allowBlank: false,
                labelAlign: 'top',
                flex: 1,
                margins: 5
            },
            defaults: {
                layout: 'hbox',
                margins: '0 10 0 10'                
            },
            items: [
                {
                    xtype: 'fieldcontainer',
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'FirstName',
                            fieldLabel: 'First Name'
                        },
                        {
                            xtype: 'textfield',
                            name: 'LastName',
                            fieldLabel: 'Last Name'
                        },
                        {
                            xtype: 'datefield',
                            name: 'DOB',
                            fieldLabel: 'DOB'
                        }
                    ]
                },
                {
                    xtype: 'fieldcontainer',
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'Address1',
                            fieldLabel: 'Address1'
                        },
                        {
                            xtype: 'textfield',
                            name: 'Address2',
                            allowBlank: true,
                            fieldLabel: 'Address2'
                        }
                    ]
                },
                {
                    xtype: 'fieldcontainer',
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'City',
                            fieldLabel: 'City'
                        },
                        {
                            xtype: 'textfield',
                            name: 'State',
                            fieldLabel: 'State'
                        },
                        {
                            xtype: 'textfield',
                            name: 'PostalCode',
                            fieldLabel: 'Postal Code'
                        }
                    ]
                },
                {
                    xtype: 'fieldcontainer',
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'Phone',
                            fieldLabel: 'Phone'
                        },
                        {
                            xtype: 'ux.form.field.remotecombobox',
                            name: 'Position',
                            fieldLabel: 'Position',
                            displayField: 'LongName',
                            valueField: 'PositionID',
                            store: {
                                type: 'option.position'
                            },
                            editable: false,
                            forceSelection: true
                        },
                        {
                            xtype: 'datefield',
                            name: 'HireDate',
                            allowBlank: false,
                            fieldLabel: 'Hire Date'
                        }
                    ]
                }
            ]
        });
        me.callParent( arguments );
    }
});

An Architectural Aside

You’ll notice above that I’m using a custom combo box to handle the Position value. This custom combo box (RemoteComboBox) is just like a regular Ext.form.field.ComboBox, but with one significant difference. When setValue() is called (such as when the loadRecord() method of the Ext.form.Panel is called), this ComboBox will automatically make a remote request to the server, passing along the value being set (e.g., the value of the ID property of the model), as well as the valueField configuration. Ultimately, it’s making a remote query for the specific value that’s being set.

There are a number of reasons I do this.

Availability of Data

The first reason is the availability of data. If we are editing a Staff Member, our assumption is that the Position value has already been set. So then, when we open the editing form, we need the Position combo box to be populated with the correct value. However, the only value that our Staff Member record shares in common with the combo box’s store is the PositionID…so the only way to make the connection is if the combo box’s store already contains a record with the matching ID.

But the only way that the data will be there is if we have manually loaded it, set autoLoad to true, or use an instance of the store that has already been loaded with data (which presumes one of the previous two options again).

So how are we going to populate the combo box? I dislike autoLoad: true, simply because of the lack of control. To really ensure that the data is available, you have to be far too concerned with the post-load happenings, and this becomes problematic when you have this config on dozens of stores.

We also can’t depend on the existence of the store data from its use in other parts of the site. For example, our store is obviously loaded when we do something in the “Positions” grid. However, if we don’t start in that section, that store will never be loaded, so we’ll be out of luck if we begin in the Staff Management section.

This is one main reason I like to attach stand-alone instances of the store to my remote combo box, and load the data on-demand.

Existence of Specific Data

If we were talking about an app where we had only a small number of “option” stores, and the maximum data in each store were very small (say 30 records or less), I might be persuaded to go the autoLoad route. But what if one of our option stores has hundreds or thousands of records? Instead of forcing the user to wait and wait for all those to be cached in the store and rendered in the combo box, it’s much for user-friendly to page these records, and even provide some kind of auto-complete. While this is really great for the creation of new data, it presents a problem for existing data.

For example, consider that our Position table has 250 records. Let’s also say that we’ve set a page limit of 30 records to our store. Let’s also say that one a particular Staff Member record, the Position value is somewhere on page 3 of the data set. If we open our form for editing, a few things will occur:

  • Our combo box’s store will, or may have already loaded the first page of data
  • setValue() will attempt to set the value, using the Position value in the Ext.data.Model instance to compare with extant records in the store
  • Since the selected value is on page 3, no match will be found
  • The combo box will appear empty, or worse, the ID of selection will show up, rather than the displayField config (since no match was found)

Not good. Not good at all.

Integrity of Data

But perhaps the biggest reason to take a different approach is that if we are depending on a single store to drive multiple areas of our site, we have to go to great lengths to maintain the integrity of our store.

What do I mean by this?

In the course of developing our app, we’re going to want to reuse data, often with different purposes in mind. For example, in one section, we may want ALL the potential options in a specific store. However, in another section, we may want to filter the result set to only show a particular subset of the data. And in yet another section, perhaps we need to further filter the subset, create a new subset, or even add a grouping or ordering to it.

What this can quickly lead to is a lot of frustration. Since we are using the same store instance, the filters, sorts, etc. that get applied in one place get applied in all places. We soon find ourselves in the business of constantly resetting/reloading the store so that we can get the proper representation of the data, and our controllers get littered with a lot of really unnecessary management code.

The Tradeoff

All of the reasons above are what inform the approach I’ve taken with this form. The tradeoff, of course, is a lot more AJAX requests. Instead of loading the store once or even a few times, we load it every time the edit form is openend, every time the store if filtered, etc. The benefit is that we don’t have to bother with a lot of store management processes; the cost is more trips to the server for data.

Ultimately, you have to determine the best option for your app. I’m not saying either approach is completely wrong or completely perfect. It comes down to knowing the data that you have, knowing its place within your app, and finding the best balance between ease of management and performance.

The Window (view/staff/edit/Window.js)

Ext.define('CarTracker.view.staff.edit.Window', {
    extend: 'Ext.window.Window',
    alias: 'widget.staff.edit.window',
    requires: [
        'CarTracker.view.staff.edit.Form'
    ],
    iconCls: 'icon_user',
    width: 600,
    modal: true,
    resizable: true,
    draggable: true,
    constrainHeader: true,
    layout: 'fit',
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            items: [
                {
                    // include form
                    xtype: 'staff.edit.form'
                }
            ],
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'bottom',
                    ui: 'footer',
                    items: [
                        {
                            xtype: 'button',
                            itemId: 'cancel',
                            text: 'Cancel',
                            iconCls: 'icon_delete'
                        },
                        '->',
                        {
                            xtype: 'button',
                            itemId: 'save',
                            text: 'Save',
                            iconCls: 'icon_save'
                        }
                    ]
                }
            ]
        });
        me.callParent( arguments );
    }
});

You’ll notice that I put the Cancel/Save buttons on the Window, rather than on the Form. You can do either. I prefer this way because I think it looks better. If you want to do it differently, be my guest :).

The Grid (view/staff/List.js)

/**
 * Grid for displaying Staff details
 */
Ext.define('CarTracker.view.staff.List', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.staff.list',
    requires: [
        'Ext.grid.column.Boolean',
        'Ext.grid.column.Date'
    ],
    title: 'Manage Staff Members',
    iconCls: 'icon_user',
    store: 'Staff',
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            columns: {
                defaults: {},
                items: [
                    {
                        text: 'Position',
                        dataIndex: '_Position',
                        width: 200
                    },
                    {
                        text: 'Name',
                        dataIndex: 'LastName',
                        renderer: function( value, metaData, record, rowIndex, colIndex, store, view ) {
                            return value + ', ' + record.get( 'FirstName' )
                        },
                        width: 200
                    },
                    {
                        xtype: 'datecolumn',
                        text: 'Hire Date',
                        dataIndex: 'HireDate',
                        dateFormat: 'Y-m-d'
                    },
                    {
                        text: 'Phone',
                        dataIndex: 'Phone'
                    },
                    {
                        xtype: 'booleancolumn',
                        text: 'Active',
                        dataIndex: 'Active',
                        trueText: 'Yes',
                        falseText: 'No'
                    },
                    {
                        xtype: 'datecolumn',
                        text: 'DOB',
                        dataIndex: 'DOB',
                        dateFormat: 'Y-m-d',
                        hidden: true
                    },
                    {
                        text: 'Address1',
                        dataIndex: 'Address1',
                        hidden: true
                    },
                    {
                        text: 'Address2',
                        dataIndex: 'Address2',
                        hidden: true
                    },
                    {
                        text: 'City',
                        dataIndex: 'City',
                        hidden: true
                    },
                    {
                        text: 'State',
                        dataIndex: 'State',
                        hidden: true
                    },
                    {
                        text: 'Postal Code',
                        dataIndex: 'PostalCode',
                        hidden: true
                    }
                ]
            },
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'top',
                    ui: 'footer',
                    items: [
                        {
                            xtype: 'button',
                            itemId: 'add',
                            iconCls: 'icon_add',
                            text: 'Add Staff Member'
                        }
                    ]
                },
                {
                    xtype: 'pagingtoolbar',
                    ui: 'footer',
                    defaultButtonUI: 'default',
                    dock: 'bottom',
                    displayInfo: true,
                    store: me.getStore()
                }
            ]
        });
        me.callParent( arguments );
    }
});

A couple things:

First, notice (again) the _Position field making its presence known again. The usage here illustrates my point about sometimes wanting to include the text representation of a non-text value (e.g., PositionID) in the response data from the server. If I DIDN’T do this, I would need to lookup the value of the ID for each record in my grid from the Positions store. Of course, in order to do this, the Positions store needs to exists AND have data. If it doesn’t exist, or hasn’t yet been loaded, my column will have no data. While it’s certainly easy enough to accomodate this (perhaps load the data immediately on app creation?), I like my approach a bit better, simply because I enjoy being lazy and not having to manage that extra complexity, all for only the sake of one column in a grid.

Second, notice the renderer used for the Name column. Instead of breaking FirstName and LastName out into their own columns, I used the renderer to create a custom view of the data as the grid is being rendered.

Putting it All Together

Now that we’ve completed our model, store, and views, we can tie them all together in our controller. In controller, let’s create Staff.js:

/**
 * Controller for all staff-related management functionality
 */
Ext.define('CarTracker.controller.Staff', {
    extend: 'CarTracker.controller.Base',
    stores: [
        'Staff'
    ],
    views: [
        'staff.List',
        'staff.edit.Form',
        'staff.edit.Window'
    ],
    refs: [
        {
            ref: 'StaffList',
            selector: '[xtype=staff.list]'
        },
        {
            ref: 'StaffEditWindow',
            selector: '[xtype=staff.edit.window]'
        },
        {
            ref: 'StaffEditForm',
            selector: '[xtype=staff.edit.form]'
        }
    ],
    init: function() {
        this.listen({
            controller: {},
            component: {
                'grid[xtype=staff.list]': {
                    beforerender: this.loadRecords,
                    itemdblclick: this.edit,
                    itemcontextmenu: this.showContextMenu
                },
                'grid[xtype=staff.list] button#add': {
                    click: this.add
                },
                'window[xtype=staff.edit.window] button#save': {
                    click: this.save
                },
                'window[xtype=staff.edit.window] button#cancel': {
                    click: this.close
                }
            },
            global: {},
            store: {},
            proxy: {} 
        });
    },
    /**
     * Displays context menu 
     * @param {Ext.view.View} view
     * @param {Ext.data.Model} record 
     * @param {HTMLElement} item
     * @param {Number} index
     * @param {Ext.EventObject} e
     * @param {Object} eOpts
     */
    showContextMenu: function( view, record, item, index, e, eOpts ) {
        var me = this;
        // stop event so browser's normal right-click action doesn't continue
        e.stopEvent();
        // if a menu doesn't already exist, create one
        if( !item.contextMenu ) {
            // add menu
            item.contextMenu = new Ext.menu.Menu({
                items: [
                    {
                        text: 'Edit Staff Member',
                        iconCls: 'icon_edit',
                        handler: function( item, e ) {
                            me.edit( view, record, item, index, e, eOpts );
                        }
                    },
                    {
                        text: 'Delete Staff Member',
                        iconCls: 'icon_delete',
                        handler: function( item, e ) {
                            me.remove( record );
                        }
                    }
                ]
            })
        }
        // show menu relative to item which was right-clicked
        item.contextMenu.showBy( item );
    },
    /**
     * Loads the grid's store
     * @param {Ext.grid.Panel} grid
     * @param {Object} eOpts
     */
    loadRecords: function( grid, eOpts ) {
        var me = this,
            store = grid.getStore();
        // clear any fliters that have been applied
        store.clearFilter( true );
        // load the store
        store.load();
    },
    /**
     * Handles request to edit
     * @param {Ext.view.View} view
     * @param {Ext.data.Model} record 
     * @param {HTMLElement} item
     * @param {Number} index
     * @param {Ext.EventObject} e
     * @param {Object} eOpts
     */
    edit: function( view, record, item, index, e, eOpts ) {
        var me = this;
        // show window
        me.showEditWindow( record );
    },
    /**
     * Creates a new record and prepares it for editing
     * @param {Ext.button.Button} button
     * @param {Ext.EventObject} e
     * @param {Object} eOpts
     */
    add: function( button, e, eOpts ) {
        var me = this,
            record = Ext.create( 'CarTracker.model.Staff' );
        // show window
        me.showEditWindow( record );
    },
    /**
     * Persists edited record
     * @param {Ext.button.Button} button
     * @param {Ext.EventObject} e
     * @param {Object} eOpts
     */
    save: function( button, e, eOpts ) {
        var me = this,
            grid = me.getStaffList(),
            store = grid.getStore(),
            win = button.up( 'window' ),
            form = win.down( 'form' ),
            record = form.getRecord(),
            values = form.getValues(),
            callbacks;

        // set values of record from form
        record.set( values );
        // check if form is even dirty...if not, just close window and stop everything...nothing to see here
        if( !record.dirty ) {
            win.close();
            return;
        }
        // setup generic callback config for create/save methods
        callbacks ={
            success: function( records, operation ) {
                win.close();
            },
            failure: function( records, operation ) {
                // if failure, reject changes in store
                store.rejectChanges();
            }
        };
        // mask to prevent extra submits
        Ext.getBody().mask( 'Saving Staff Member...' );
        // if new record...
        if( record.phantom ) {
            // reject any other changes
            store.rejectChanges();
            // add the new record
            store.add( record );
        }
        // persist the record
        store.sync( callbacks );
    },
    /**
     * Persists edited record
     * @param {Ext.button.Button} button
     * @param {Ext.EventObject} e
     * @param {Object} eOpts
     */
    close: function( button, e, eOpts ) {
        var me = this,
            win = button.up( 'window' );
        // close the window
        win.close();
    },
    /**
     * Displays context menu 
     * @param {Ext.data.Model[]} record
     */
    remove: function( record ) {
        var me = this,
            store = record.store;
        // show confirmation before continuing
        Ext.Msg.confirm( 'Attention', 'Are you sure you want to delete this Staff Member? This action cannot be undone.', function( buttonId, text, opt ) {
            if( buttonId=='yes' ) {
                store.remove( record );
                store.sync({
                    /**
                     * On failure, add record back to store at correct index
                     * @param {Ext.data.Model[]} records
                     * @param {Ext.data.Operation} operation
                     */
                    failure: function( records, operation ) {
                        store.rejectChanges();
                    }
                })
            }
        })
    },
    /**
     * Displays common editing form for add/edit operations
     * @param {Ext.data.Model} record
     */
    showEditWindow: function( record ) {
        var me = this,
            win = me.getStaffEditWindow(),
            isNew = record.phantom;
        // if window exists, show it; otherwise, create new instance
        if( !win ) {
            win = Ext.widget( 'staff.edit.window', {
                title: isNew ? 'Add Staff Member' : 'Edit Staff Member'
            });
        }
        // show window
        win.show();
        // load form with data
        win.down( 'form' ).loadRecord( record );
    }
});

Honestly, there’s not a lot that’s new here beyond what we’ve done before. If you notice toward the top, we include the refs for all our views that will be managed by this controller. Then we have the standard control section, our add/edit methods, a mechanism for saving, etc.

The most interesting thing in this controller, I think, is how we have made both the add() and edit() methods function the same way. Both of them marshall the data record that we want to edit, and then hand it off to the common showEditWindow() method which handles the actual view rendering.

But other than that, our controller follows much of the same principles as the one we created for Options…it’s just tailored a bit for the specific needs of this section.

Once we’re done, all that’s left is to add our Staff controller to our app.js controllers[] config, and we’re good to go.

A Few More Tweaks

If you’ve followed along, you may be wondering where all the validation for our form is. Sure, we have the allowBlank config set on most of the fields. However, there is nothing in the way of any other kind of validation whatsoever.

The reason for this is that I am lazy. I believe very strongly in validating data…but I HATE doing it twice. I dislike creating strong server-side validation, and then having to replicate it on the client. This makes for a maintenance headache as the app expands since we perpetually have to duplicate work.

To get around this, I do all of the validation on the server side. The basic approach is:

  • Submit form
  • Before saving, validate data
  • If data has validation errors, return errors and do not save
  • If data has no validation errors, save and return success

Since all of our forms in this app are AJAX submissions anyway, we lose nothing in the approach other than the pain of having to replicate validation.

Of course, in order to support this, we need to add some logic to our application to handle the response and display of these validation errors. Since we’re probably going to want to use this on other forms as well, it will be in our best interests to abstract this as best we can.

Handling the Response

First, we need to handle the response of validation errors.

If you remember from a few posts back, we added a hook into the exception event of our custom REST proxy. This was setup to trap errors coming back in response to our proxy requests. While this is nice for catastrophic failures, it will also fire if we ever return success: false, such as we might like to do in the case of validation errors. Plus, it will be handy for other purposes to have a hook into ALL responses from the server via our proxy, not just the failures. Therefore, let’s ditch the exception event, and go a different direction.

By default, Ext.data.proxy.Server has a method called afterRequest(). This method in the source is a template method, which basically means that it is there for us to override in our subclass and do whatever we’d like with it. While we may expand it more in the future, for now let’s just have our afterRequest() fire a custom event. In proxy/Rest.js, we’ll add:

afterRequest: function( request, success ) {
    var me = this;
    // fire requestcomplete event
    me.fireEvent( 'requestcomplete', request, success );
}

Now we can listen to this event anywhere within our app and act on it. Since we’re striving to be as abstract as possible, let’s add some hooks into our main App.js controller so that we can handle responses for proxy requests across our entire app. In App.js, let’s replace the proxy exception event listener with an event listener for our new requestcomplete event :

this.listen({
    ...
    proxy: {
        '#baserest': {
            requestcomplete: this.handleRESTResponse
        }
    } 
});

And then, we can add the new handleRESTResponse() method:

/**
 * After a REST response is completed, this method will marshall the response data and inform other methods with relevant data
 * @param {Object} request
 * @param {Boolean} success The actual success of the AJAX request. For success of {@link Ext.data.Operation}, see success property of request.operation
 */
handleRESTResponse: function( request, success ) {
    var me = this,
        rawData = request.proxy.reader.rawData;
    // in all cases, let's hide the body mask
    Ext.getBody().unmask();
    // if proxy success
    if( success ) {
        // if operation success
        if( request.operation.wasSuccessful() ) {
            ...
        }
        // if operation failure
        else {
            // switch on operation failure type
            switch( rawData.type ) {
                case 'validation':
                    me.showValidationMessage( rawData.data, rawData.success, rawData.message, rawData.type );
                    break;
            }
        }
    }
    // otherwise, major failure...
    else {
        ...
    }
}

A few things of importance. First, it’s critical to understand that the success argument coming from afterRequest() refers to the overall success of the AJAX request, NOT the success value of the data returned in the request. This is why when we verify that the request was successful ( if(success) ), we then check the success state of the operation. The latter represents the success status determined from the data returned from the server, while the former is related to the actual success of the AJAX request. Using this method, then, we can globally handle all sorts of things, like validation error messages, other error messages, post-save success messages…and even catastrophic AJAX request failures.

But since we’re talking form validation, let’s look at the section in bold. By default, this method expects that all responses from the server will include a type data attribute, which will be used in the switch to determine what we want to do with the response. In this case, if the type is “validation”, we will call the showValidationMessage() method, which looks like this:

/**
 * Displays errors from JSON response and tries to mark offending fields as invalid
 * @param {CarTracker.proxy.Rest} proxy
 * @param {Array} data
 * @param {Boolean} success
 * @param {String} message
 * @param {String} type
 */
showValidationMessage: function( data, success, message, type ) {
    var me = this,
        errorString = '<ul>';
    // looping over the errors
    for( var i in data ) {
        var error = data[ i ];
        errorString += '<li>' + error.message + '</li>';
        // match form field with same field name
        var fieldMatch = Ext.ComponentQuery.query( 'field[name=' + error.field + ']' );
        // match?
        if( fieldMatch.length ) {
            // add extra validaiton message to the offending field
            fieldMatch[ 0 ].markInvalid( error.message );
        }
    }
    errorString += '</ul>';
    // display error messages in modal alert
    Ext.Msg.alert( message, errorString );
}

The notes should be pretty self-explanatory, but in a nutshell, this loops over the array of error messages in the data, builds out an HTML unordered list, and displays the error messages via Ext.MessageBox.alert(). Additionally, it tries to find a rendered field that matches the name of the field in the error message. If it finds a match, it marks the field as invalid (red box, by default) and adds the returned error message to the field. The result is something like so:

ErrorMessagesFieldError

 

While certainly not a silver-bullet for validation, I like this approach because it works pretty well in most scenarios, and best of all, only requires that validation happens in one place. This makes the scenarios that don’t fit in the box a bit more palatable.

NOTE: While I do all the validation server-side anyway, I do take advantage of any simple built-in validations that are baked into ExtJS form fields by default, like number, date, min/max, etc.

Conclusion

And with that, we come to the end of this installment. In this walkthrough, we built a complex data form, bound it to a controller, and even expanded our REST proxy to provide more robust response tracking. But even though we forged new ground in terms of the views we created, we also saw that we were able to reuse most of the same principles that we used when developing our editable grid CRUD management view. In fact, the only thing that fundamentally changed between the two sections was the way in which data was entered; the actual management of the data remained virtually unchanged.

We’ve come along way, and our app is really starting to come together. In the next installment, we’ll attack the major section of the site. See you then!