Sencha Touch

Dynamic Sencha Touch Forms : Part 2 : Dynamically loading Select Fields based on previously selected values

Sencha Touch,Web Design & Development Blog 17 Comments

Firstly, apologies for the lateness of this follow up post!

The aim of this post is to describe how we can use Select Fields (combo-boxes) to create a form whose options change based on the previous selections made by the user. So basically when a user makes a selection from the first Select Field we want the next one’s possible values to change, reflecting the choice made.

The scenario we will use is a simple form where the User can choose a Country and then a City from two Select Fields which could be used in a form where a User completes their Address.


As with all our tutorials there is a Tutorial Package that contains all the files you need to try the examples and follow the tutorial step by step. If you click on the Step’s header you can view the demo for that step.

Dynamic Sencha Touch Forms - Part 2 - Tutorial Package

Just drop into the Sencha Touch ‘examples’ directory


Step 1 – Create the form

I have thrown together a quick form to demonstrate this idea which consists of 2 Models (Country & City), 2 Stores (to contain the Country and City data) and a form with 2 Select Fields linked to the Country and City stores. If you view the Step 1 demo page (by clicking the Step 1 title) you will see that the first Select Fields contains 4 Countries and the second contains 8 Cities.


DynamicForms.MyForm = Ext.extend(Ext.form.FormPanel, {

    initComponent: function(){

        Ext.apply(this, {
            floating: true,
            width: 350,
            height: 370,
            centered: true,
            modal: true,
            hideOnMaskTap: false,
            items: [{
                xtype: 'selectfield',
                label: 'Country',
                store: countryStore,
                displayField: 'CountryName',
                valueField: 'CountryID'
            }, {
                xtype: 'selectfield',
                label: 'City',
                store: cityStore,
                displayField: 'CityName',
                valueField: 'CityID'
            }]
        });

        DynamicForms.MyForm.superclass.initComponent.call(this);

     }
});

Now, at a basic level this form is fine – people should know what Country the city they live in is and they should be able to pick their City from an alphabetical list of cities. However, we know we’re better than this because this will no doubt cause some problems with data integrity later on (and we also know that, in general, we have to assume that all end-users are not very smart…) so we should make it easier and more bullet-proof. So, once the User has picked a Country we’re only going to give them the option to select a City that we already know is that chosen Country.

Step 2 – Make it Dynamic!

Like we did in the first Part we are going to hook into the ‘change’ event of the first Select Field and filter the Store (containing the Cities) attached to the second Select Field. This is especially easy because we have our Models set up to have nice relationships between them meaning we can pick up the CountryID of the selected Country and apply that to the City store to get a list of Cities. Easy!

Firstly we’ll listen for the ‘change’ event as we can see below. This code is placed below the initComponent superclass call and we also add the onCountryChange method that will be executed when the event happens:


...
initComponent: function(){

    // config omitted

    DynamicForms.MyForm.superclass.initComponent.call(this);

    this.items.get(0).on({
        change: this.onCountryChange,
        scope: this
    });

},

onCountryChange: function(selectField, value){

}
...

Next we need to implement what happens when a new Country is chosen, i.e. the contents of our onCountryChange method.

In pseudocode this is what we want to do:

  1. Remove any Filter that is currently on the City select field
  2. Add a new Filter based on the newly selected Country ID
  3. Select the first City that appears in that list by default

So, how do we actually do that?

Remove any Existing Filters

Firstly, we grab a reference to the City Select Field component using the ‘get‘ method of the items’ MixedCollection.


var citySelectField = this.items.get(1);

The ‘this‘ reference in our onCountryChange method refers to our GridPanel. This is accomplished by using the ‘scope‘ config option when setting up the listener, which we assigned to the ‘this’ value within the initComponent method.

We can use this config option to force the scope of the listeners which would otherwise be executed in the context of the component that fired them.

Next we remove the existing filter that is on the City select field. We do this by calling the ‘clearFilter‘ method of the Select Field’s store which we can access via the Select Field’s public property ‘store‘.


citySelectField.store.clearFilter(); // remove the previous filter

Filter the Cities Based on the Selected Country

