Category Archives: Ext JS 4

Ext JS: Generating a Checkbox Group from a Store

Ext JS checkbox groups enable you to group checkboxes into a single logical field. Since these checkboxes are often times dynamically generated from a store, I thought that it might make sense to extend the class with some store binding. Here’s my first, lightly tested attempt:

 Ext.define('Ext.ux.CheckboxStoreGroup', {
    extend: 'Ext.form.CheckboxGroup',
    alias: 'widget.checkboxstoregroup',
    config: {
        store: null,
        labelField: 'label',
        valueField: 'id',
        checkedField: 'checked',
        columns: 3,
        boxFieldName: 'mycheckbox'
    },
    applyStore: function(store) {
        if (Ext.isString(store)) {
            return Ext.getStore(store);
        } else {
            return store;
        }
    },
    updateStore: function(newStore, oldStore) {
        if (oldStore) {
            store.removeEventListener('datachanged', this.onStoreChange, this)
        }
        newStore.on('datachanged', this.onStoreChange, this);
    },
    onStoreChange: function(s) {
        
        Ext.suspendLayouts();
        this.removeAll();
        
        var vField = this.getValueField();
        var lField = this.getLabelField();
        var cField = this.getCheckedField();
        var fName = this.getBoxFieldName();
        var rec = null;
        
        for (var i=0; i<s.getCount(); i++) {
            rec = s.getAt(i);
           
            this.add({
                xtype: 'checkbox',
                inputValue: rec.get(vField),
                boxLabel: rec.get(lField),
                checked: rec.get(cField),
                name: fName
            });
        }
        
        Ext.resumeLayouts(true);
        
    }, 
    initComponent: function() {
        this.callParent(arguments);
        this.on('afterrender', this.onAfterRender);
    },
    onAfterRender: function() {   
        if (this.getStore().totalCount) {
            this.onStoreChange(this.getStore);
        }
    }
});

You can test and play around with the code here:
https://fiddle.sencha.com/#fiddle/i51

Case Study: NACCHO Model Practices

The National Association of City and County Health Officials engaged Fig Leaf Software to develop a dynamic form/workflow/collaboration application using web standards. We used Sencha’s Ext JS 4 javascript framework for the front-end development and Microsoft .NET/SQL Services on the back-end to create a 3-tiered REST-based architecture that positions them for future growth.

The Challenge

NACCHO’s Model Practices Program honors and recognizes outstanding local health practices from across the nation and shares and promotes these practices among local health departments (LHDs). Model and promising practices cut across all areas of local public health, including, but not limited to, community health, environmental health, emergency preparedness, infrastructure, governmental public health, and chronic disease.

Once practices are designated as model or promising, they are stored in the Model Practices Database so all LHDs can benefit from them. NACCHO began accepting Model Practices submissions in 2003. Since then, NACCHO has placed numerous model and promising practices in the searchable online Model Practice Database with more added each year.

Since 2003, the collection and review of submissions had largely been a manual process. LHD’s entered data through a web form which was ultimately downloaded into either a Microsoft Excel spreadsheet or Microsoft Access database. Submissions went through a two-stage content review and collaboration process, often involving four or more reviewers whose actions were coordinated by NACCHO personnel and tracked manually through a series of excel spreadsheets. Due to growth of the program, this labor-intensive process was deemed to be unsustainable and Fig Leaf Software was called in to design and implement a workflow system that would automate the submission, review, collaboration, and publication cycle.

Here’s a flow chart that we created as part of our specification process that models the review cycle:

Workflow process model

Workflow process model (click to enlarge)

3-tier

Figure 1: 3-Tier Architecture

3-Tiered Architecture

A 3-tiered architecture segments your app into three distinct service layers:

  1. The User-Interface
    In the pre-smartphone days, organizations could settle on creating a single front-end that only supported desktop browsers. With the proliferation of mobile devices of all shapes, sizes, and capabilities, corporate IT must now consider developing multiple front-ends for their apps. At an absolute minimum, line-of-business apps should support desktop and tablet with reduced functionality available for phones. This requirement frequently requires that multiple front-end apps be developed in parallel, each accessing a common REST-based webservices api implemented at the business intelligence tier.We chose to develop the front-end using Sencha’s frameworks – Ext JS 4 and Sencha Touch for the following reasons:

    1. The toolkits are based on web-standards (javascript & html5)
    2. Sencha Toolkits use a well-defined client-side MVC architecture, helping to ensure coding standards among developer team members thereby leading to reduced future maintenance costs.
    3. Both frameworks have good tooling (Sencha Architect and Sencha Cmd)
    4. Consistency in the Desktop and Phone API’s means that we can rapidly develop a mobile phone GUI by repurposing the data model classes from the desktop GUI.
    5. Flexibility to upgrade in the future to the recently released Ext JS 5, which will enable us to support both desktop and tablet GUI’s from a single codebase.
    6. Ext JS 4 has full backwards compatibility with IE8 – an important consideration when we evaluated NACCHO’s target audience of municipal health departments.
      .
  2. The Business Intelligence Tier
    This tier marshals resources from  “back-office” resources – enterprise databases, CRM, mail servers, and more. NACCHO’s I.T. group is in the process of migrating from Adobe ColdFusion to a Microsoft .NET platform as their corporate standard. We honored their preferences by creating a rich REST-based webservices API that could be invoked from virtually any client-side technology that can parse data in JavaScript Object Notation (JSON) format. This architecture gives NACCHO the flexibility to publish their API so that third-parties could easily develop a custom front-end or mashup that leverages the model-practices data. At Fig Leaf, we strongly believe in “open government” and actively seek opportunities to make .gov data resources available to other developers.
    .
  3. The Database Tier
    NACCHO’s corporate standard is Microsoft SQL Server. We designed an efficient, normalized 20-table schema with referential integrity rules that enforce valid data input. Due to the dynamic nature of the application, MongoDB might have actually been a better choice from a development perspective – but since it wasn’t a corporate standard and we had already chosen .NET as the middleware, we went with “old reliable.” We use Microsoft Full Text indexing to drive the front-end keyword search.

Organizing Around Perspectives

