Tuesday, April 2, 2013

Linked Grid and Form Using Full CRUD Functionality

I started working with an ExtJS 4.0 Grid linked to a form example and miraculously got in working. I say miraculously because I believed with the changes to version 4 this would not be possible. But it was. The internal workings of ExtJS changed slightly but nonetheless I got it to work.

In my previous cell and row editor examples, after an insert, I check the response, get the insert_id or the id for the new row and insert it into the data before I load the grid. Here is the code:

tbar: [{
                text: 'Add Movie',
               /* icon: 'images/table_add.png',*/
    icon: 'icons/fam/add.gif',
                cls: 'x-btn-text-icon',
                handler: function() {
                    Ext.Ajax.request({
                        url: 'movies.cfc?method=addMovie',
                        params: {
                            action: 'create',
                            TITLE: 'New Movie'
                        },
                        success: function(resp,opt) {
                            var INSERT_ID = Ext.util.JSON.decode(
                                resp.responseText
                            ).INSERT_ID;
                            grid.getStore().insert(0, new Movie({
                                ID: INSERT_ID,
    COVERTHUMB: 'blankmovie.jpg',
                                TITLE: 'New Movie',
                                DIRECTOR: 'Bud Hines',
                                RUNTIME: '2',
                                RELEASED: '10/27/2002',
                                GENRE: '1',
        
                                TAGLINE: 'What a Great Movie',
     PRICE: 22.99,
    AVAILABLE: 1   
                            }, INSERT_ID)
       );
                            grid.startEditing(0,0);
                        },
                        failure: function(resp,opt) {
                            Ext.Msg.alert('Error','Unable to add movie');
                        }
                    });
                }
            },

In the CRUD example, you do not need to query the response to load the grid. ExtJS does this automatically by grabbing the row you pass back and inserting it into the grid and form. Pretty cool.

Well what did I change in the writer example to get it to work with ColdFusion?

First, I had to add an application.cfc. I kept debugging my code and seeing in my firebug html response that ColdFusion kept connecting to the ColdFusion administrator. Apparently every ColdFusion application requires the application cfc or it dies. I have seen this in numerous other jquery demos where I was using ColdFusion. So first, copy an application CFC, change what is necessary like the application name, and the "#request.dsn#" and paste it in the examples folder.

In this example, I am assuming you are modifying the code in the examples folder and working out of the examples folder. So go to the examples folder in ExtJS 3.2, look for the writer folder and click on that. We will be working with the files inside this folder. We will rename all of the files we need to touch from the file name to the filenamecfm so we can distinguish our changes.

There are four files we need to change in this example:

  • The form javascript file: UserForm.js which was renamed to UserFormcfm.js
  • The grid javascript file: UserGrid.js which was renamed to UserGridcfm.js
  • The writer javascript file: writer.js which was renamed to writercfm.js
  • The writer html file: writer.html which was renamed to writer.cfm
I also Added the UsersExt.cfc to execute the queries to read, edit, add and remove the data rows.

Let's take a look at each one and start with the UserForm.js file. Here the only thing I needed to change was the name of the data fields. Since ColdFusion, the way I coded it, returns uppercased names, I changed email to EMAIL, firstname to FIRSTNAME AND lastname to LASTNAME. Those are all the changes needed in this file.

/*!
 * Ext JS Library 3.2.0
 * Copyright(c) 2006-2010 Ext JS, Inc.
 * licensing@extjs.com
 * http://www.extjs.com/license
 */
Ext.ns('App', 'App.user');
/**
 * @class App.user.FormPanel
 * A typical FormPanel extension
 */
App.user.Form = Ext.extend(Ext.form.FormPanel, {
    renderTo: 'user-form',
    iconCls: 'silk-user',
    frame: true,
    labelAlign: 'right',
    title: 'User -- All fields are required',
    frame: true,
    width: 500,
    defaultType: 'textfield',
    defaults: {
        anchor: '100%'
    },

    // private A pointer to the currently loaded record
    record : null,

    /**
     * initComponent
     * @protected
     */
    initComponent : function() {
        // build the form-fields.  Always a good idea to defer form-building to a method so that this class can
        // be over-ridden to provide different form-fields
        this.items = this.buildForm();

        // build form-buttons
        this.buttons = this.buildUI();

        // add a create event for convenience in our application-code.
        this.addEvents({
            /**
             * @event create
             * Fires when user clicks [create] button
             * @param {FormPanel} this
             * @param {Object} values, the Form's values object
             */
            create : true
        });

        // super
        App.user.Form.superclass.initComponent.call(this);
    },

    /**
     * buildform
     * @private
     */
    buildForm : function() {
        return [
            {fieldLabel: 'Email', name: 'EMAIL', allowBlank: false, vtype: 'email'},
            {fieldLabel: 'First', name: 'FIRSTNAME', allowBlank: false},
            {fieldLabel: 'Last', name: 'LASTNAME', allowBlank: false}
        ];
    },

    /**
     * buildUI
     * @private
     */
    buildUI: function(){
        return [{
            text: 'Save',
            iconCls: 'icon-save',
            handler: this.onUpdate,
            scope: this
        }, {
            text: 'Create',
            iconCls: 'silk-user-add',
            handler: this.onCreate,
            scope: this
        }, {
            text: 'Reset',
            handler: function(btn, ev){
                this.getForm().reset();
            },
            scope: this
        }];
    },

    /**
     * loadRecord
     * @param {Record} rec
     */
    loadRecord : function(rec) {
        this.record = rec;
        this.getForm().loadRecord(rec);
    },

    /**
     * onUpdate
     */
    onUpdate : function(btn, ev) {
        if (this.record == null) {
            return;
        }
        if (!this.getForm().isValid()) {
            App.setAlert(false, "Form is invalid.");
            return false;
        }
        this.getForm().updateRecord(this.record);
    },

    /**
     * onCreate
     */
    onCreate : function(btn, ev) {
        if (!this.getForm().isValid()) {
            App.setAlert(false, "Form is invalid");
            return false;
        }
        this.fireEvent('create', this, this.getForm().getValues());
        this.getForm().reset();
    },

    /**
     * onReset
     */
    onReset : function(btn, ev) {
        this.fireEvent('update', this, this.getForm().getValues());
        this.getForm().reset();
    }
});

The next file we will change is the UserGrid.js file. Here again I changed email, lastname and firstname to upper case and that is all that was necessary.

/*!
 * Ext JS Library 3.2.0
 * Copyright(c) 2006-2010 Ext JS, Inc.
 * licensing@extjs.com
 * http://www.extjs.com/license
 */
Ext.ns('App', 'App.user');
/**
 * App.user.Grid
 * A typical EditorGridPanel extension.
 */
App.user.Grid = Ext.extend(Ext.grid.EditorGridPanel, {
    renderTo: 'user-grid',
    iconCls: 'silk-grid',
    frame: true,
    title: 'Users',
    height: 300,
    width: 500,
    style: 'margin-top: 10px',

    initComponent : function() {

        // typical viewConfig
        this.viewConfig = {
            forceFit: true
        };

        // relay the Store's CRUD events into this grid so these events can be conveniently listened-to in our application-code.
        this.relayEvents(this.store, ['destroy', 'save', 'update']);

        // build toolbars and buttons.
        this.tbar = this.buildTopToolbar();
        this.bbar = this.buildBottomToolbar();
        this.buttons = this.buildUI();

        // super
        App.user.Grid.superclass.initComponent.call(this);
    },

    /**
     * buildTopToolbar
     */
    buildTopToolbar : function() {
        return [{
            text: 'Add',
            iconCls: 'silk-add',
            handler: this.onAdd,
            scope: this
        }, '-', {
            text: 'Delete',
            iconCls: 'silk-delete',
            handler: this.onDelete,
            scope: this
        }, '-'];
    },

    /**
     * buildBottomToolbar
     */
    buildBottomToolbar : function() {
        return ['@cfg:', '-', {
            text: 'autoSave',
            enableToggle: true,
            pressed: true,
            tooltip: 'When enabled, Store will execute Ajax requests as soon as a Record becomes dirty.',
            toggleHandler: function(btn, pressed) {
                this.store.autoSave = pressed;
            },
            scope: this
        }, '-', {
            text: 'batch',
            enableToggle: true,
            pressed: true,
            tooltip: 'When enabled, Store will batch all records for each type of CRUD verb into a single Ajax request.',
            toggleHandler: function(btn, pressed) {
                this.store.batch = pressed;
            },
            scope: this
        }, '-', {
            text: 'writeAllFields',
            enableToggle: true,
            tooltip: 'When enabled, Writer will write *all* fields to the server -- not just those that changed.',
            toggleHandler: function(btn, pressed) {
                store.writer.writeAllFields = pressed;
            },
            scope: this
        }, '-'];
    },

    /**
     * buildUI
     */
    buildUI : function() {
        return [{
            text: 'Save',
            iconCls: 'icon-save',
            handler: this.onSave,
            scope: this
        }];
    },

    /**
     * onSave
     */
    onSave : function(btn, ev) {
        this.store.save();
    },

    /**
     * onAdd
     */
    onAdd : function(btn, ev) {
        var u = new this.store.recordType({
            FIRSTNAME : '',
            LASTNAME: '',
            EMAIL : ''
        });
        this.stopEditing();
        this.store.insert(0, u);
        this.startEditing(0, 1);
    },

    /**
     * onDelete
     */
    onDelete : function(btn, ev) {
        var index = this.getSelectionModel().getSelectedCell();
        if (!index) {
            return false;
        }
        var rec = this.store.getAt(index[0]);
        this.store.remove(rec);
    }
});

The next file I changed was the writer.js file. Here I needed to change the calls to the php urls to my urls and add a method=getUsers or method=addUser or method=editUser or method=removeUser depending on the action the action that is requested.Here are the changes I just described:


read: 'UsersExt.cfc?method=getUsers',
//Add a row
  create: 'UsersExt.cfc?method=addUser',
//Update a row 
  update: 'UsersExt.cfc?method=editUser',
//Delete a row
        destroy: 'UsersExt.cfc?method=removeUser'
I then needed to change the json reader and change the totalProperty to DATASET, root to ROWS, id to ID, messageProperty to MESSAGE and successProperty to SUCCESS as this is what my CFC returns.

var reader = new Ext.data.JsonReader({
 totalProperty:'DATASET',//This is how many total records are there in the set.
     root:'ROWS',//The Root of the data.        
  proxy:proxy,
  id:'ID',
  messageProperty: 'MESSAGE',
   successProperty: 'SUCCESS'
    //totalProperty: 'total',
    //successProperty: 'success',
   // idProperty: 'id',
    //root: 'data',
    //messageProperty: 'message'  // <-- New "messageProperty" meta-data
}, [
    {name: 'ID'},
    {name: 'EMAIL', allowBlank: false},
    {name: 'FIRSTNAME', allowBlank: false},
    {name: 'LASTNAME', allowBlank: false}
]);

// The new DataWriter component.
var writer = new Ext.data.JsonWriter({
    encode: true,
    writeAllFields: false
});

Please pay attention to the uppercasing of these fields. I also changed the email, firstname and lastname fields to uppercase where I define the fields for the reader as well further down in the code where I am defining my userColumns. Notice that i changed the dataIndex names to uppercase as well.

/*!
 * Ext JS Library 3.2.0
 * Copyright(c) 2006-2010 Ext JS, Inc.
 * licensing@extjs.com
 * http://www.extjs.com/license
 */
// Application instance for showing user-feedback messages.
var App = new Ext.App({});

// Create HttpProxy instance.  Notice new configuration parameter "api" here instead of load.  However, you can still use
// the "url" paramater -- All CRUD requests will be directed to your single url instead.
var proxy = new Ext.data.HttpProxy({
    api: {
//Read the table and pass back an array of data

       read: 'UsersExt.cfc?method=getUsers',
//Add a row
  create: 'UsersExt.cfc?method=addUser',
//Update a row 
  update: 'UsersExt.cfc?method=editUser',
//Delete a row
        destroy: 'UsersExt.cfc?method=removeUser'
    }
});

// Typical JsonReader.  Notice additional meta-data params for defining the core attributes of your json-response

var reader = new Ext.data.JsonReader({
 totalProperty:'DATASET',//This is how many total records are there in the set.
     root:'ROWS',//The Root of the data.        
  proxy:proxy,
  id:'ID',
  messageProperty: 'MESSAGE',
   successProperty: 'SUCCESS'
    //totalProperty: 'total',
    //successProperty: 'success',
   // idProperty: 'id',
    //root: 'data',
    //messageProperty: 'message'  // <-- New "messageProperty" meta-data
}, [
    {name: 'ID'},
    {name: 'EMAIL', allowBlank: false},
    {name: 'FIRSTNAME', allowBlank: false},
    {name: 'LASTNAME', allowBlank: false}
]);

// The new DataWriter component.
var writer = new Ext.data.JsonWriter({
    encode: true,
    writeAllFields: false
});

// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
    id: 'user',
    proxy: proxy,
    reader: reader,
    writer: writer,  // <-- plug a DataWriter into the store just as you would a Reader
    autoSave: true // <-- false would delay executing create, update, destroy requests until specifically told to do so with some [save] buton.
});