The second of the two parameters that are passed into our ‘onCountryChange‘ method is the value that was selected. This is the Model instance’s CountryID because of the way we configured the Countries Select Field. If you look back to the first code snippet we set the ‘valueField‘ config option to ‘CountryID’ which means that when calling methods such as ‘getValue‘ or ‘setValue‘ on this field you will be dealing with the CountryID of the selected Model instance.

Now that we know which Country has been selected, and the CountryID of it, we can use that to filter the Cities Select Field’s store by that value. This is a simple case of calling the ‘filter‘ method on the City store passing in the Field we want to filter on, and the Value to filter with. For anyone with SQL experience, this is performing a simple WHERE-style filtering, i.e. .filter(‘CountryID’, 1) <=> WHERE CountryID = 1.


citySelectField.store.filter('CountryID', value);

Make a Default Selection

The final step is to make sure that we keep things tidy. If you run your code right now you will find that the filtering works a treat but after selecting a City and then changing your Country, the selected City remains. We can’t have that because then an incorrect City could be submitted for the selected Country. We can fix this by automatically selecting the first City in the newly filtered list or, if there are no Cities available for the chosen Country, just set the value to a blank.

First we grab the first City in the filtered Store.


var firstCity = citySelectField.store.getAt(0);

If there are no Cities present then the firstCity variable will be set to ‘undefined’ and so we can use this as a check before we set the default City value because if there isn’t any we want to reset the Select Field to a blank string.

// if not undefined we can set the value to the first record's CityID
if(firstCity){
 citySelectField.setValue(firstCity.data.CityID);
} else {
 citySelectField.setValue('');
}

That’s all we need to put in our change event handler and the full method looks like this:


onCountryChange: function(selectField, value){
    var citySelectField = this.items.get(1);

    citySelectField.store.clearFilter(); // remove the previous filter

    // Apply the selected Country's ID as the new Filter
    citySelectField.store.filter('CountryID', value);

    // Select the first City in the List if there is one, otherwise set the value to an empty string
    var firstCity = citySelectField.store.getAt(0);
    if(firstCity){
        citySelectField.setValue(firstCity.data.CityID);
    } else {
        citySelectField.setValue('');
    }
}

The last bit of code we need is to force all this to happen when the form first loads because otherwise all the Cities are present in the list even though no Country is selected. We can do this by simply calling the ‘onCountryChange‘ method ourselves at the end of the initComponent method. We pass in a reference to the Country Select Field and a value (CountryID) of 1, which is the default selection.


this.onCountryChange(this.items.get(0), 1);

And there we have it, a simple chained Select Field form.

This technique can improve your form in a couple of ways:

We reduce the possibility of incorrect data hugely.
There will always be someone who picks their Country as England and their City as Cardiff (obviously, being good app designers, we’re checking all this again on the serverside but we want to make that code redundant for the average, non-hacking user) so preventing them from doing this in the first place is an instant win.

It enhances the user experience.
In our simple example the lists are only a few of entries long meaning finding the right entry is fairly easy, but imagine dealing with much larger lists and the problem is multiplied, leading to frustrated and angry users.

I hope this quick tutorial has helped you to add some more clever dynamics to your forms. If you have any ideas for other ways to make forms a little more clever then leave a comment and we will see if we can come up with wee tutorial explaining how to do it.

Share this

