sencha-logoAfter a few weeks off, we’re back to it! Hopefully, you’ve done your homework and are caught up 🙂

We’ve been working over the last several sessions on laying down some supporting pieces for some real functionality within our application. Not that we haven’t been doing “real” stuff to this point; but most of the pieces have been admittedly simple and are parts of the app that will be used infrequently. That is, managing car colors, categories, makes and models isn’t particularly “business logic”–they’re just ancillary (although necessary) parts of a greater whole.

But now that we have several of these foundational elements in place, we can really start plugging away at the core purpose of our app, which is Inventory Management.

So without further ado, let’s get to it.

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

Our Goals for this Session

In this installment, we want to tackle the following:

  • Build an Inventory Form for entering data
  • Wire up form to a new controller for adding/editing inventory records
  • Display inventory records in a grid (surprise, surprise!)
  • Build a search form for our inventory records

Our inventory management form is actually going to be a bit complex (not in difficulty, but in data collection), so we won’t actually finish the whole thing this time. However, we will see some options for for nesting views and familiarize ourselves with some fun form field types.

Model and Store

By now, we’re pro with building models and stores, so we’ll not go into any detail on this whatsoever, beyond the fields that we want in the model. Car.js looks like this:

/**
 * Model representing a Staff object
 */
Ext.define('CarTracker.model.Car', {
    extend: 'CarTracker.model.Base',    
    idProperty: 'CarID',
    fields: [
        // id field
        {
            name: 'CarID',
            type: 'int',
            useNull : true
        },
        // simple values
        {
            name: 'Description',
            type: 'string'
        },
        {
            name: 'Year',
            type: 'int'
        },
        {
            name: 'ListPrice',
            type: 'int'
        },
        {
            name: 'SalePrice',
            type: 'int'
        },
        {
            name: 'AcquisitionDate',
            type: 'date',
            dateWriteFormat: 'Y-m-d'
        },
        {
            name: 'SaleDate',
            type: 'date',
            dateWriteFormat: 'Y-m-d'
        },
        {
            name: 'IsSold',
            type: 'boolean'
        },
        // relational properties
        {
            name: 'Status',
            type: 'auto'
        },
        {
            name: 'Make',
            type: 'auto'
        },
        {
            name: 'Model',
            type: 'auto'
        },
        {
            name: 'Category',
            type: 'auto'
        },
        {
            name: 'SalesPeople',
            type: 'auto'
        },
        {
            name: 'Color',
            type: 'auto'
        },
        {
            name: 'Features',
            type: 'auto'
        },
        // decorated properties
        {
            name: '_Status',
            type: 'string',
            persist: false
        },
        {
            name: '_Make',
            type: 'string',
            persist: false
        },
        {
            name: '_Model',
            type: 'string',
            persist: false
        },
        {
            name: '_Category',
            type: 'string',
            persist: false
        },
        {
            name: '_Color',
            type: 'string',
            persist: false
        },
        {
            name: '_Features',
            type: 'string',
            persist: false
        },
        {
            name: '_SalesPeople',
            type: 'string',
            persist: false
        }
    ] 
});

As we’ve already discussed, I’m including a number of non-persistent fields (the ones with the underscores) which will contain the values of any foreign key values. Of course, if you want to look these up from the various store instances within the app, you can do that as well…I prefer this method better, but it’s completely up to you and what works best within your application.

A New Controller

In previous installments, we’ve tended to begin with the views when developing new sections of functionality. However, once you become familiar with the approach to section-based development within ExtJS 4 applications, sometimes starting with a Controller makes sense and helps you define from the get-go precisely what you want to happen (and then build the views to support that).

We’ll come back to this later, but let’s start by stubbing out some basic parts of our controller (Cars.js), based on the goals we’ve already defined:

/**
 * Controller for all car-related management functionality
 */