// load the store immeditately
store.load();

////
// ***New*** centralized listening of DataProxy events "beforewrite", "write" and "writeexception"
// upon Ext.data.DataProxy class.  This is handy for centralizing user-feedback messaging into one place rather than
// attaching listenrs to EACH Store.
//
// Listen to all DataProxy beforewrite events
//
Ext.data.DataProxy.addListener('beforewrite', function(proxy, action) {
    App.setAlert(App.STATUS_NOTICE, "Before " + action);
});

////
// all write events
//
Ext.data.DataProxy.addListener('write', function(proxy, action, result, res, rs) {
    App.setAlert(true, action + ':' + res.message);
});

////
// all exception events
//
Ext.data.DataProxy.addListener('exception', function(proxy, type, action, options, res) {
    if (type === 'remote') {
        Ext.Msg.show({
            title: 'REMOTE EXCEPTION',
            msg: res.message,
            icon: Ext.MessageBox.ERROR,
            buttons: Ext.Msg.OK
        });
    }
});

// A new generic text field
var textField =  new Ext.form.TextField();

// Let's pretend we rendered our grid-columns with meta-data from our ORM framework.
var userColumns =  [
    {header: "ID", width: 40, sortable: true, dataIndex: 'ID'},
    {header: "Email", width: 100, sortable: true, dataIndex: 'EMAIL', editor: textField},
    {header: "First", width: 50, sortable: true, dataIndex: 'FIRSTNAME', editor: textField},
    {header: "Last", width: 50, sortable: true, dataIndex: 'LASTNAME', editor: textField}
];