17 Comments to "Dynamic Sencha Touch Forms : Part 2 : Dynamically loading Select Fields based on previously selected values"

  1. jayesh

    May 26, 2011

    In the same example i changed CountryID: 11 for CountryName: ‘Northern Ireland’ and CityID: 8, CountryID: 11, CityName: ‘Londonderry’ which says i m suppose to get Londonderry when i select Northern Ireland’ but problem is when i select Scotland also i get Londonderry …why is that …1 and 11 is it searching string in string ?

  2. Stuart

    May 30, 2011

    Hi Jayesh

    Thanks for the comment and for pointing out this flaw.

    By default the filter method (when just specifying a Field and a Value) will do a ‘non-exact’ match filter. Which means that the regex created does not include a ‘$’ at the end. We can correct this by passing a full Filter config to the filter method which will give us this fine control over how the filtering is done.

    Our code should become:

    citySelectField.store.filter({
    property: 'CountryID',
    value: value,
    exactMatch: true
    });

    Additional options you can specify include ‘caseSensitive’ and ‘anyMatch’.

  3. Mihail

    June 4, 2011

    Thanks a mil! :D

  4. Dinesh

    July 21, 2011

    Can you please post the source code.

  5. Stuart

    July 21, 2011

    If you download the Tutorial Files from the link at the top of the article you will have access to all the source code.

    Thanks
    Stuart

  6. Taik

    July 28, 2011

    Cheers bookmarked!

  7. Michael

    December 7, 2011

    i get [Exception: TypeError: Object [object Object] has no method ‘get’] on this.dockedItems.get(0)

    any ideas?

  8. Stuart

    December 7, 2011

    The dockedItems property is an array and so doesn’t have a ‘get’ method. You should access its elements using the usual array syntax (this.dockedItems[0]).

  9. Michael

    December 7, 2011

    Ahhh… :P

  10. Michael

    December 7, 2011

    Actually I’m trying to do something a bit different then what you are doing here.. I have a segmented button with 4 items, each of these items have a tap handler.

    My problem is I need the scope (this) in this.periodChanged because it is calling another method… I tried replacing handler: this.periodChanged with addListener: { tap: this.periodChanged, scope: this } but nothing happens when i tap the button…

    {
    xtype: ‘segmentedbutton’,
    allowDepress: true,
    items: [{ text: 'Day', handler: this.periodChanged, pressed: true },
    { text: 'Week', handler: this.periodChanged },
    { text: 'Month', handler: this.periodChanged },
    { text: 'Year', handler: this.periodChanged }
    ]
    };

    Thank you

  11. Michael

    December 7, 2011

    nevermind… I got! thanks you!

  12. Brian

    January 27, 2012

    How would you add another layer… with “Region” for instance?

  13. Stuart

    January 27, 2012

    You can add another field in the chain by first adding the select field. Then add a new method called onRegionChange and attach it to the new field’s change event. It’s then just a case of duplicating the logic in the example method (onCountryChange) but point it to the next selectfield.

    The process will look a little like this:

    Country Select Field -> -> onCountryChange fires where we filter Region Select Field based on selection (potentially resetting the City selection)
    Region Select Field -> -> onRegionChange fires where we filter City Select Field based on region selection.

    Hope that makes sense!
    Stuart
    .

  14. Brian

    January 30, 2012

    I have begun to add the suggestions you made. In this, I created a Ext.regModel(‘Region’…), created a regionStore, added a Region ‘selectfield’ to the beginning of the Ext.apply(…items…), and created an onRegionChange method:

    onRegionChange: function(selectField, value){
    this.items.get(0).on({
    change: this.onCountryChange,
    scope: this,
    });

    var countrySelectField = this.items.get(1);

    countrySelectField.store.clearFilter();

    countrySelectField.store.filter(‘RegionID’, value);

    var firstCountry = countrySelectField.store.getAt(0);

    if(firstCountry){
    countrySelectField.setValue(firstCountry.data.CountryID);
    } else {
    countrySelectField.setValue(”);
    }
    }

    Currently only the first items in Ext.apply(…items…) filters the second. Currently changing ‘Region’ changes ‘Country’, while the ‘City’ drop down list displays all of the cities.

    How and where should I fire the filter on the cities?

    Any suggestions would be appreciated!!!

  15. Stuart

    January 30, 2012

    You should put the Cities filter in the change event handler function of the second select field (the Country field in your case I think) so that when a value is chosen the City field is filtered.

    I would recommend moving the change event binding you are doing at the start of your onRegionChange method to your initComponent method as this will bind the onCountryChange method to the change event everytime the region changes and so might give you some weird results.

    Cheers
    Stuart

  16. Brian

    February 2, 2012

    I added to the
    “if(firstCountry){countrySelectField.setValue(firstCountry.data.CountryID);…}”:


    this.items.get(0).on({
    change: this.onCountryChange,
    scope: this
    });

    this.onCountryChange(this.items.get(0), 1);

    And it works! Thanks!!!

  17. Brian

    February 2, 2012

    Additionally I added the below methods to the initComponent: function() after this.onRegionChange(this.items.get(0), 1);


    this.onRegionChange(this.items.get(0), 1);

    this.items.get(1).on({
    change: this.onCountryChange,
    scope: this
    });

    This ensures that every time you change a country the cities are updated.

    Great tutorial and great support!!!

Leave a Comment