NACCHO Model Practices has four different user roles:

  1. Casual browsers who want to search and retrieve Practices.
  2. Applicants who are submitting Practices for review
  3. Internal Reviewers on the NACCHO staff who parcel out Practices for review
  4. External Reviewers who review and comment on Practices as well as collaborate on forming an opinion as to whether a submission is a Model Practice, Promising Practice, or Neither
  5. Administrators who perform an initial review of submitted practices, create the submission form, create and run ad-hoc reports, and manage the overall review cycle.

To address the very different roles and responsibilities of the stakeholders, we organized the application around a series of “Perspectives”

The Browser Perspective

The Browser Perspective, as illustrated by Figure 2, enables users to easily apply filter criteria to search through NACCHO’s Model Practice database. The Ext JS 4 data grid automatically downloads records in the background as the user scrolls, reusing DOM elements on-the-fly to keep memory overhead at manageable levels. Search filters are applied automatically after a user stops typing in a field.

mpie8-1

Figure 2: The Browse Perspective running in Satan’s favorite browser (IE 8)

Users can resize and rearrange grid columns. In addition, all of the sections of the layout can be expanded or collapsed in order to maximize available space. These settings persist between a user’s sessions, enabling them to create a personalized interface that only shows the information that they find to be helpful.

The Applicant Perspective

The Applicant Perspective, as illustrated in Figure 3, enables logged-in users to edit, save, and submit a Model Practice for review. Ext JS’ rich form field widgets, customizable validation, and flexible layouts enable us to dynamically assemble the form at runtime based on instructions that are read from the server. We also implemented a “Print” feature that redraws the form in a printer-friendly format with hard page-breaks that separate each section.

Figure 3: The Applicant Perspective

Figure 3: The Applicant Perspective. Yes, this works in IE 8 too!

In order to facilitate the editing of large blocks of text, we created an Ext JS extension that integrates the best-in-class TinyMCE 4 WYSIWYG editor and added a previous/next buttons at the bottom of the screen to help users navigate through the different tabbed-based sections.

The Administrator Perspective

The Administrator Perspective, depicted in figure 4, uses roles-based security to restrict access to administrator/reviewer functionality by role.

“Super Admins”, of course, have full run of the system but are primarily responsible for the following tasks:

  1. Reviewing the initial submissions and assigning them to an “internal reviewer”, a subject matter expert within NACCHO, who reads through the practice and decides whether it has been properly categorized.
  2. Designing forms
  3. Creating and running reports
  4. Auditing the review cycle
  5. Managing accounts
Assigning an initial reviewer

Figure 4: Assigning an initial reviewer

Using the Form Builder Perspective

Since NACCHO’s survey form changes from year-to-year, Fig Leaf Software designed the Model Practices application to enable non-technical admins to customize their forms without involving I.T. Form fields can be grouped into tab panels and we support collecting data via text input fields, wysiwyg editor (TinyMCE 4), select boxes, checkboxes, and radio buttons. Admins can set data validation rules to require input on select fields, restrict text input by word count, and more!

Admins can build custom forms without involving I.T.

Figure 5: Admins can build custom forms without involving I.T.

Running Reports

We’ve implemented several query-by-example reporting tools into the Model Practices application. Admins can quickly identify the status of practices in workflows and run statistical roll-ups on approved documents. Using Sencha Ext JS 4, we were able to easily  present data in a scalable grid and display aggregate statistics in a native web chart. We also developed a custom extension that allows users to export the information in any grid to Microsoft Excel, as illustrated in figure 7.

Figure 7: Executing reports, charting the results, and exporting to Microsoft Excel

Figure 7: Executing reports, charting the results, and exporting to Microsoft Excel

Query EVERYTHING!

In rare cases, query-by-example interfaces might not be sufficient to enable administrators to extract the information that they require. To handle any reporting criteria that might come up in the future, we implemented the query builder, depicted in figure 8, that enables admins to create a dynamic filter for every field on any form. We’ll be posting the Query Builder code to GitHub before the end of the year.

The Query Builder enables admins to create and save custom reports.

Figure 8: The Query Builder enables admins to create and save custom reports.

 The Internal Reviewer Perspective

As illustrated in Figure 9, the Internal Reviewer’s job is to read through the submitted document and assign subject-matter experts (external reviewers) who will grade and judge the responses. If an Internal Reviewer decides that the submission has been incorrectly classified, they can reclassify it forward it back to the Administrator who, in turn, can pass it on to a different internal reviewer. This view uses a drag & drop, searchable grids that facilitate the assigning of external reviewers. We used a third-party extension, Ext.ux.grid.Filterbar, to define the “filter row” depicted in the “Search for Reviewers” grid control.

Internal reviewers read through the submissions and assign them to external reviewers for "grading"

Figure 9: Internal reviewers read through the submissions and assign them to external reviewers for “grading”

The External Reviewer Perspective

External reviewers are charged with reviewing the application and answering a series of targeted review questions that were defined in the Form Builder perspective. Responses that don’t meet the designated data validation criteria are denoted by a red [X] in the left-side tree control. Once they have completed commenting on applicant responses, they designate the application as being a “Model Practice,” “Promising Practice,” or “Neither.”

Figure X: Ranking and reviewing applicant responses

Figure 10: Ranking and reviewing applicant responses

The Reconciliation Perspective

If two or more external reviewers disagree as to whether an application is a “Promising Practice” or “Model Practice”, they are directed into the perspective, depicted in figure 11, where each reviewer can see all of the other reviewer’s responses and comments. They can also schedule a conference call from directly within the GUI to take place via VOIP (implemented by integrating the Twilio API) where they can hash out their differences. All external reviewers must ultimately reach consensus as to whether a submission is a “Model Practice”, “Promising Practice”, or “Neither”.

Figure: Comparing other reviewer's responses, scheduling a conversation, using VOIP

Figure 11: Comparing other reviewer’s responses, scheduling a conversation, using VOIP

Once the external reviewers reach consensus the applicants are notified via email of it’s final disposition and, if rated as a “Model Practice”, or “Promising Practice”, the application becomes accessible to the public on the web site.

Built using Sencha Architect

Sencha Architect, depicted in figure 12, enabled our development team to respond to changes in our customer’s requirements with agility as well as rapidly prototype and visualize new features. Using it’s deep integration with Sencha Cmd made it easy for us to create and post development, testing, and production builds.

