Sencha Inc. | HTML5 Apps

Blog

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

December 10, 2013 | Aaron Conran

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.

There are 8 responses. Add yours.

Wayne Rudd

1 year ago

So you can’t use Architect to make an User Extension to use in Architect? Really?

Aaron Conran Sencha Employee

1 year ago

@Wayne Rudd - Of course you can! Go to the + in the Inspector and add a JavaScript Resource and then implement the User Extension. You can then use it in your project via createAlias. Our next blog entry is going to go into how to wrap it up as a .aux file so that it renders in the canvas shows the new configurations like `disclosureMode` and `loadMode` directly in the configuration panel.

Wayne Rudd

1 year ago

My point was: your appear to be forced to hand code the extension to be able to use it in Architect? You can’t build the extension in Architect itself? So not really viable if your workflow is actually centred around Architect…

Aaron Conran Sencha Employee

1 year ago

@Wayne - You could in fact. Architect is great at building applications, that’s what we’ve created it for, to take the individual legos/building blocks assemble them together and get a final product. Architect is not focused on building the individual legos/building blocks or custom components as much.

I believe that different environments are better suited to application developers vs framework/user component developers. Make sense?

Tom Coulton

12 months ago

Merry Christmas! We’ve translated this blog article into Japanese here: http://www.xenophy.com/sencha-blog/9814

Also, this is the Sencha Japan User Group: http://www.meetup.com/Japan-Sencha-User-Group/

Sudhir

12 months ago

Thanks for the article, waiting for second entry and expecting some hints for my following open source project. Please read the readme.md to know what I am trying to do.

https://github.com/sdharmadhikari/addon-spring-roo-sencha-touch

Here is my question. After above mentioned addon generates Sencha Touch code, I am wondering how can I let developers import the generated code into Architect for customization. If that can be done, it would be fantastic and very productive.
The generated code will be exactly as architect would have created, because I am using Architect 3.0 for creating code templates. I am wondering if I can geneate xds/metadata as well.

Other option is to move away from MVC and just create ST component for each backend entity including embedded event-handling and follow your blog entry to make each entity “importable” into architect. But I would not prefer that because MVC is preferred pattern to use.

Steve Sobel

12 months ago

@Sudhir

User extensions don’t generate code / metadata themselves, but are rather loaded and used by the code generated by Architect.  You’d definitely be able to allow users to import the User Extension code for re-use!

Hopefully, when our next blog post is released (very soon!) it will shed some light on how User Extensions are authored and how they work within Architect.  In the meantime, please feel free to visit the Market page at https://market.sencha.com/extensions and try some of them out!

Sudhir

12 months ago

Thanks for response. Does that mean any MVC sencha application written outside of Architect can be authored as User Extension and can be imported into Architect ? If its is, it’s big news for me smile As per Aaron it was very difficult because of dynamic nature of Javascript.

Also, All these extension you pointed out, AFAIK, all are created by Architect and thats why they are importable into Architect.

Comments are Gravatar enabled. Your email address will not be shown.

Commenting is not available in this channel entry.