Ext.define('CarTracker.controller.Cars', {
    extend: 'CarTracker.controller.Base',
    ...
    views: [
        'car.List',
        'car.edit.Form',
        'car.edit.Window',
        'car.search.Form',
        'car.search.Window'
    ],
    refs: [
        {
            ref: 'CarList',
            selector: '[xtype=car.list]'
        },
        {
            ref: 'CarEditWindow',
            selector: '[xtype=car.edit.window]'
        },
        {
            ref: 'CarEditForm',
            selector: '[xtype=car.edit.form]'
        },
        {
            ref: 'CarSearchWindow',
            selector: '[xtype=car.search.window]'
        },
        {
            ref: 'CarSearchForm',
            selector: '[xtype=car.search.form]'
        }
    ],
    init: function() {
        this.listen({
            controller: {},
            component: {},
            global: {},
            store: {},
            proxy: {} 
        });
    }
});

Nothing out of the ordinary from what we’ve already developed. However, as you’ll notice, we’ve already defined the views[], as well as the refs[]. As I said earlier, this shows that we’ve already planned what we want to accomplish for this particular section of our app. All that’s left now is to build out the views to support it, and by specifying this ahead of time, we have something of a blueprint of our next steps.

NOTE: If we were to add Cars.js to our app.js right now, we’d be greeted with several error messages as ExtJS complains about not being able to find the views defined. This is expected, of course, since we haven’t created those files yet. If you follow this controller-first approach, be careful about this. If you don’t realize what’s happening, you might waste precious time debugging.

And since we’re pretty comfortable with connecting our controllers to views, there’s actually no reason why we couldn’t start developing the supporting controller methods. However, since this is our first real form for this app, let’s hold off until a bit later.

The Form

Our form for adding/editing inventory records is going to be a bit different than the fairly good-sized one that we made for managing Sales Staff. There are good reasons for this.

First, as we saw in reviewing the Model for our records, we have a fair number of data fields to capture. If we’re not smart, this will make our form really big and awkward to complete.

Second, since we want to be able to manage a fancy list of “Features” that are available for our individual cars, we’re going to use the fancy ItemSelector field type. While this will let us easily maintain a list of features, it is a bulky (in visual size) field element. So it probably makes sense to deal with this specially and separately from the rest of the form fields.

In order to mitigate the impact of these two considerations, we’re going to incorporate a few tricks to help our form’s “flow” feel a bit more manageable:

  1. Group fields together logically (not necessarily functionally) by using Ext.form.FieldSet
  2. Turn our entire Ext.form.Panel in a Ext.tab.Panel. Our first tab will contain the majority of the details for the Car, and the second tab will contain our ItemSelector form field.

We have a plan! So let’s code! Here’s view/car/edit/Form.js:

/**
 * Form used for creating and editing Staff Members
 */
Ext.define('CarTracker.view.car.edit.Form', {
    extend: 'Ext.form.Panel',
    alias: 'widget.car.edit.form',
    requires: [
        'Ext.tab.Panel',
        'Ext.form.FieldContainer',
        'Ext.form.FieldSet',
        ...
    ],
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            items: [
                {
                    xtype: 'tabpanel',
                    bodyPadding: 5,
                    // set to false to disable lazy render of non-active tabs...IMPORTANT!!!
                    deferredRender: false,
                    items: [
                        {
                            xtype: 'car.edit.tab.detail',
                            title: 'Details'
                        },
                        {
                            xtype: 'car.edit.tab.feature',
                            title: 'Available Features'
                        }
                    ]
                }
            ]
        });
        me.callParent( arguments );
    }
});

As you can see, we start by defining our form just like we would with any custom component in our application. The important part is in items[]; you can see that the first (and only) items is a Ext.tab.Panel, which itself has two additional items. As we discussed above, what this will do is create a TabPanel within our Form. Each tab will have different content…in our case, the first tab (labeled “Details”) will have a number of form fields, while the second (labeled “Available Features”) will encapsulate our “Featurs” ItemSelector field.

NOTE: A very important thing to note is that we have set deferredRender: false on the TabPanel. By default, TabPanel’s lazy-render the contents of their tabs, which basically means that any content within those unselected tabs will be nonexistent until the tab is activated. While this works really well in some scenarios, it can be really BAD for forms, especially if your application is expecting ALL form fields to be submitted, regardless of whether they are modified or not.