Sencha Architect's visual designer enabled our development team to act with agility.

Figure 12: Sencha Architect’s visual designer enabled our development team to act with agility.

Futures – Mobile and More!

Model Practice Mobile Prototype

Model Practice Mobile Prototype

Using Ext JS for the project paid off handsomely when the customer asked us to develop a level-of-effort (LOE) for porting the search perspective over to a mobile-phone form factor. Using Sencha Architect and relying on the REST-based api that we produced during the desktop app development phase allowed us to create a quick proof-of-concept using Sencha’s Touch framework.

Would you like to know more?

Please contact us at info@figleaf.com to find out more about our custom application development services and how we can help you realize your visions of productivity enhancements across your enterprise in a cost-effective manner!

Upgrading to Ext JS 5 in five easy steps!

Historically, the upgrade path between different versions of Sencha Ext JS has been challenging for developers and product managers alike. Much to the chagrin of project managers everywhere, migrating apps from Ext JS 3 to Ext JS 4 typically required major refactoring, frequently necessitating a complete rewrite. The complaints of the IT community were clearly heard loud and clear by Sencha which, I’m happy to report, has actually made the Ext 4 to Ext 5 upgrade path nearly seamless.

Top 5 reasons for upgrading to Ext JS 5

  1. New Components
    Ext 5 adds support for a drag & drop dashboard component, an improved combo box with tag selector, and grid widgets.

    gridwidgets

    Ext JS 5 Grid Widgets

  2. A new charting package based on SVG and Canvas
    The new charting engine is going to be shared with Sencha Touch. It’s a lot simpler for developers to add additional sprites to the drawing layer and floating axis and more complex user interactions are now supported. Sencha has also cleaned up some of the chart config properties (e.g. axis types are now all lower-case) to make it more consistent across the framework. Since the charts are now SVG/Canvas based, client-side methods have been added that enable a user to download the generated chart as a bitmap. You won’t have to rely on the Sencha.io cloud service anymore to convert vectors into bitmaps.

    chart

    Ext JS 5 chart with added custom sprites

  3. Viewmodels and Bi-Directional Data Binding
    You now have the option of using an Model-View-ViewModel (MVVM) architecture instead of the Ext 4.x MVC framework. MVVM enables you to use views expressly for layout while compartmentalizing event handling into a separate, view-specific file. ViewModels enable you to easily implement bi-directional databinding with your views, which significantly cuts down on the amount of code that you’ll need to write (and debug!).

    Combining MVVM with MVC

    Combining MVVM with MVC

  4. Chained Stores and other Improvements to the Data Package
    Most apps are going to require multiple views of the same data store. For instance, you might want to simultaneously output two grids that refer to the same core dataset but have different client-side filters applied to them. In Ext 4 this typically required the instantiation of two separate stores and the duplication of records in memory. Chained stores are analogous database table views.  They link back to a single  “source” store containing your records. The chained store’s  filters, sorters and groupers are defined independently of the data “source” Any updates to the source store automatically triggers a refresh in its associated chained stores.
  5. Tablet Compatibility
    I saved the best for last. Ext JS 5 now supports touch gestures (pinch, rotate, longpress, etc) and comes bundled with two touch-compatible themes – “Crisp” and “Neptune Touch.”  You’ll likely need to make some slight changes to your layouts after applying these themes, however, in most cases you should be able to support both desktop and tablet browsers from a single codebase.

    tablet

    Using the “Crisp” touch-compatible theme

Upgrading to Ext JS 5 in Five Easy Steps

During this tutorial, you’ll upgrade the Congressional Spending Portal – an app that I had initially developed using Sencha Architect in Ext JS 4.1.

Upgrade this app from Ext 4 to Ext 5 in five easy steps!

Upgrade this app from Ext 4 to Ext 5 in five easy steps!Download the tutorial assets

Before you begin

Step 1: Create a Sencha Command Project

Open a command prompt to /extjs5upgrade/ and enter the following statement:

sencha generate app -ext SpendingPortal spendingportal

Note that Sencha Command will automatically download and install the latest version of Ext 5 from the CDN! Cool!

