Introducing React ReExt – Sencha Ext JS Components in React! LEARN MORE

Building a User Extension and Integrating It Into Sencha Architect 3 (Part 1)

December 10, 2013 105 Views
Show

Building an Extension in Sencha Architect 3Regardless of the type of application you’re building, there is almost always room to create a custom component or user extension that can be used across projects or teams. There is a rich ecosystem of user extensions surrounded by the vibrant Sencha community. We love to see developers share their work with the community and empower other developers to accomplish tasks more quickly. We’d like to show you some of the considerations that we take into account when building the API for a user extension.

This is the first article in a two-part series on building a user extension and integrating it into Architect. We will walk through some of the challenges in creating a robust user extension, and in the next article, we will put it all together and package it for reuse in Architect.

When building an application, sometimes you’re able to recognize which components should be user extensions right away. Others may take an implementation or two before you recognize that you are repeating yourself and need to refactor it into a user extension. The common use case that we have selected is Linked or Chained ComboBoxes. Here’s an example of what we consider a chained combo: consider the case where a person has to select the Make, Model and then Trim level of their car to purchase parts. Each time the person selects an option, it further limits the choices available in the next combobox. This is a perfect example of an interaction that developers implement over and over in different ways. It’s also a great example of a use case that can be wrapped up into a user extension for use inside and outside of Architect.

Linked Combo Container

When developing a custom component, you need to consider several use cases to make it useful to the majority of people while not making the API too complex. Oftentimes, configurations may be mutually exclusive (or certain configurations may only be valid when another configuration is set to a certain value). The trick is to make the component configurable enough to fit most situations while not making it overly complicated to configure to the developer’s specific use case. This is a common struggle that all API designers face. Some languages coerce you to be more rigid in structure than others, but this is JavaScript and like Voltaire said, “With great power comes great responsibility.”

Here are some of the things that we wanted to generalize while developing the new component, which we decided to call LinkedComboContainer:

  • Data Retrieval — The options displayed in each ComboBox should be able to come in any format via a Reader (JSON, XML, Arrays, etc) and from any source via a Proxy (Ajax, JSON-P, Direct, etc). It should also be capable of loading all of the data up-front or loading the data on demand.
  • Number of Linked ComboBoxes — A developer should be able to configure any number of linked combos together. There should be no fixed number like 3 or 4.
  • Developers should be able to use their own subclass of ComboBox — We wanted to allow developers to use their own ComboBoxes. Our user extension should not require that a user subclasses yet another class just to use their own ComboBox.
  • Display of the LinkedCombos — Should the next active combo be progressively disclosed by showing the combo from a hidden state or should it be disclosed by enabling it from a disabled state? The user should be able to configure the way ComboBoxes are disclosed.
  • Support various box layout options — The LinkedComboContainer should be able to use any configuration of vbox or hbox layout.
  • Configurations of ComboBox — We wanted to try to make all of the configurations of ComboBox available for use. For example, we should be able to set the displayField, valueField, tpl, etc.

We’re going to explore in further detail how we supported multiple approaches to load the ComboBoxes, so we could fit most environments. This article assumes that you are familiar with Ext.define and the Basics of Creating Custom Components.

Independent Stores per ComboBox

The first approach to load data covers the case where developers need to go to several different endpoints. Data in each store can be of a different proxy, reader and model. Developers can define their own stores on each ComboBox they want to use, and the LinkedComboContainer handles loading the next store each time a selection is made.

Here is a small code snippet that works off of a live JSON-P API called CarQueryAPI to retrieve Makes, Models and Trims of cars from the year 2000.

var carQueryApiUrl = 'http://www.carqueryapi.com/api/0.3/?year=2000&sold_in_us=1&';
var makeStore = new Ext.data.Store({
        proxy: {
                type: 'jsonp',
                url: carQueryApiUrl + 'cmd=getMakes',
                reader: {
                        root: 'Makes',
                        type: 'json'
                }
        },
        fields: ['make_id','make_display']
});
 