That’s it. Now we simply need to define the contents of our tab panels. While you can put them wherever you like, I have opted to create a new tab package under view/car/edit, and here I’ll create two new files: Detail.js and Feature.js.

Detail.js

As we discussed above, this “tab” will contain the lion-share of the form fields in our form. As we also discussed, we’ll use Ext.form.FieldSet to provide some separation between logical sections of the form. In the end, we’re going for something that looks like so:

CarForm

As we see above, we’ve divided the form between two fields sets: “Car Info” and “Sales Info”. Of course, the FieldSets don’t drive any particular functionality relating to the fields grouped beneath them, but they do provide a really nice visual separation and grouping which helps the eye focus on logical areas to attack.

A Ext.form.FieldSet is extremely simple. Here’s the syntax used above for the Car Info:

{
     xtype: 'fieldset',
     title: 'Car Info',
     items: [...fields here...]
}

Feature.js

We discussed above that the ItemSelector field allows us a really  nice interface for selecting items from a list. While we could accomplish the same thing with a multi-select Ext.form.field.ComboBox, the ItemSelector field provides a little better visual representation of the data. Here’s an example of what ours will look like:

ItemSelector

Pretty cool, huh?

While the ItemSelector fields provides a really powerful and aesthetically pleasing way to manage large selection lists, it’s configuration is actually not that different from the ComboBox that we’ve already been using:

{
    xtype: 'itemselectorfield',
    name: 'Features',
    anchor: '100%',
    store: {
    	type: 'option.feature'
    },
    displayField: 'LongName',
    valueField: 'FeatureID',
    allowBlank: false,
    msgTarget: 'side',
    fromTitle: 'Available Features',
    toTitle: 'Selected Features',
    buttons: [ 'add', 'remove' ],
    delimiter: undefined
}

A few things to note:

  • By default, there are 6 buttons for ItemSelector field. You can target only particular ones by specifying an array of desired buttons in the buttons[] config. Since we don’t care about sort order at this point, we’ll just use “add” and “remove”
  • By default, ItemSelector will bundle the selected values together as a comma-delimited string. If you need to submit the field as an array, simply set delimiter:null
  • To get the images to show up in the buttons, we’ll need to add a few lines of code to our style.css file, as well as some images to our resources/images/icon folder (see source for these)

Completing the Form