Ext.onReady(function() {
    Ext.QuickTips.init();

    // create user.Form instance (@see UserForm.js)
    var userForm = new App.user.Form({
        renderTo: 'user-form',
        listeners: {
            create : function(fpanel, data) {   // <-- custom "create" event defined in App.user.Form class
                var rec = new userGrid.store.recordType(data);
                userGrid.store.insert(0, rec);
            }
        }
    });

    // create user.Grid instance (@see UserGrid.js)
    var userGrid = new App.user.Grid({
        renderTo: 'user-grid',
        store: store,
        columns : userColumns,
        listeners: {
            rowclick: function(g, index, ev) {
                var rec = g.store.getAt(index);
                userForm.loadRecord(rec);
            },
            destroy : function() {
                userForm.getForm().reset();
            }
        }
    });
});

Our next step is to modify the writer.html file and save it as writer.cfm. Here we modify the javascript files to reference the files we have just changed:

  • UserFormcfm.js
  • UserGridcfm.js
  • writercfm.js
Here is the code:



Grid with DataWriter Example



    
     
     
     

    
    
    
    
    

    
    
    



Ext.data.DataWriter Example

This example shows how to implement a Writer for your Store. A Writer-enabled Store frees you from having to manually compose Ajax requests to perform CRUD actions on a Store.

