Fusioncube

The online journey of a technophile, by Steve Brownlee

New AjaxCFCProxy class for Sencha ExtJS

Friday
Apr 15,2011

Related Article(s): AjaxCFC ported to ExtJS

My integration of ExtJS, ajaxCFC and ColdFusion continues.

Note: This is for version 3.3.1. I’ve also been happily playing around with version 4, but until it’s officially released, I must continue building with 3.3.1.

Anyway, here’s what I was trying to solve. When I want to allow users to serach for items from a ComboBox, I configure a store, and inside the store, I configure an Ext.data.HttpProxy to hit a separate ColdFusion page that performs a query and returns the results.

Standard HttpProxy Method

var WidgetStore = new Ext.data.Store({
    proxy: new Ext.data.HttpProxy({
        url: 'liveQueries/widgets.cfm'
    }),
    reader: new Ext.data.JsonReader({
        root: 'data',
        totalProperty:'totalRecords'
    }, Widget)
});

ColdFusion ‘Live’ Query

<cfsetting enablecfoutputonly="true">

<cfparam name="query" default="">

<cfquery name="chargeCodes" datasource="#getDatasource().getName()#">
SELECT UNIQUE widget_seq_no, identifier, descr
FROM 	widgets
WHERE 	(REGEXP_LIKE(identifier,<cfqueryparam cfsqltype="cf_sql_varchar" value="^#query#">,'i')
   OR REGEXP_LIKE(descr,<cfqueryparam cfsqltype="cf_sql_varchar" value="^#query#">,'i'))
   AND widget_type_cd='SNAPPYDS'
ORDER BY identifier asc
</cfquery>

<cfscript>
jsonBean = createobject("component","ajaxCFC.JSON");
jsonEncodedCriteria = jsonBean.encode(data=chargeCodes, queryFormat="array");
writeOutput(jsonEncodedCriteria);
</coldfusion>

<cfsetting enablecfoutputonly="false">

I hate this, because I already am using my Ext.AjaxCFC class to query my ColdSpring beans asynchronously, so why do I have to set up these standalone ColdFusion pages to performs queries that should be executed in the component?

I decided to take matters into my own hands, and I have to give the ExtJS team props again, because it was easy to accomplish by extending the Ext.data.MemoryProxy class. As you can see, all I need to do is override the doRequest() method to execute Ext.AjaxCFC.request() with the data passed in during initialization.

Ext.ux.data.AjaxCFCProxy.js

Ext.ns('Ext.ux.data');

Ext.ux.data.AjaxCFCProxy = Ext.extend(Ext.data.MemoryProxy, {
   constructor : function(data){
      Ext.ux.data.AjaxCFCProxy.superclass.constructor.call(this);
      this.data = data;
   },
   doRequest : function(action, rs, params, reader, callback, scope, arg){
      this.data.data.query = params.query;
      Ext.AjaxCFC.request({
         bean : this.data.bean,
         method : this.data.method,
         data : this.data.data,
         success: function(rs) {
            var result = reader.readRecords(rs);
            callback.call(scope, result, arg, true);
         },
         error: function(results) {
            Ext.MessageBox.alert('Load Failed', 'Unable to load requested data');
         }
      });
   }
});

New and Improved Combobox for Searching