Since we’ve been through developing forms before, we’ll not dwell on it here. To wrap up this form, we’ll do the same Form-in-Window style that we used for the Staff section, and our Details.js will need the following fields:

  • Make (remotecombobox)
  • Model (remotecombobox)
  • Status (remotecombobox)
  • Year (number field)
  • Category (remotecombobox)
  • Color (remotecombobox)
  • Description (htmleditor)
  • ListPrice (numberfield)
  • AcquisitionDate (datefield)
  • SalePrice (numberfield)
  • SaleDate (datefield
  • SalesPeople (remotecombobox w/multiSelect=true)

Back to the Controller

Since we’re using the same basic approach on this form as we used when creating our Staff section, we can utilize the same methods in our Cars.js controller, namely add(), edit(), showEditWindow() and save(). There is one important change we need to add, however.

Oftentimes, when utilizing the grid-to-form approach that we’ve been exploring, it is beneficial to have a slightly different view of data between the grid and the form. For example, on our Inventory form, we have a lot of fields that we *need* to track in the database; however, we don’t necessarily care to show every single data field in the grid. And in fact, with properties like Features, we can’t easily show all the data on the grid. Additionally, if we have a lot of fields per record, we increase the size of the JSON packet that must be returned from the server, and that can cause performance issues with our grid.

Because of this, we’ll make a slight adjustment to our workflow. Instead of loading the form with the same record that is selected in our grid, we can do a pre-form-populate AJAX request to retrieve the full data record from the server. This will let our grid perform with only the necessary data it needs, while still giving our form the full representation of the data that it needs.

To pull this off, we’ll first add a new method to our Base.js controller:

/**
 * Common way to retrieve full data record from the server before performing another action
 * @param {Ext.data.Record} record
 * @param {String} scope
 * @param {Functino} callbackFn
 * @param {Object} extraData
 */
loadDetail: function( record, scope, callbackFn, extraData ) {
    // first, reject any changes currently in the store 
    //so we don't build up an array of records to save by viewing the records
    record.store.rejectChanges();
    // make request for detail record
    Ext.Ajax.request({
    	url: record.store.getProxy().url + '/' + record.internalId + '.json',
    	callback: function( options, success, response ) {
    	    if( success ) {
    	        // set "safe mode" so we don't get hammered with giant Ext.Error
    		data = Ext.decode( response.responseText, true );
                // update record
    		record.set( data );
    		// call callback method
    		callbackFn.call( scope, record, extraData );
    	    }
    	}
    });
}

Pretty simple. This method accepts 4 arguments:

  • record – The data record (from our grid selection)
  • scope – The scope in which to execute the method
  • callbackFn – The method to fire when this is complete
  • extraData – Any extra data to pass along to the callbackFn

To implement this, we simply need to tweak our edit() method in Cars.js a bit:

/**
 * 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.loadDetail( record, me, me.showEditWindow );
}

As we see, instead of calling showEditWindow() directly as we did in Staff.js, we now use our fancy new loadDetail() method, and fire showEditWindow() as our callback method.

Inventory Grid

We created a number of Ext.grid.Panels thus far, so we don’t need to dwell on this right now. Nothing has changed in our general approach, so we can simply create a new one with a few relevant columns:

  • Make
  • Model
  • Category
  • Year
  • Color
  • Acquired
  • List Price
  • Sale Price
  • Sold?
  • Sale Date

This setup does allow us to utilize a number of the data-type columns that ExtJS provides out of the box, so be sure to experiment with those. Ultimately, we want it to look like this:

CarGrid

Search Functionality

Now that we’ve got our regular grid/add/edit/save functionality out of the way, let’s venture into some new territory: adding a search form. In honesty, it’s not that out of bounds of what we’ve already done. It’s just a few more new views and a few new controller methods.

NOTE: At this point in our development, we’ve covered a good percentage of what the majority of your development will probably entail. This is important to realize, because adding functionality to ExtJS is really that simple: a few more views and controller methods. Once you understand this, the speed of your development will really take off!

First, let’s start by creating new package under view/car called search. In here, we’ll create two new classes: Form and Window (surprising, isn’t it?). Since we’ve been down this road before, I won’t go into a lot of detail about what they’re doing since it’s self-explanatory. Our search form (and window) needs to look like this:

CarSearch

I’ll leave you to flesh this out, but I will point out a few items.

First, you’ll notice that our ComboBox fields all have an “X” icon on them. This is a custom plugin (CarTracker.ux.form.field.plugin.ClearTrigger) that allows us to clear the value of the ComboBox field quickly. Our plugin, which we’ll create in ux/form/field/plugin, looks like this:

/**
 * @class CarTracker.ux.form.field.plugin.ClearTrigger
 * @extends Ext.AbstractPlugin
 * Plugin that adds the ability to clean an input field with a trigger class
 * @ptype cleartrigger
 */
 Ext.define('CarTracker.ux.form.field.plugin.ClearTrigger', {
     extend: 'Ext.AbstractPlugin',
     alias: 'plugin.cleartrigger',
     constructor: function() {
         var me = this;
 	 me.callParent( arguments );
 	 var field = me.getCmp();
     },
     init: function( field ) {
         var me = this;
 	 // combobox
 	 if( field.isXType( 'combobox' ) || field.isXType( 'datefield' ) || field.isXType( 'timefield' ) ) {
 	     field.trigger2Cls = 'x-form-clear-trigger';
 	     field.onTrigger2Click = function() {
 	         field.clearValue();
 	     }
         }
         else {
 	     field.triggerCls = 'x-form-clear-trigger';
 	     field.onTriggerClick = function() {
 	         field.reset();
 	     }
 	 }
 	 me.callParent( arguments );
    }
});

To use this on our ComboBox fields, we simply need to add it to each field’s plugins[] configuration, like so:

plugins: [
    { ptype: 'cleartrigger' }
]

NOTE: In this example above, ptype functions in a similar was as xtype for components, but is a special convention just for plugins.

Adding Search to Our Controller

Now that we have our views in place, let’s do some searching! In our Cars.js controller, let’s first set up some listeners for our search form:

nit: function() {
    this.listen({
        controller: {},
        component: {
            ...
            'grid[xtype=car.list] button#search': {
                click: this.showSearch
            },
            'grid[xtype=car.list] button#clear': {
                click: this.clearSearch
            },
            'window[xtype=car.search.window] button#search': {
                click: this.search
            },
            'window[xtype=car.search.window] button#cancel': {
                click: this.close
            }
        },
        ... 
    });
}

(If you haven’t already added the relevant search buttons to the grid and search window, respectively, go ahead and do that now).

Again, nothing new here. We’re simply adding methods in our controller to accomodate the new views…same methodology as with everything else we’ve done before, just different names. With this in mind, let’s concentrate on only two methods: clearSearch() and search().

search()

This is our main method for performing the search against the data in our grid:

/**
 * Executes search
 * @param {Ext.button.Button} button
 * @param {Ext.EventObject} e
 * @param {Object} eOpts
 */
search: function( button, e, eOpts ) {
    var me = this,
        win = me.getCarSearchWindow(),
        form = win.down( 'form' ),
        grid = me.getCarList(),
        store = grid.getStore(),
        values = form.getValues(),
        filters=[];
    // loop over values to create filters
    Ext.Object.each( values, function( key, value, myself ) {
        if( !Ext.isEmpty( value ) ) {
            filters.push({
                property: key,
                value: value
            })
        }
    });
    // clear store filters
    store.clearFilter( true ); // silent=true tells store not to reload
    store.filter( filters );
    // close window
    win.hide();
}

Ultimately, what we are doing here is really quite simple (highlighted in bold):

  • Get form fields from search form
  • Create array of search filters ([{property=fieldName,value=fieldValue},…]) 
  • Clear existing filters from store (so we don’t duplicate filters)
  • Add filters to the store

Since our Cars.js store is configured to remote sort AND remotely filter data, all of our searches will necessarily be sent to the server to be processed, rather than handled locally within the store itself. This is important to understand, as any criteria that we search on will need to be recognized and acted upon by our server-side code. Our remote store, then, is merely making a “handshake” with the server that it will return the proper, filtered data; other than passing along the filters, our remote store is agnostic about the actual accuracy of the filters in relation to the data set which it is managing.

NOTE: I make this point because this is an often-asked question on the forums and StackOverflow. If you have a store with remote filtering, remote sorting, or both, you must absolutely pass off the responsibility of these actions to the server (generally incorporated into some manner of SQL query). Otherwise, your store will not know what to do with the data, other than to display whatever it receives back from the server.

clearSearch()

In addition to searching, we need to be able to clear the search, effectively returning our Inventory grid’s recordset back to its unfiltered form. This is super-simple:

/**
 * Clears search form and resets results
 * @param {Ext.button.Button} button
 * @param {Ext.EventObject} e
 * @param {Object} eOpts
 */
clearSearch: function( button, e, eOpts ) {
    var me = this,
        grid = me.getCarList(),
        store = grid.getStore();
    // clear filter
    store.clearFilter( false ); // silent=false tells store to reload 
}

Wrapping Up and Planning for Next Time

We’ve covered a lot of ground in this installment. From adding and editing Inventory records, to search on them, we’ve knocked out a big chunk of what we want our app to ultimately be. You may have also noticed that we skipped over explicit walkthroughs of every particular bit of functionality. The hope is that by this point, we’ve done these approaches enough that you’re able to fill in the blanks on your own (it’s good practice!).

In spite of how much we accomplished on our form, it’s still incomplete. There are a few things we still need to implement in our Inventory form:

  1. Ability to upload and save images for each car
  2. Create dependent relationship between make and model on form (right now you can choose a “Ford Ram”…)
  3. A “detail” view that will show a graphical, non-editable view of each inventory record’s data 

We’re out of time for that, but we’ll tackle in next time. See you then!