var modelStore = new Ext.data.Store({
        proxy: {
                type: 'jsonp',
                url: carQueryApiUrl + 'cmd=getModels',
                reader: {
                        root: 'Models',
                        type: 'json'
                }
        },
        fields: ['model_name']
});
var trimStore = new Ext.data.Store({
        proxy: {
                type: 'jsonp',
                url: carQueryApiUrl + 'cmd=getTrims',
                reader: {
                        root: 'Trims',
                        type: 'json'
                }
        },
        fields: [{
                name: 'model_trim',
                convert: function(v, record) {
                        // Some vehicles only come in one trim and the API returns "".
                        // Lets provide a meaningful trim option.
                        return v || 'Standard';
                }
        }]
});
 
var carComboCt = Ext.create('Ext.ux.LinkedComboContainer', {
        // configs to be passed to all combos created.
        defaultComboConfig: {
                width: 350
        },
        comboConfigs: [{
                name: 'make',
                valueField: 'make_id',
                displayField: 'make_display',
                fieldLabel: 'Choose a Car',
                store: makeStore
        },{
                name: 'model',
                valueField: 'model_name',
                displayField: 'model_name',
                store: modelStore
        },{
                name: 'trim',
                displayField: 'model_trim',
                valueField: 'model_trim',
                store: trimStore
        }]
});

Each API call to CarQueryAPI returns a different set of data and a different model. This is a good example of where we need to use different stores for each ComboBox. Here is a live example of the data that will be retrieved when choosing Lotus and then Exige.

First we grab all makes that were in year 2000.

Then we get all models for year 2000 and make of Lotus.

Then we get all trims available for 2000 Lotus Exige.

A Single Master Store

The next approach allows the developer to load all of the data for the ComboBox on demand from the same endpoint. When a selection is made, the current field will be sent as well as the previous selections made in the chain. For example, if we always want to retrieve data from the same ajax endpoint: getOptions.php.

This code snippet shows how we would do it:

var masterMockStore = new Ext.data.Store({
        proxy: {
                type: 'ajax',
                url: 'getOptions.php',
                reader: {
                        type: 'array'
                }
        },
        fields: ['name','value']
});
var mockLinkedCombo = Ext.create('Ext.ux.LinkedComboContainer', {
        // master store to load all combos from
        store: masterMockStore,
        // configs to be passed to all combos created.
        defaultComboConfig: {
                displayField: 'name',
                valueField: 'value'
        },
        comboConfigs: [{
                name: 'parentOption',
                fieldLabel: 'Parent Option'
        },{
                name: 'childOption',
                fieldLabel: 'Child Option'
        }],
        listeners: {
                subselect: function(comboCt, value, combo) {
                        // console.log('Chose ' + combo.getValue() + ' from ' + combo.name);
                },
                select: function(comboCt, value, combo) {
                        Ext.Msg.alert("A Fine Selection!", Ext.encode(value));
                }
        }
});

Requests for this configuration would be:

getOptions.php?field=parentOption

This code would return all the parentOptions.

After making a selection, the request would be:

getOptions.php?field=childOption&parentOption=optB

This code would return all the childOptions that have a parentOption of optB. With each additional subselect (selecting one option in the chain of combos), a new request will be sent including all of the previously selected fields (until all have been completed).

Loading All the Data as a List — Up-front

For small sets of data, it is nice to load all data up-front and then have the LinkedComboContainer do all the filtering on the client-side. Loading the data as a list allows you to define one store and load the data once.

Data defined as a list looks like:

[{
        state: 'CA',
        county: 'San Mateo',
        city: 'San Mateo'
},{
        state: 'CA',
        county: 'San Mateo',
        city: 'Burlingame'
},{
        state: 'CA',
        county: 'Santa Clara',
        city: 'Palo Alto'
},{
        state: 'CA',
        county: 'Santa Clara',
        city: 'Sunnyvale'
},{
        state: 'MD',
        county: 'Frederick',
        city: 'Frederick'
},{
        state: 'MD',
        county: 'Frederick',
        city: 'Middletown'
},{
        state: 'MD',
        county: 'Carroll',
        city: 'Westminster'
},{
        state: 'MD',
        county: 'Carroll',
        city: 'Eldersburg'
}]

And here is an example using that flat-list:

var stateCountyStore = new Ext.data.Store({
        proxy: {
                type: 'ajax',
                url: 'aslist.json',
                reader: {
                        type: 'json'
                }
        },
        fields: ['state','county','city']
});
var stateCountyCombo = Ext.create('Ext.ux.LinkedComboContainer', {
        // master store to load all combos from
        store: stateCountyStore,
        loadMode: 'aslist',
        comboConfigs: [{
                name: 'state',
                displayField: 'state',
                valueField: 'state',
                fieldLabel: 'State'
        },{
                name: 'county',
                displayField: 'county',
                valueField: 'county',
                fieldLabel: 'County'
        },{
                name: 'city',
                displayField: 'city',
                valueField: 'city',
                fieldLabel: 'City'
        }]
});

This is probably the easiest way to get your data in a chain of ComboBoxes but it certainly hits the client-side the hardest. There are additional optimizations that could be made by caching resultsets when filtering. Take a look at getSubListRecords of LinkedComboContainer.js.

Loading Data as a Tree — Up-front

There is one last method of retrieving the data that we wanted to tackle with our user extension — loading data in a tree format. There are some pending enhancements to Ext JS that will make this drastically easier, so we’ll put that discussion off until then.

Implementing Flexible Data

Building a widget that can be so flexible with its data is a testament to the strength of the data package in our frameworks. There were really only a few tricky bits to get all of the functionality we wanted.

When using the single store configuration. the LinkedComboContainer will create a store per ComboBox by using the same model.

The LinkedComboContainer tracks whether or not the user has provided a store when creating the ComboBox by a internal flag `userProvidedStore`; this way it knows if it should load the data from the master store or directly load the store.

userProvidedStore: !!userComboConfig.store,

When creating new instances of stores, we use the same model from the master store. By using the modelName we are able to copy in explicit models defined by a class or even implicit models that are defined just by fields.

var store = new Ext.data.Store({
        proxy: {
                type: 'memory'
        },
        // we are re-using the model from the master store
        model: this.store.model.modelName
});

Achieving Our Original Goals

Take a look at the LinkedComboContainer.js code and you will notice that we did not create any additional store classes or custom ComboBox subclasses. We also did not use any overrides. This makes it very easy for developers to use their own custom ComboBoxes, Stores, Reader & Proxies as well as integrate the component into their code base. We did run into one small problem along the way: The LinkedComboContainer needed to know when a ComboBox trigger was clicked. By design, no events are exposed when the trigger is clicked. Individual subclasses of TriggerField typically implement onTriggerClick. Because of this, we had to hijack onTriggerClick of every ComboBox that was added to the LinkedComboContainer. This way regardless of the users implementation of onTriggerClick, the LinkedComboContainer code will still be run.

// no events are exposed onTriggerClick and we'd like developers to be able to use any subclass
// of combobox without subclassing a custom one, therefore we hijack triggerClick
combo.onTriggerClick = Ext.Function.createInterceptor(combo.onTriggerClick, this.onBeforeComboTriggerClick, this);

You’ll also see that onBeforeComboTriggerClick will now run in the context of the LinkedComboContainer (meaning this. will point to the LinkedComboContainer). In order to find out what combo generated the request, we need to check the .target property which is tagged on by createInterceptor.

onBeforeComboTriggerClick: function() {
        // grab the combo that generated the triggerClick via .target (tagged on by Ext.Function.createInterceptor)
        var combo = arguments.callee.target,
                comboIndex = this.combos.indexOf(combo);
 
        // the first combo is the only one that load is triggered by clicking on the combo trigger
        // all of the others are loaded once a selection has been made.
        if (comboIndex === 0 && combo.store.getCount() === 0) {
                this.doLoad(combo);
        }
},

Please note that arguments.callee is not available in strict mode of ES5. We chose to use it in this case rather than using something like named functions in order to maintain Internet Explorer support.

What’s Next

We hope that this article has helped provide insight into custom component development and some of the issues faced when designing an API. In the next article, we will take the LinkedComboContainer and wrap it up into an Architect User Extension for drag and drop configuration. Can you think of any new features that would be useful to add to the LinkedComboContainer? Have you coded something similar? Did you do it as a one-off implementation, or did you write a custom component? Let us know.