WidgetSearch = new Ext.form.ComboBox({
    minChars:           2,
    loadingText:        '',
    itemSelector:       'div.search-item',
    triggerClass:       'x-form-search-trigger',
    emptyText:          '',
    width:              300,
    listWidth:          0,
    store: new Ext.data.Store({
        // I've extended the base Ext.data.MemoryProxy()
        // class to use the AjaxCFC object for searching
        // instead of having to use a liveQuery. Much cleaner.
        proxy: new Ext.ux.data.AjaxCFCProxy({
            bean    : 'Widget',
            method  : 'search',
            data    : { source_table : 'WIDGET' }
        }),
Wednesday
Jul 21,2010

In an ExtJS ComboBox, it’s fairly easy to implement a search feature where given the user’s entry, you can return a list of possible matches. I outline this mechanism in Sencha ExtJS: Simple Autocomplete Example.

However, recent user feedback was, “If I know the unit I’m looking for, why can’t I just type it in and hit ENTER and skip the whole search feature to find the one I need?”

Doh.

Yes, I was making the users wait – even if for 1 second – for a list of possible matches to be displayed, when 90% of the time they know which one they want to open.

Luckily, I was able to briefly look at the documentation for ComboBox and find out how to accomplish this. You need to set the enableKeyEvents config property to true, and then hook into the keypress event and check for the user pressing the ENTER (or RETURN) key. When the ENTER key is pressed, prevent the query from being executed, and open the item the user entered.

var WidgetSearch = new Ext.form.ComboBox({
    minChars:           3,
    loadingText:        '',
    itemSelector:       'div.search-item',
    triggerClass:       'x-form-search-trigger',
    applyTo:            'search-field',
    enableKeyEvents:  true,
    store: new Ext.data.Store({
        proxy: new Ext.data.HttpProxy({url: 'widgets.cfm'}),
        reader: new Ext.data.JsonReader({
            root:           'data',
            totalProperty:  'recordcount'
        }, [
            {name: 'widget_number', type: 'string'},
            {name: 'widget_name', type: 'string'}
        ])
    }),
    tpl: new Ext.XTemplate(
        '<tpl for="."><div class="search-item">',
            '{widget_number} - {widget_name}',
        '</div></tpl>'
    ),
    onSelect: function(record){
        document.location.href = 'displayWidgetDetails.cfm&amp;wid=' + record.data.widget_number;
    },
    listeners:
    {
        // Hook into the keypress event to detect if the user pressed the ENTER key
        keypress: function(comboBox, e){
            if (e.getCharCode() == e.ENTER) {
                // Prevent the default query action since the user
                // believes she has entered a proper widget number
                comboBox.on('beforequery', function(q){q.cancel=true;},this);

                // Redirect browser to widget detail view
                document.location.href = 'displayWidgetDetails.cfm&amp;wid=' + WidgetSearch.getValue();
            }
        }
    }
});
Tuesday
Jul 20,2010

Man this took a long time to figure out.

Ok, inside a Sencha ExtJS DataGrid, I have assigned a ComboBox as the editor for one of the columns populated with its own Store of items. I’ve got a ColdFusion Component returning a query object, which is then converted into JSON with the ajaxCFC class.

var ItemCode = Ext.data.Record.create([
       {name: 'item_id', type: 'int'}
       {name: 'item_code', type: 'string'}
]);

var ItemCodesStore = new Ext.data.Store({
    autoLoad:false,
    proxy: new Ext.data.MemoryProxy(),
    reader: new Ext.data.JsonReader({ root: 'data'; }, ItemCode)
});

// Snippet from ColumnModel of DataGrid
header: 'Code',
dataIndex: 'existingCode',
width: 70,
editor: new Ext.form.ComboBox({
   mode: 'local',
   displayField: 'item_code',
   valueField: 'item_id',
   store: ItemCodesStore
})

This is straightforward code. The column represents the existingCode value in the DataGrid’s store, and when the user clicks on an individual cell, a ComboBox is rendered with possible values stored in ItemCodesStore.

My problem is that I want the item_code value displayed at all times. By default, when a user selects an item from the ComboBox, the item_id value will be displayed, because even though I have set displayField:’item_code’ in the ComboBox, the DataGrid has no knowledge of this, and always shows the valueField.

What I had to do was write a custom renderer for that column that does a search in the ItemCodes store for a matching item_id and return the item_code to be rendered instead.

function CodeRenderer(val){
    var matching = ItemCodesStore.queryBy(
                      function(rec,id){
                         return rec.item_id == val;
                      });
    return (matching.items[0]) ? matching.items[0].data.item_code : '';
};

// Snippet from ColumnModel of DataGrid
header: 'Code',
dataIndex: 'existingCode',
width: 70,
renderer: CodeRenderer,
editor: new Ext.form.ComboBox({
   mode: 'local',
   displayField: 'item_code',
   valueField: 'item_id',
   store: ItemCodesStore
})

Replacing data in Sencha ExtJS grid store

Thursday
May 20,2010

I’ve got a data grid in one of my applications that is paginated because the underlying data set is over 9700 records in size. As I’ve said in previous articles, pagination is an annoying design pattern, but a necessary evil nonetheless.

To make it easier on my users, I put a Ext.form.ComboBox in the toolbar of the grid which can be used by the users to search for records. The problem I was having was twofold:

  1. The ComboBox has one Ext.data.Store and the data grid has another one that is used to display the default, first 100 records in the table.
  2. The ComboBox displays its results in a list beneath itself by default, but I wanted the results displayed in the grid.

The second problem was easy enough to solve. When the list is shown it raises the expand event, so I capture that and immediately close it again with the collapse() method.

However, having the data grid, in essence, be able to display the contents of two different Stores was a bit more problematic since Ext.grid.GridPanel doesn’t have a setStore() method (which, I suppose, I could have implemented). My solution was to copy the contents of the search results into the store used by the GridPanel. This way, the user can still page the entire data set if she so wishes, but search results will override the Store when it is done.

// Create the data store for all things found with the search bar with each row represented by a Thing
var ProviderSearchStore = new Ext.data.Store({
    proxy: new Ext.data.HttpProxy({url: 'liveQueries/searchForStuff.cfm'}),
    reader: new Ext.data.JsonReader({ root:'data',totalProperty:'recordcount'}, Thing)
});

// Since the grid is using the ProviderStore, when the store for the
// search feature is populated, clear out the grid's store and populate
// it with the records retrieved in the search
SearchStore.on('load',
    function()
    {
        var allRecords = SearchStore.getRange();
        DefaultStore.removeAll();
        DefaultStore.add(allRecords);
    });

/*
 * When the list of results is shown, immediately collapse it since we're
 * showing the results in the grid
 */
combo.on('expand', function(){ combo.collapse(); });

// Create the data store for all insurers with each row represented by an Insurer
var DefaultStore = new Ext.data.Store({
    proxy: new Ext.data.HttpProxy({url: 'liveQueries/getStuff.cfm'}),
    reader: new Ext.data.JsonReader({ root:'data', totalProperty:'totalRecords' }, Thing)
});

// Create the grid to show insurers
var grid = new Ext.grid.GridPanel({
    store: DefaultStore,
    renderTo: 'grid-div',
    title:'Stuff',
    frame:true,
	bbar: PagingBar,
	tbar:[
            {text:'Search for insurer'},
            new Ext.Toolbar.Spacer(),
	    ProviderSearch
    ]
});

Here’s what the final control looks like. As you see, I searched for ‘ZA’, which returned only one result, which is now shown in the grid. Below, you can see that the original data set is still being stored in the paging toolbar’s data store, so just by clicking the refresh button, the first 100 rows in the table will be displayed again.

image

Sencha ExtJS: TwinTrigger Autocomplete Example

  • Filed under: ajax, Ext
Tuesday
Jun 10,2008

In addition to the basic ComboBox functionality available in the Ext library, there is a poorly-documented extension called the TwinTriggerField. This is simply a standard ComboBox with two control icons on the right which you can customize.

This article is an extension of my Ext: Simple Autocomplete Example article, so please read that one as well to get all related code. This article will simply show the additional code needed to make a TwinTrigger work.

By default the ComboBox will have the down arrow which allows users to see the available elements, but you can add another icon to the right of that which, when clicked, can perform any function that you like.

The TwinTrigger Class

Start off by making a new Javascript file in your project named Ext.TwinTrigger.js and paste the following code into it. We’ll go through the code later to show what’s going on.

Ext.form.TwinTriggerField = function(config) {
    Ext.form.TwinTriggerField.superclass.constructor.apply(this, arguments);
};
Ext.extend(Ext.form.TwinTriggerField, Ext.form.ComboBox, {

    trigger1Class: 'x-form-search-trigger',
    trigger2Class: 'x-form-select-trigger',

    initComponent : function(){
        Ext.form.TwinTriggerField.superclass.initComponent.call(this);
        this.record = new Object();
        this.triggerConfig = {
            tag:'span', cls:'x-form-twin-triggers', cn:[
            {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger1Class},
            {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger2Class}
        ]};
    },
    getTrigger : function(index){
        return this.triggers[index];
    },
    initTrigger : function(){
        var ts = this.trigger.select('.x-form-trigger', true);
        var triggerField = this;
        ts.each(function(t, all, index){
            t.hide = function(){
                var w = triggerField.wrap.getWidth();
                this.dom.style.display = 'none';
                triggerField.el.setWidth(w-triggerField.trigger.getWidth());
            };
            t.show = function(){
                var w = triggerField.wrap.getWidth();
                this.dom.style.display = '';
                triggerField.el.setWidth(w-triggerField.trigger.getWidth());
            };
            var triggerIndex = 'Trigger'+(index+1);

            if(this['hide'+triggerIndex]){
                t.dom.style.display = 'none';
            }
            t.on("click", this['on'+triggerIndex+'Click'], this, {preventDefault:true});
            t.addClassOnOver('x-form-trigger-over');
            t.addClassOnClick('x-form-trigger-click');
        }, this);
        this.triggers = ts.elements;
    },
    onTrigger1Click : function() {
        this.onTriggerClick();
    },
    onTrigger2Click : function() {
        this.onTrigger2Click();
    }
});

The User’s View

Then include your file in an HTML page.

<html>
<head>
    <link href="css/ext-all.css" rel="stylesheet" type="text/css">

    <script type="text/javascript" src="js/ext-all.js"></script>
    <script type="text/javascript" src="js/Ext.TwinTrigger.js"></script>
    <script type="text/javascript" src="js/interaction.example.js"></script>
</head>

<body>
    <input type="text" size="20" id="facilitySearchField">
</body>
</html>

The Interaction Layer

And in your interaction layer, create an instance of your TwinTrigger field. In this example, you’ll see I’m using the facilityStore object that I set up in the previous article.

var search = new Ext.form.TwinTriggerField({
    applyTo:'divName',
    displayField:'name',
    store: facilityStore,
    minChars:4,
    forceSelection:true,
    width: 210,
    listWidth:350,
    onSelect: function(record){    },
    onTrigger2Click: function(){    }
});

Custom Style for Second Trigger

If you want to have a custom icon for the 2nd trigger, you’ll have to do two things.

First, define a custom CSS class and specify it in the Ext.TwinTrigger.js file. You can name this class anything you like, but try to keep it consistent with Ext’s naming conventions. You can see the one that I chose in my code above.

trigger2Class: 'x-form-select-trigger',

Second, modify the ext-all.css style sheet and specify the image that you’d like to use for your new class.

.x-form-field-wrap .x-form-select-trigger{background-image:url(../images/default/form/select-trigger.gif);cursor:pointer;}

The Guts

Now that I’ve laid all the code out, I show you the code to focus on. The important code in your TwinTrigger class is…

onTrigger2Click : function() {
    this.onTrigger2Click();
}

What this does is expose a new event that you can handle in your interaction layer.

onTrigger2Click: function(){
    // Do something wonderful when the user clicks the 2nd trigger icon
}

Those are the basics for having two trigger icons for a ComboBox. Like I said, refer to my previous article on how to get the basics of a autocomplete ComboBox working, and then implement this code if you need it.

Comments and questions, as always, are welcome.

About Steve

I am a technologist, and have been ever since 1980 when I got my very first TRS-80 and programmed it to do my math homework. I love to share the gift of technology with others and show them the wonderful things it can do for them, and how they should not fear it, but embrace it.

Latest Tweets

  • Ok... stayed up way too late trying out website designs for my wife's new nonprofit. The kids will be getting me u... — http://t.co/QrKh5iBI
  • Am I the only one who likes Google’s new privacy policy? http://t.co/qwcym5wH
  • All that time wasted learning the .NET framework - Fusioncube - http://t.co/krANoWmg
  • @marcesher libraries like Less only do what you tell them. You can make a mixin to do that, but it doesn't assume anything (which is good)
  • Circus about to start. Girls are so excited they can't stand it!!!! (with Sabrina and Tessa at @BrdgstoneArena) [pic] — http://t.co/PXwi5emj

Subscribe

Entries (RSS)
Comments (RSS)