Note that the js is not minified so it is readable. See writer.js, UserForm.js and UserGrid.js.

The HttpProxy plugged into the store in this example uses the new api configuration instead of an url. A simple MVC-like php backend has been created for this example which simulates a database by storing records in $_SESSION. See the file /remote/app/controllers/users.php. You may have to configure your web-server to allow scripts to be executed in the /examples directory.

var proxy = new Ext.data.HttpProxy({
    api: {
        read:   'UsersExt.cfc?method=getUsers',//Our URL for reading the grid data
  create:  'UsersExt.cfc?method=addUser',//For Adding a new User (future implementation)
  update:  'UsersExt.cfc?method=editUser',//When a User is updated
        destroy:  'UsersExt.cfc?method=removeUser'//When a User is updated
    }
});

Take note of the requests being generated in Firebug as you interact with the Grid and Form.

An Error has been simulated on the server-side: Attempting to update a record having ODD-numbered id will generate this errror. Responses from the update action will have successProperty === false along with a message. This error can be handled by listening to the "exception" event upon your Store.

exception : function(proxy, type, action, options, res, arg) {
    if (type === 'remote') {
        Ext.Msg.show({
            title: 'REMOTE EXCEPTION',
            msg: res.message,
            icon: Ext.MessageBox.ERROR
        });
    }
}