Step 2: Copy the Ext 4 classes into the Ext 5 project

  1. Delete the controller, model, store, and view folders from /extjs5upgrade/spendingportal/app
  2. Copy /extjs5upgrade/before/app/*.* to /extjs5upgrade/spendingportal/app
  3. Copy /extjs5upgrade/before/app.js to /extjs5upgrade/spendingportal/app/Application.js

Step 3: Tweak the Application.js file

Refactor the /extjs5upgrade/spendingportal/app/Application.js file to resemble the following:

Ext.define('SpendingPortal.Application', {
    extend: 'Ext.app.Application',
    
    name: 'SpendingPortal',
   
    controllers: [
        'Main',
        'Sponsors',
        'Earmarks',
        'Feedback'
    ],

    launch: function () {
        // TODO - Launch the application

        var pnl = Ext.ComponentQuery.query('#centerpanel')[0];

        Ext.widget('sponsors', {
            constrainTo: pnl.getEl(),
            x: 5,
            y: 20
        });

        Ext.widget('earmarksviewer', {
            constrainTo: pnl.getEl(),
            x: 500,
            y: 20
        });

        Ext.widget('sponsorchart', {
            constrainTo: pnl.getEl(),
            x: 500,
            y: 300
        });
    }
});

Tweak the /extjs5upgrade/spendingportal/app.js file to set autoCreateViewport to true.

Ext.application({
    name: 'SpendingPortal',
    extend: 'SpendingPortal.Application',
    autoCreateViewport: true
});

Step 4: Invoke Ext 4 Compatibility mode

Open the app/app.json file and add the following code to enable “compatibility” mode:

"compatibility" : {
        "ext" : "4.2"
}

Modify the requires[] property in the app/app.json file to load the Ext 4 charting package:

"requires": [
  "ext-charts"
],

Step 5: Add Tablet Support

Modify the theme property in the /extjs5upgrade/spendingportal/app/app.json file to resemble the following:

"theme": "ext-theme-crisp"

Test the App!

Return to your command prompt and change directories to /extjs5upgrade/spendingportal. Then issue the following command to update the bootstrap.json file which indicates the classes to be loaded:

sencha app refresh

Then tell Sencha Command to create a build and make it accessible via http (Sencha Command has a built-in webserver):

sencha app watch

Test the app in your browser by accessing the URL generated by Sencha command (typically http://localhost:1841).

Now wasn’t that easy?

Congrats! Now that you’ve gotten the app up and running on Ext 5, you can start to improve upon it by invoking the broad range of new features (including Sencha Charts!) that have been added to the framework. Check back here often to learn about new Ext 5 tips, traps, and techniques. And consider engaging Fig Leaf Software for your Sencha consulting and training needs!

Ext JS 4 Fundamentals: Editing Database Tables with the Grid

Every app has “lookup” tables – those pesky little things that associate labels with numeric, autoincrementing primary keys. And those tables typically need to be editable by system administrators in order to maintain the flexibility of the system of which they are a part.

Using the following template-based approach. to quickly build these interfaces by leveraging the Ext JS 4 grid control as illustrated below:

Image

 

The first step in building this GUI is to define a data model and proxy. The model specifies the names of the fields that will be available as a JSON or XML feed. The proxy specifies the url from where the data feed will be generated. Most lookup table models will only specify two or three fields. Ext JS 4 considers the field named “id” to contain the primary key of the record. At Fig Leaf Software, we prefer to use a RESTful data services implementation. Ext automatically transmits a GET/POST/PUT/DELETE request header based on the type of operations that we’re going to perform, which can be a big time saver. All application servers (including Adobe ColdFusion) support the creation of REST-based services.

Ext.define('MyApp.model.PracticeCategory', {
    extend: 'Ext.data.Model',

    requires: [
        'Ext.data.Field',
        'Ext.data.proxy.Rest'
    ],

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'label',
            type: 'string'
        },
        {
            name: 'isActive',
            type: 'boolean'
        }
    ],

    proxy: {
        type: 'rest',
        url: 'http://[some domain]/[some app name]/category'
    }
});

The next step is to define a data Store which will hold multiple data model instances/records. This part is usually pretty boilerplate since most lookup tables typically contain fewer than 100 records. If you’re expecting more data points, you might want to look at setting the remoteFilter and remoteSort properties, as well as implementing data pagination.

Ext.define('MyApp.store.PracticeCategories', {
    extend: 'Ext.data.Store',

    requires: [
        'MyApp.model.PracticeCategory'
    ],

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            autoLoad: true,
            model: 'MyApp.model.PracticeCategory',
        }, cfg)]);
    }
});

Now that the data package plumbing has been completed, we’re ready to generate the editing GUI. In order to compartmentalize everything in a nice, neat package, I’ve opted to merge the event handling with the view. Some might argue that they’d prefer putting the event handling into a view controller, but for this specific limited use-case, I find that punching everything into a single file is slightly easier to maintain. And since it’s my code, it’s my rules.

Ext.define('MyApp.view.admin.PracticeCategoryEditor', {
    extend: 'Ext.window.Window',
    alias: 'widget.practicecategoryeditor',

    requires: [
        'Ext.grid.Panel',
        'Ext.form.field.Text',
        'Ext.grid.column.CheckColumn',
        'Ext.form.field.Checkbox',
        'Ext.grid.View',
        'Ext.grid.plugin.RowEditing',
        'Ext.panel.Tool'
    ],

    autoShow: true,
    height: 250,
    width: 400,
    layout: 'fit',
    title: 'Practice Categories',

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            items: [
                {
                    xtype: 'gridpanel',
                    header: false,
                    store: 'PracticeCategories',
                    columns: [
                        {
                            xtype: 'gridcolumn',
                            dataIndex: 'label',
                            text: 'Practice Category',
                            flex: 1,
                            editor: {
                                xtype: 'textfield',
                                allowBlank: false
                            }
                        },
                        {
                            xtype: 'checkcolumn',
                            width: 80,
                            dataIndex: 'isActive',
                            text: 'Active',
                            editor: {
                                xtype: 'checkboxfield',
                                inputValue: 'true',
                                uncheckedValue: 'false',
                                listeners: {
                                    change: {
                                        fn: me.onCheckboxfieldChange,
                                        scope: me
                                    }
                                }
                            },
                            listeners: {
                                beforecheckchange: {
                                    fn: me.onCheckcolumnBeforeCheckChange,
                                    scope: me
                                }
                            }
                        }
                    ],
                    plugins: [
                        Ext.create('Ext.grid.plugin.RowEditing', {
                            pluginId: 'roweditor',
                            autoCancel: false,
                            clicksToMoveEditor: 1,
                            listeners: {
                                edit: {
                                    fn: me.onRowEditingEdit,
                                    scope: me
                                },
                                canceledit: {
                                    fn: me.onRowEditingCanceledit,
                                    scope: me
                                }
                            }
                        })
                    ],
                    listeners: {
                        selectionchange: {
                            fn: me.onGridpanelSelectionChange,
                            scope: me
                        }
                    }
                }
            ],
            tools: [
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {

                        var grid = tool.up('window').down('grid');
                        var rowEditor=grid.getPlugin('roweditor');
                        var rec = Ext.create(grid.getStore().model, {label: 'New Practice Category'});

                        rowEditor.cancelEdit();
                        grid.getStore().insert(0,rec);
                        rowEditor.startEdit(0,0);

                    },
                    itemId: 'btnAdd',
                    tooltip: 'Add New Record',
                    type: 'plus'
                },
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {
                        var grid = tool.up('window').down('grid');
                        var rowEditor=grid.getPlugin('roweditor');
                        var sm = grid.getSelectionModel();


                        Ext.Msg.confirm(
                        "Delete Practice Category",
                        "Delete " + sm.getSelection()[0].get('label') + "?",
                        function(b) {

                            var store = grid.getStore();
                            rowEditor.cancelEdit();
                            store.remove(sm.getSelection());
                            if (store.getCount() > 0) {
                                sm.select(0);
                            }
                            store.sync();

                        }
                        );
                    },
                    disabled: true,
                    itemId: 'btnDelete',
                    tooltip: 'Delete selected record',
                    type: 'minus'
                },
                {
                    xtype: 'tool',
                    handler: function(event, toolEl, owner, tool) {
                        tool.up('window').down('grid').getStore().load();
                    },
                    tooltip: 'Refresh',
                    type: 'refresh'
                }
            ]
        });

        me.callParent(arguments);
    },

    onCheckboxfieldChange: function(field, newValue, oldValue, eOpts) {

        var rec = field.up('grid').getSelectionModel().getSelection()[0];
        rec.set('isActive',newValue);
    },

    onCheckcolumnBeforeCheckChange: function(checkcolumn, rowIndex, checked, eOpts) {
        return false;
    },

    onRowEditingEdit: function(editor, context, eOpts) {
        var rec = context.record;
        
        // this is a key technique:
        // The insert web service must return a primary key value, 
        // which is subsequently applied to the record in memory.
        // The record.commit() removes the grid's default indicator of modified fields
        rec.save({
            success: function(record,operation) {
                if (operation.action == 'create') {
                  var pk = Ext.decode(operation.response.responseText).id;
                  record.set('id',pk);
                }
                record.commit();
            },
            failure: function(record,operation) {
                Ext.Msg.alert('Operation failed',"Please try again later.");
                console.log(arguments);
            }
        });
    },

    onRowEditingCanceledit: function(editor, context, eOpts) {
        var rec = context.record;
        if (rec.phantom) {
         context.grid.getStore().remove(rec);
        }
    },

    onGridpanelSelectionChange: function(model, selected, eOpts) {

        var delBtn = this.down('#btnDelete');
        delBtn.setDisabled(!selected.length);
    }

});

That’s all folks! You should be able to integrate this solution into your apps with very few modifications other than tweaking the delete prompt text and changing the default text for a new record label.

Happy coding!

Protip: Constraining Ext JS Windows

This is the only methodology that I’ve found which reliably constrains windows to panels.

Step 1: Define your window with a constrain: true property

Ext.define('MyApp.view.MyWindow', {
    extend: 'Ext.window.Window',
    alias: 'widget.mywindow',
    autoShow: true,
    height: 320,
    width: 501,
    constrain: true,
    layout: {
        type: 'fit'
    },
    title: 'My Constrained Window'
});

Step 2: Instantiate the window into a container using the constrainTo property.

  // get reference to parent panel / container
  var parentPanel = Ext.ComponentQuery.query('#dashboardPanel')[0];
  
  // instantiate and constrain the window

  Ext.widget('mywindow', {
     constrainTo: parentPanel.getEl(),
     x: 5,
     y: 20
  });

Using Twilio to Add VOIP/Conference Calling to your Ext JS 4 Apps

I was recently tasked with automating a grant submission and approval workflow process for a non-profit corporation, as illustrated in the figure below. When there is disagreement among the reviewers as to whether a project should be funded, a conference call must take place between the reviewers in order to facilitate a consensus.

naccho1

The scope of the project initially dictated that we simply add-in the capability to schedule a call and send out meeting invitations. However, when it came time to actually build out this feature, I realized that we could take it one step further, “bring the awesome”, and use the Twilio API to integrate VOIP conference calling directly into the app.

Twilio is a cloud-based service that has both client-side and REST-based API’s for adding telephony and SMS features to your desktop and mobile apps. It can also bridge IP and regular voice communications.

There are three components to every Twilio app.

1) Generating a secure passkey to access the API (server-side REST)
2) Creating a UX to place calls (client-side Javascript)
3) Handling Twilio server callbacks with TwiML, the Twilio Markup Language (server-side REST)

app flow

Generating a secure passkey

You can generate a secure passkey by leveraging one of Twilio’s “Helper APIs” which include support for PHP, Ruby, Python, .NET, Java, Salesforce, Node, ColdFusion, and others. My app has access to both ColdFusion and .NET resources, so we decided to build out the initial Twilio handlers using ColdFusion as we feel that it’s a little faster for rapid prototyping. Ultimately, we may port this over to .NET which comprises the overwhelming bulk of the appserver code.

The Twilio ColdFusion helper library is located at the following URL:
https://github.com/jasonfill/ColdFusion-Twilio-Library

Using the helper library to generate a secure passkey is a relatively straightforward process (illustrated below):


<!--- 
 TwilioSettings.cfm contains the following settings:
 - Your Twilio Account ID
 - Your Twilio passcode
 - The Twilio API version that you intend to use
 - The Twilio endpoint (api.twilio.com)
--->
<cfinclude template="TwilioSettings.cfm" />

<!--- Create a new instance of the Twilio Lib, this can be stored in the App scope or elsewhere as a singleton... --->
<cfset REQUEST.TwilioLib = createObject("component", "twilio.TwilioLib").init(
   REQUEST.AccountSid, 
   REQUEST.AuthToken, 
   REQUEST.ApiVersion, 
   REQUEST.ApiEndpoint
) />

<!--- bind for outgoing calls  --->
<cfset cap = REQUEST.TwilioLib.getCapability() />

<cfset params = StructNew() />

<!--- the appsid is registered with Twilio from their Admin GUI --->
<!--- each app has a unique callback URL --->
<cfset appSid = "[insert twilio application id here]">

<!--- Allow the client to make outgoing calls by passing in the appsid --->
<cfset cap.allowClientOutgoing(appSid, params) />

<cfset token = cap.generateToken() />

<cfoutput>
<!DOCTYPE HTML>

<html>
<head>
    <meta charset="UTF-8">
    <title>ModelPractice</title>
<link rel="stylesheet" href="resources/MyApp-all.css"/>
<!--- output the security token --->
<script type="text/javascript">
  TwilioToken = "#token#";
</script>
<script type="text/javascript" src="app.js"></script>
<link rel="stylesheet" href="resources/css/app.css">
</head>
<body>
</body>
</html>
</cfoutput>

Creating a UX to place calls

Now that I had a valid security token accessible through the global Javascript variable TwilioToken, I could get on with the process of building out the user experience, depicted in the image below:

reviewers

Users can initiate a conference call by checking the boxes for with whom they wish to communicate. Once the selection is made, I needed to load the Twilio Javascript library and make the appropriate method calls. I also needed to provide an area that could display status messages as well as toggle the availability of a “hangup” button. To accomplish these tasks, I wrapped the Twilio library in a custom Ext JS class, illustrated below:

Ext.define('Ext.ux.Twilio', {

	config: {
		statusMsg: null, // ui component to display status
		ready: false,
		hangupButton: null // ui button to terminate call
	},

	constructor: function(config) {
		this.initConfig(config);
		this.callParent(arguments);
	},

	init: function(callback, scope, arg1, arg2) {

		if (!window['Twilio']) {

			var me = this;

			// load the twilio api and configure event listeners

			Ext.Loader.loadScript({

				url: '//static.twilio.com/libs/twiliojs/1.1/twilio.min.js',

				onLoad: function() {

					Twilio.Device.setup(TwilioToken, {
						debug: true
					});

					Twilio.Device.ready(function(device) {
						me.getStatusMsg().update('Ready to Connect');
						me.setReady(true);
						callback.call(scope, arg1, arg2);
						me.getHangupButton().hide();
					});

					Twilio.Device.offline(function(device) {
						me.getStatusMsg().update('Connection offline');
						me.getHangupButton().hide();
					});

					Twilio.Device.error(function(error) {
						me.getStatusMsg().update(error);
						console.log('twilio error', error);
						me.getHangupButton().hide();
					});

					Twilio.Device.connect(function(conn) {
						me.getStatusMsg().update("Successfully joined call");
						me.getHangupButton().show();
					});

					Twilio.Device.disconnect(function(conn) {
						me.getStatusMsg().update("Disconnected");
						me.getHangupButton().hide();
					});

				}


			}); // loadscript

		}
	},

	makeOutgoingCall: function(phoneNumber) {

		if (!this.getReady()) {
			this.init(this.makeOutgoingCall, this, phoneNumber);
			return;
		}

		Twilio.Device.connect({
			"PhoneNumber": phoneNumber,
			"tAction" : "placecall"
		});

	},

	// roomId is an arbitrary string that identifies
	// a unique virtual conference room

	// phoneNumbers is a comma-delimited string of 
	// telephone numbers to call and automatically 
	// add people to the conference line.
	
	makeConferenceCall: function(roomId, phoneNumbers) {

		if (!this.getReady()) {
			this.init(this.makeConferenceCall, this, roomId, phoneNumbers);
			return;
		}

		Twilio.Device.connect({
			"conferenceRoom": roomId,
			"phoneNumbers": phoneNumbers,
			"tAction" : "conferencecall"
		});
	}

});

When the user presses the conference call button, I aggregate the phone numbers from the selected records and invoke my custom Twilio class as follows:

(note that I have controller REFs that point to an Ext.Toolbar component containing an Ext.toolbar.TextItem component to display status messages and an Ext.button.Button to enable the caller to terminate the connection)

onConferenceCall: function(panel,grid) {
 
 if (!this.Twilio) {
    this.Twilio = Ext.create('Ext.ux.Twilio', {
        statusMsg : this.getTelStatusBar(),
        hangupButton: this.getHangupButton()
    });
 }

 var selectedRecords = grid.getSelectionModel().getSelection();

 if (selectedRecords.length === 0) {

  // join
  this.Twilio.makeConferenceCall('Room ' + this.surveyInstanceId,"");

 } else {

    // join and dial others
    var phoneNumbers = [];
    for (var i=0; i<selectedRecords.length; i++) {
        // ignore current user
        if (selectedRecords[i].get('reviewerId') != MyApp.app.userCredentials.id) {
            if (!Ext.isEmpty(selectedRecords[i].get('phone'))) {
                phoneNumbers.push(selectedRecords[i].get('phone'));
            }
        }
    }
    this.Twilio.makeConferenceCall('Room ' + this.surveyInstanceId,phoneNumbers.join(','));

 }
}

Handling Twilio REST Callbacks

When the Twilio.Device.connect() method is invoked, the Twilio service accesses a file on my server that provides additional directives via TwiML- Twilio’s XML language. Our application was relatively straightforward – we either needed to dial a single phone number (taction = placecall) or join a conference call (taction=conferencecall). In the case of joining a conference call, I also wanted the moderator to be able to have Twilio proactively call reviewers and add them to the virtual conference room. The “TwiML” application is listed below. Note that while any arguments passed to the Twilio.Device.connect() method will automatically be transmitted to your TwiML app, when you’re invoking the Twilio REST api’s custom passthrough arguments must be enumerated specifically as illustrated on line 58.


<cfsetting enablecfoutputonly="true">

<!--- your Twilio Account ID --->
<cfset accountSid = "[insert account sid here]">

<!--- your "secret" Twilio authorization token --->
<cfset authToken = "[insert auth token here]">

<!--- 
 The caller id phone number that you register 
 with Twilio
--->
<cfset callerIdNumber="[insert caller id number here]">

<cfif taction is "placecall">
  <cfsavecontent variable="out">
   <cfoutput><Number>#PhoneNumber#</Number></cfoutput>
  </cfsavecontent>
  <cfcontent type="text/xml"><cfoutput><?xml version="1.0" encoding="UTF-8"?>
    <Response>
    <Say>Placing your call now.</Say>
    <Dial callerId="#callerIdNumber#">#out#</Dial>
  </Response></cfoutput>
</cfif>


<cfif taction is "conferencecall">

	<cfcontent type="text/xml"><cfoutput><?xml version="1.0" encoding="UTF-8"?>
	<Response>
		<Say>Connecting to the Conference. You will hear music until another reviewer arrives.</Say>
		<Dial callerId="#callerIdNumber#">
		   <Conference>#ConferenceRoom#</Conference>
		</Dial>
	</Response></cfoutput>

	<cfif (isdefined("phoneNumbers"))>
	  <cfset i = 0>
	  <cfloop list="#phonenumbers#" index="thisPhoneNumber">
	      <cfset i=i+1>
		  
		  <cfthread phoneNumber="#thisPhoneNumber#" 
					name="conferenceOtherUser#i#"
					conferenceRoom = "#conferenceRoom#"
					accountSid="#accountSid#"
					authToken="#authToken#"
					callerIdNumber="#callerIdNumber#">
					
			 <cfhttp 
					url="https://api.twilio.com/2010-04-01/Accounts/#accountSid#/Calls"
					port="443"
					method="post"
					username="#accountSid#"
					password="#authtoken#">
					
					<cfhttpparam name="Caller" type="formfield" value="#callerIdNumber#">
					<cfhttpparam name="Called" type="formfield" value="#phoneNumber#">
					<cfhttpparam name="Url" type="formfield" value="http://www.naccho.org/mpdev/VoiceRequest.cfm?taction=conferencecall&conferenceRoom=#urlencodedformat(conferenceRoom)#">
					
					<cfhttpparam name="IfMachine" type="formfield" value="Hangup">
					
			  </cfhttp>	
			  
			  <cflog file="twilio1" text="#cfhttp.filecontent#">
		  </cfthread>
	  
	  </cfloop>

	</cfif>

</cfif>

So what are you waiting for? Add VOIP/Telephony/SMS capabilities to your apps and “bring the awesome!”

Generate an Excel File from a Tree Panel / Tree Grid!

Recently I was tasked with building an application whereby the user could export report data contained within a  treegrid to Microsoft Excel. Under normal circumstances, I would have used a server-side approach using ColdFusion’s robust functionality. However, in this particular case, we were using .NET and frankly, I wanted the middleware developer on the project to stay focused on building the core .NET CRUD webservices that were required for the project.

treegrid

Here’s the first-pass at a solution, which I implemented as an override to the tree panel control. Calling it’s rather quite simple. Just invoke the tree panel’s downloadExcelXml() method.

 {
   xtype: 'button',
   flex: 1,
   text: 'Download to Excel',
   handler: function(b, e) {
     b.up('treepanel').downloadExcelXml();
   }
 }

And here’s the conversion of the tree store data to an Excel spreadshet…

Ext.define('MyApp.view.override.TreePanel', {
	override: 'Ext.tree.Panel',
	requires: 'Ext.form.action.StandardSubmit',

	/*
        Kick off process
    */

	downloadExcelXml: function(includeHidden, title) {

		if (!title) title = this.title;

		var vExportContent = this.getExcelXml(includeHidden, title);

		var location = 'data:application/vnd.ms-excel;base64,' + Base64.encode(vExportContent);

		/* 
          dynamically create and anchor tag to force download with suggested filename 
          note: download attribute is Google Chrome specific
        */

		if (Ext.isChrome || Ext.isGecko || Ext.isSafari) { // local download
			var gridEl = this.getEl();

			var el = Ext.DomHelper.append(gridEl, {
				tag: "a",
				download: title + "-" + Ext.Date.format(new Date(), 'Y-m-d Hi') + '.xls',
				href: location
			});

			el.click();

			Ext.fly(el).destroy();

		} else { // remote download

			var form = this.down('form#uploadForm');
			if (form) {
				form.destroy();
			}
			form = this.add({
				xtype: 'form',
				itemId: 'uploadForm',
				hidden: true,
				standardSubmit: true,
				url: 'http://webapps.figleaf.com/dataservices/Excel.cfc?method=echo&mimetype=application/vnd.ms-excel&filename=' + escape(title + ".xls"),
				items: [{
					xtype: 'hiddenfield',
					name: 'data',
					value: vExportContent
				}]
			});

			form.getForm().submit();
		}
	},

	/*

        Welcome to XML Hell
        See: http://msdn.microsoft.com/en-us/library/office/aa140066(v=office.10).aspx
        for more details

    */
	getExcelXml: function(includeHidden, title) {

		var theTitle = title || this.title;

		var worksheet = this.createWorksheet(includeHidden, theTitle);
		var totalWidth = this.columns.length;

		return ''.concat(
			'<?xml version="1.0"?>',
			'<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">',
			'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office"><Title>' + theTitle + '</Title></DocumentProperties>',
			'<OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office"><AllowPNG/></OfficeDocumentSettings>',
			'<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">',
			'<WindowHeight>' + worksheet.height + '</WindowHeight>',
			'<WindowWidth>' + worksheet.width + '</WindowWidth>',
			'<ProtectStructure>False</ProtectStructure>',
			'<ProtectWindows>False</ProtectWindows>',
			'</ExcelWorkbook>',

			'<Styles>',

			'<Style ss:ID="Default" ss:Name="Normal">',
			'<Alignment ss:Vertical="Bottom"/>',
			'<Borders/>',
			'<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="12" ss:Color="#000000"/>',
			'<Interior/>',
			'<NumberFormat/>',
			'<Protection/>',
			'</Style>',

			'<Style ss:ID="title">',
			'<Borders />',
			'<Font ss:Bold="1" ss:Size="18" />',
			'<Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1" />',
			'<NumberFormat ss:Format="@" />',
			'</Style>',

			'<Style ss:ID="headercell">',
			'<Font ss:Bold="1" ss:Size="10" />',
			'<Alignment ss:Horizontal="Center" ss:WrapText="1" />',
			'<Interior ss:Color="#A3C9F1" ss:Pattern="Solid" />',
			'</Style>',


			'<Style ss:ID="even">',
			'<Interior ss:Color="#CCFFFF" ss:Pattern="Solid" />',
			'</Style>',


			'<Style ss:ID="evendate" ss:Parent="even">',
			'<NumberFormat ss:Format="yyyy-mm-dd" />',
			'</Style>',

			'<Style ss:ID="evenint" ss:Parent="even">',
			'<Numberformat ss:Format="0" />',
			'</Style>',

			'<Style ss:ID="evenfloat" ss:Parent="even">',
			'<Numberformat ss:Format="0.00" />',
			'</Style>',

			'<Style ss:ID="odd">',
			'<Interior ss:Color="#CCCCFF" ss:Pattern="Solid" />',
			'</Style>',

			'<Style ss:ID="groupSeparator">',
			'<Interior ss:Color="#D3D3D3" ss:Pattern="Solid" />',
			'</Style>',

			'<Style ss:ID="odddate" ss:Parent="odd">',
			'<NumberFormat ss:Format="yyyy-mm-dd" />',
			'</Style>',

			'<Style ss:ID="oddint" ss:Parent="odd">',
			'<NumberFormat Format="0" />',
			'</Style>',

			'<Style ss:ID="oddfloat" ss:Parent="odd">',
			'<NumberFormat Format="0.00" />',
			'</Style>',

			'<Style ss:ID="indent1even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="1"/>',
			'</Style>',

			'<Style ss:ID="indent2even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="2"/>',
			'</Style>',

			'<Style ss:ID="indent3even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="3"/>',
			'</Style>',

			'<Style ss:ID="indent4even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="4"/>',
			'</Style>',

			'<Style ss:ID="indent1odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="1"/>',
			'</Style>',

			'<Style ss:ID="indent2odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="2"/>',
			'</Style>',

			'<Style ss:ID="indent3odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="3"/>',
			'</Style>',

			'<Style ss:ID="indent4odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="4"/>',
			'</Style>',


			'</Styles>',
			worksheet.xml,
			'</Workbook>'
		);
	},


	getData: function() {

		var pnl = this;
		var cols = [];
		var maxDepth = 0;

		// get columns

		for (var i = 0; i < pnl.columns.length; i++) {
			cols.push({
				dataIndex: pnl.columns[i].dataIndex,
				title: pnl.columns[i].text,
				xtype: pnl.columns[i].xtype
			});
		}

		var aResult = [];
		var rootNode = pnl.getRootNode();

		rootNode.cascadeBy(function(node) {
			var rec = {};
			for (var j = 0; j < cols.length; j++) {
				rec[cols[j].dataIndex] = node.get(cols[j].dataIndex);
			}
			rec.depth = node.getDepth();
			if (rec.depth > maxDepth) {
				maxDepth = rec.depth;
			}
			aResult.push(rec);
		}, this);


		return {
			cols: cols,
			maxDepth: maxDepth,
			data: aResult
		}


	},



	createWorksheet: function(includeHidden, theTitle) {
		// Calculate cell data types and extra class names which affect formatting

		var data = this.getData();

		var cellType = [];
		var cellTypeClass = [];

		var totalWidthInPixels = 0;
		var colXml = '';
		var headerXml = '';
		var visibleColumnCountReduction = 0;

		for (var i = 0; i < data.cols.length; i++) {
			colXml += '<Column ss:AutoFitWidth="1"/>';
			headerXml += '<Cell ss:StyleID="headercell">' + '<Data ss:Type="String">' + data.cols[i].title + '</Data>' + '<NamedCell ss:Name="Print_Titles"></NamedCell></Cell>';
			switch (data.cols[i].xtype) {
				case "numbercolumn":
					cellType.push("Number");
					cellTypeClass.push("int");
					break;
				case "booleancolumn":
					cellType.push("String");
					cellTypeClass.push("");
					break;
				case "datecolumn":
					cellType.push("DateTime");
					cellTypeClass.push("date");
					break;
				default:
					cellType.push("String");
					cellTypeClass.push("");
					break;
			}
		}


		var visibleColumnCount = data.cols.length;

		var result = {
			height: 9000,
			// width: Math.floor(totalWidthInPixels * 30) + 50
			width: 1000
		};

		// Generate worksheet header details.

		// determine number of rows
		var numGridRows = data.data.length + 1;



		// create header for worksheet
		var t = ''.concat(
			'<Worksheet ss:Name="' + theTitle + '">',

			'<Names>',
			'<NamedRange ss:Name="Print_Titles" ss:RefersTo="=\'' + theTitle + '\'!R1:R2">',
			'</NamedRange></Names>',

			'<Table ss:ExpandedColumnCount="' + (visibleColumnCount + 2),
			'" ss:ExpandedRowCount="' + numGridRows + '" x:FullColumns="1" x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="15">',
			colXml,
			'<Row ss:Height="38">',
			'<Cell ss:MergeAcross="' + (visibleColumnCount - 1) + '" ss:StyleID="title">',
			'<Data ss:Type="String" xmlns:html="http://www.w3.org/TR/REC-html40">',
			'<html:b>' + theTitle + '</html:b></Data><NamedCell ss:Name="Print_Titles">',
			'</NamedCell></Cell>',
			'</Row>',
			'<Row ss:AutoFitHeight="1">',
			headerXml,
			'</Row>'
		);

		// Generate the data rows from the data in the Store
		var groupVal = "",
			cellClass = null,
			v = null;
		
		for (var i = 1; i < data.data.length; i++) {

			cellClass = (i & 1) ? 'odd' : 'even';
			t += '<Row>';
			

			for (var j = 0; j < data.cols.length; j++) {
				v = data.data[i][data.cols[j].dataIndex];

				if (j == 0 && data.data[i].depth > 1) {
					// first col might be indented
				    t += '<Cell ss:StyleID="indent' + data.data[i].depth + cellClass + '"><Data ss:Type="' + cellType[j] + '">';
				} else {
					t += '<Cell ss:StyleID="' + cellClass + cellTypeClass[j] + '"><Data ss:Type="' + cellType[j] + '">';
				}

				
				if (cellType[j] == 'DateTime') {
					t += Ext.Date.format(v, 'Y-m-d');
				} else {
					t += v;
				}
				t += '</Data></Cell>';


			}
			t += '</Row>';
		}

		result.xml = t.concat(
			'</Table>',
			'<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">',
			'<PageLayoutZoom>0</PageLayoutZoom>',
			'<Selected/>',
			'<Panes>',
			'<Pane>',
			'<Number>3</Number>',
			'<ActiveRow>2</ActiveRow>',
			'</Pane>',
			'</Panes>',
			'<ProtectObjects>False</ProtectObjects>',
			'<ProtectScenarios>False</ProtectScenarios>',
			'</WorksheetOptions>',
			'</Worksheet>'
		);

		return result;
	}
});

The server-side code used to echo the generated spreadsheet back to the browser and force a “download” operation is the following:

<cfcomponent>

 <cffunction name="echo" access="remote" returntype="void">
  
  <cfargument name="mimetype" type="string" required="no" default="text/html">
  <cfargument name="filename" type="string" required="yes">
  <cfargument name="data" type="string" required="no" default="">

  <cfif isdefined("form.data")>
   <cfset arguments.data = form.data>
  </cfif>

  <cfheader name="Content-Disposition" value="attachment; filename=#arguments.filename#">
  
  <cfcontent type="#arguments.mimetype#"><cfoutput>#arguments.data#</cfoutput>

 </cffunction>

</cfcomponent>

Note that the ColdFusion-based “echo” webservice is available for evaluation/testing purposes only. No warranty or level of service is expressed or implied.

You can play around with the code on Sencha Fiddle:
https://fiddle.sencha.com/#fiddle/4pr

Go check it out!