Note: This new "exception" event supercedes the old loadexception event which is now deprecated.

And finally we create the CFC. Here is the code for the ExtUsers CFC:


 
  
  
  
  
  
  
  
  
  
    
  
          
    SELECT
     UserID as ID, Email, FirstName, LastName
    FROM
     Users
    ORDER BY UserID Asc
  
  
  
  
  
  
  
 
 
 
 
  
  
  
  
   
    
  
  
  
  
  
    
   
   
            
   
            
   
   
      
  
  
  
  
  
 
 
 
  
  
  
  
  
  
  
         
         
  
  
  
   
        
  
   
        
        
   
        
  
  
  
      
  
   UPDATE Users
            Set Email = '#stcData.EMAIL#',
                LastName = '#stcData.LASTNAME#',
                FirstName = '#stcData.FIRSTNAME#'
  
   WHERE UserID = #Arguments.id#
          
  
  
  
   SELECT
    UserID as ID, Email, FirstName, LastName
   FROM
    Users
   WHERE
    UserID = #Arguments.id#
  
  
  
  
  
  
  
  
 
 
 
  
  
  
  
  
         
  
  
  
        
  
  
       
        
  
  
  
        
        Insert into Users
        (   Email, FirstName, LastName 
        )
        Values
            ( '#stcData.EMAIL#', '#stcData.FIRSTNAME#', '#stcData.LASTNAME#'
  )
        
        
       
         
         SELECT UserID AS ID, Email, FirstName, LastName
          FROM  Users
          WHERE (Email = '#stcData.EMAIL#') AND (LastName = '#stcData.LASTNAME#') AND (FirstName = '#stcData.FIRSTNAME#')
         
           
           
          
           
          
          
           
          
         
          
            
            
 
    
    
  
  
  
  
  
  
  
        
        
         
  
  
  
  
  
  
   Delete from Users
   WHERE UserID = #Arguments.id#
  
  
  
  
  
  
  
  
  
  
 



Now an important part of this application is your database. I am assuming, that you have a SqlServer, Derby, Access or MySql database with a table named Users with the following fields defined:

  • UserID defined as Integer or int
  • FirstName defined as Charachter 50 or nvarchar(50)
  • LastName defined as Charachter 50 or nvarchar(50)
  • Email defined as Character 50 or nvarchar(50)
Now all you need to do is go into firefox, type in http://127.0.0.1:8500/ext-3.2.0/examples/writer/writer.cfm (be sure to change ext-3.2.0 to whatever you have this file named in your root directory) and it should work. If not, check firebug. Happy coding!

No comments:

Post a Comment