Guest User

ExtJS Editable DataGrid Visualforce Component

a guest
Sep 21st, 2010
631
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <apex:component >
  2.     <!-- A Visualforce component implementing an ExtJS editable grid backed by a store wrapping an SObject -->
  3.     <!-- Jeff Trull 9/17/2010 -->
  4.     <apex:attribute name="object" type="Object" description="sObject to generate a grid for" required="true"/>
  5.     <apex:attribute name="fields" type="String[]" description="list of fields to display" required="true"/>
  6.     <apex:attribute name="minimized" type="Boolean" description="whether to initially render the grid in minimized state" default="false"/>
  7.     <apex:attribute name="rows" type="int" description="how many rows to display in grid" default="19"/>
  8.     <!-- load AJAX Toolkit -->
  9.     <script type="text/javascript">
  10.     var __sfdcSessionId = '{!GETSESSIONID()}';
  11.     </script>
  12.     <script src="../../soap/ajax/19.0/connection.js" type="text/javascript"></script>
  13.  
  14.     <!-- load ExtJS -->
  15.     <link rel="Stylesheet" type="text/css"
  16.         href="{!$Resource.ExtJS}/resources/css/ext-all.css" />
  17.     <script type="text/javascript"
  18.         src="{!$Resource.ExtJS}/adapter/ext/ext-base.js"></script>
  19.     <script type="text/javascript"
  20.         src="{!$Resource.ExtJS}/ext-all.js"></script>
  21.     <script type="text/javascript">
  22.         Ext.BLANK_IMAGE_URL="{!$Resource.ExtJS}/resources/images/default/s.gif"
  23.     </script>
  24.  
  25.     <!-- Define proxy class -->
  26.     <script type="text/javascript">
  27.         // Inherit from DirectProxy, which is quite similar to what we want, except that *we* are supplying
  28.         // the functions to be called for CRUD actions - they are not part of a Provider in the Ext sense
  29.         Ext.ux.SFDCProxy = Ext.extend(Ext.data.DirectProxy, {
  30.             constructor: function (config) {
  31.                 config = config || {};
  32.                 // override user-supplied config, if any, with our API definition
  33.                 Ext.applyIf(config, {
  34.                     api: {
  35.                         read: Ext.apply(this.launchRead.createDelegate(this),
  36.                         // make read method look like a Provider
  37.                         {
  38.                             directCfg: {
  39.                                 method: {
  40.                                     len: 1
  41.                                 }
  42.                             }
  43.                         }),
  44.                         create: this.launchCreate.createDelegate(this),
  45.                         update: this.launchUpdate.createDelegate(this),
  46.                         destroy: this.launchDestroy.createDelegate(this)
  47.                     }
  48.                 });
  49.                 Ext.ux.SFDCProxy.superclass.constructor.call(this, config);
  50.             },
  51.             launchRead: function (params, callback, scope) {
  52.                 // construct the query string.  We need to know the names of the selected fields.
  53.                 // in normal proxies this information is held only by the reader.  Our custom store
  54.                 // will supply these to us as a config parameter in the same format expected by readers
  55.                 // get the names of the configured fields
  56.                 var fieldlist = [];
  57.                 for (var i = 0; i < this.fields.length; i++) {
  58.                     fieldlist.push(this.fields[i].name);
  59.                 }
  60.        
  61.                 // assemble the query string
  62.                 var querystr = 'SELECT ' + fieldlist.join(', ') + ' from ' + this.sobjname;
  63.                 // Implement server-side sorting
  64.                 if ("sort" in params) {
  65.                     querystr += ' ORDER BY ' + params["sort"];
  66.                     if ("dir" in params) {
  67.                         querystr += ' ' + params["dir"];
  68.                     }
  69.                 }
  70.                 // I could theoretically use LIMIT here to reduce the amount of data transferred in cases
  71.                 // where paging is in force.  Unfortunately this produces misleading results b/c we can't know
  72.                 // the "total".  So I'm stuck with transferring extra data - but at least (due to the query
  73.                 // locater paging mechanism) not *all* of it
  74.                 // we have to create our own callback for the sforce request, for when it succeeds or fails, with
  75.                 // the signature SF wants to see.  These callbacks must in turn call the Ext callback for
  76.                 // data conversion (in the success case) or error reporting (fail)
  77.                 // sforce signature is cb(queryResult, userarg) for success and cb(error, userarg) for failure
  78.                 // Ext callback signature is cb(queryResult, statusobj), cb(error, statusobj) for failure
  79.                 // So we will insert an extra status argument after the first one
  80.                 sforce.connection.query(querystr,
  81.                 // connect the ExtJS callbacks to the AJAX Toolkit callback system
  82.                 // use createDelegate because it permits inserting args at arbitrary points
  83.                 // but the scope fixing feature is not needed
  84.                 {
  85.                     onSuccess: callback.createDelegate(window, [{
  86.                         status: true
  87.                     }], 1),
  88.                     onFailure: callback.createDelegate(window, [{
  89.                         status: false
  90.                     }], 1)
  91.                 });
  92.             },
  93.             launchCreate: function (jsonData, callback, scope) {
  94.                 // create and update are very similar, so consolidate
  95.                 this.launchCreateOrUpdate('create', jsonData, callback, scope);
  96.             },
  97.             launchUpdate: function (jsonData, callback, scope) {
  98.                 this.launchCreateOrUpdate('update', jsonData, callback, scope);
  99.             },
  100.             launchCreateOrUpdate: function (action, jsonData, callback, scope) {
  101.                 var recs = jsonData.records; // named this way due to the "root" config option
  102.                 // if writer is not "listful" we will sometimes get a single object here instead of a size 1 array
  103.                 var sobjs = new Array();
  104.                 for (var recno = 0; recno < recs.length; recno++) {
  105.                     var sobj = new sforce.SObject(this.sobjname);
  106.                     for (var k in recs[recno]) {
  107.                         sobj[k] = recs[recno][k];
  108.                     }
  109.                     sobjs.push(sobj);
  110.                 }
  111.                 sforce.connection[action](sobjs, {
  112.                     onSuccess: callback.createDelegate(window, [{
  113.                         status: true
  114.                     }], 1),
  115.                     onFailure: callback.createDelegate(window, [{
  116.                         status: false
  117.                     }], 1)
  118.                 });
  119.             },
  120.             launchDestroy: function (jsonData, callback, scope) {
  121.                 var recs = jsonData.records;
  122.                 sforce.connection.deleteIds(recs, {
  123.                     onSuccess: callback.createDelegate(window, [{
  124.                         status: true
  125.                     }], 1),
  126.                     onFailure: callback.createDelegate(window, [{
  127.                         status: false
  128.                     }], 1)
  129.                 });
  130.             },
  131.             onRead: function (action, trans, result, res) {
  132.                 // Assemble result
  133.                 var rT = trans.reader.recordType;
  134.                 var records = [];
  135.                 var it = new sforce.QueryResultIterator(result);
  136.                 // only access results we absolutely have to, to avoid calling queryMore via the result iterator
  137.                 var cur_rec = 0;
  138.                 var recs_supplied = 0;
  139.                 // if paging is in force, stop transferring data as soon as we have all that was requested
  140.                 while (it.hasNext() && (!("limit" in trans.params) || (recs_supplied < trans.params["limit"]))) {
  141.                     var sobj = it.next();
  142.                     if (("start" in trans.params) && (cur_rec++ < trans.params["start"])) {
  143.                         // we have not come to the beginning of the requested data yet
  144.                         continue;
  145.                     }
  146.                     var r = {};
  147.                     var id;
  148.                     for (var k in sobj) {
  149.                         if (k == 'Id') {
  150.                             id = sobj[k];
  151.                         }
  152.                         r[k] = sobj[k];
  153.                     }
  154.                     records.push(new rT(r, id));
  155.                     recs_supplied++;
  156.                 }
  157.                 // indicate load complete with event, and supply records via callback
  158.                 this.fireEvent("load", this, res, trans.request.arg);
  159.                 trans.request.callback.call(trans.request.scope, {
  160.                     records: records,
  161.                     success: true,
  162.                     totalRecords: result.size
  163.                 }, trans.request.arg, true);
  164.             },
  165.             onWrite: function (action, trans, result, res, rs) {
  166.                 // Report results the way Ext wants to see them:
  167.                 // produce an array of objects that just have IDs
  168.                 // note it's possible to do a lot more error checking
  169.                 // (esp comparing data we thought we uploaded with what actually happened)
  170.                 // plus we have success/failure on a per-record level from sforce.
  171.                 var data = [];
  172.                 for (var i = 0; i < result.length; i++) {
  173.                     data.push({
  174.                         id: result[i].id
  175.                     });
  176.                 }
  177.                 this.fireEvent("write", this, action, data, res, rs, trans.request.arg);
  178.                 trans.request.callback.call(trans.request.scope, data, res, true);
  179.             }
  180.         });
  181.        
  182.         // finally, the store class itself
  183.         // just sets things up right for the proxy (writer, pass sobj/fields to proxy)
  184.         Ext.ux.SFDCStore = Ext.extend(Ext.data.DirectStore, {
  185.             constructor: function (config) {
  186.                 config = config || {};
  187.                 var proxyCfg = Ext.copyTo({}, config, 'paramOrder,paramsAsHash,directFn,api,sobjname,fields');
  188.                 Ext.applyIf(config, {
  189.                     proxy: new Ext.ux.SFDCProxy(proxyCfg),
  190.                     // "encode: false" keeps data in a nice format for our proxy onWrite
  191.                     writer: new Ext.data.JsonWriter({
  192.                         encode: false,
  193.                         listful: true
  194.                     }),
  195.                     root: 'records'
  196.                 });
  197.                 Ext.ux.SFDCStore.superclass.constructor.call(this, config);
  198.             }
  199.         });            
  200.     </script>
  201.        
  202.     <!-- End SFDCStore component definition;  begin code for grid page -->
  203.     <!-- Icons.  Using those included with ExtJS. -->
  204.     <style type="text/css">
  205.         .icon-add
  206.         {
  207.             background:url({!$Resource.ExtJS}/examples/shared/icons/fam/add.gif) 0 no-repeat !important
  208.         }
  209.         .icon-save
  210.         {
  211.             background:url({!$Resource.ExtJS}/examples/shared/icons/save.gif) 0 no-repeat !important
  212.         }
  213.         .icon-delete
  214.         {
  215.             background:url({!$Resource.ExtJS}/examples/shared/icons/fam/delete.gif) 0 no-repeat !important
  216.         }
  217.     </style>
  218.  
  219.     <script type="text/javascript">
  220.         Ext.onReady(function () {
  221.             // check version - we require 3.2.1 or later
  222.             var required_version = [3, 2, 1];
  223.             var vercomps = Ext.version.split('.');
  224.             if ((vercomps[2] < required_version[2]) || (vercomps[2] == required_version[2]) && ((vercomps[1] < required_version[1]) || (vercomps[1] == required_version[1]) && (vercomps[0] < required_version[0]))) {
  225.                 Ext.Msg.alert('This code has only been tested with Ext version', required_version.join('.'));
  226.             }
  227.             // use AJAX Toolkit calls to verify that the supplied attributes make sense
  228.             var describeResult;
  229.             try {
  230.                 describeResult = sforce.connection.describeSObjects(["{!object}"]);
  231.             } catch (err) {
  232.                 Ext.Msg.alert('describe call failed on sobject {!object}:', err);
  233.                 return;
  234.             }
  235.             // locate our fields within the list of fields and copy properties
  236.             var fieldtypes = [],
  237.                 fieldlabels = [],
  238.                 allowblank = [],
  239.                 modifiable = [],
  240.                 sortable = [];
  241.             var fields = "{!fields}";
  242.             var fieldlist = fields.split(',');
  243.             var objfields = describeResult[0].fields;
  244.             for (var i = 0; i < fieldlist.length; i++) {
  245.                 // locate this requested field within the describe result
  246.                 var field_idx = -1;
  247.                 for (var j = 0; j < objfields.length; j++) {
  248.                     if (fieldlist[i] == objfields[j].name) {
  249.                         field_idx = j;
  250.                     }
  251.                 }
  252.                 if (field_idx == -1) {
  253.                     Ext.Msg.alert('{!object} does not have a field called', fieldlist[i]);
  254.                     return;
  255.                 }
  256.                 // extra information will help us do a better job with each field
  257.                 var objfield = objfields[field_idx];
  258.                 fieldtypes.push(objfield.type);
  259.                 fieldlabels.push(objfield.label);
  260.                 allowblank.push(objfield.nillable);
  261.                 // let users modify if they can create or update AND it's not the Id field
  262.                 // unwelcome discovery: a boolean stored in an Array becomes a string, and therefore "true"
  263.                 // have to do string compares later :(
  264.                 modifiable.push(objfield.createable || objfield.updateable);
  265.                 sortable.push(objfield.sortable);
  266.                 // TBD implement picklist editor by creating an ArrayStore to supply values
  267.             }
  268.             // create the Store
  269.             var extfields = [];
  270.             if (fieldlist.indexOf('Id') == -1) {
  271.                 extfields.push({
  272.                     name: 'Id'
  273.                 }); // always include ID in the store
  274.             }
  275.             for (var i = 0; i < fieldlist.length; i++) {
  276.                 extfields.push({
  277.                     name: fieldlist[i]
  278.                 });
  279.             }
  280.             var store = new Ext.ux.SFDCStore({
  281.                 sobjname: '{!object}',
  282.                 fields: extfields,
  283.                 remoteSort: true,
  284.                 autoSave: false,
  285.                 batch: true
  286.             });
  287.        
  288.             // and now the Grid that references it - columns first
  289.             var columns = [];
  290.             for (var i = 0; i < fieldlist.length; i++) {
  291.                 var col = {
  292.                     id: fieldlist[i],
  293.                     dataIndex: fieldlist[i],
  294.                     header: fieldlabels[i],
  295.                     sortable: sortable[i],
  296.                     width: 150
  297.                 };
  298.                 // use data type from above to determine type of Editor and xtype to use
  299.                 if ((fieldtypes[i] == "int") || (fieldtypes[i] == "double")) {
  300.                     col.xtype = 'numbercolumn';
  301.                     if (fieldtypes[i] == "int") {
  302.                         // do not show decimals
  303.                         col.format = '0,000';
  304.                     }
  305.                     if (modifiable[i] == "true") {
  306.                         col.editor = new Ext.form.NumberField({
  307.                             allowBlank: allowblank[i],
  308.                             allowDecimals: (fieldtypes[i] == "double")
  309.                         });
  310.                     }
  311.                 } else if (fieldtypes[i] == "boolean") {
  312.                     col.xtype = 'booleancolumn';
  313.                     if (modifiable[i] == "true") {
  314.                         col.editor = new Ext.form.Checkbox();
  315.                     }
  316.                 } else if (fieldtypes[i] == "date") {
  317.                     col.xtype = 'datecolumn';
  318.                     if (modifiable[i] == "true") {
  319.                         // NOTE not fully tested.  I observed some type of time zone issue where
  320.                         // value stored on server was 1 day off from the one chosen
  321.                         col.editor = new Ext.form.DateField({
  322.                             allowBlank: allowblank[i]
  323.                         });
  324.                     }
  325.                 } else {
  326.                     // default xtype is OK
  327.                     if (modifiable[i] == "true") {
  328.                         // fall back on TextField, but use validators if possible
  329.                         var cfg = {
  330.                             allowBlank: allowblank[i]
  331.                         };
  332.                         if ((fieldtypes[i] == "url") || (fieldtypes[i] == "email")) {
  333.                             cfg.vtype = fieldtypes[i];
  334.                         }
  335.                         col.editor = new Ext.form.TextField(cfg);
  336.                     }
  337.                 }
  338.                 columns.push(col);
  339.             }
  340.             var pagesize = parseInt('{!rows}'); // otherwise we end up with a string value, which subtly fails
  341.             var initially_minimized = '{!minimized}';
  342.             if (initially_minimized !== 'true') {
  343.                 store.load({
  344.                     params: {
  345.                         start: 0,
  346.                         limit: pagesize
  347.                     }
  348.                 });
  349.             }
  350.             var grid = new Ext.grid.EditorGridPanel({
  351.                 store: store,
  352.                 columns: columns,
  353.                 stripeRows: true,
  354.                 height: 500,
  355.                 title: '{!object}',
  356.                 collapsible: true,
  357.                 collapsed: (initially_minimized == "true"),
  358.                 listeners: {
  359.                     beforeexpand: function (panel, animate) { // load data via query when grid expanded by user
  360.                         var store = panel.getStore(); // a reference to the original store we configured the panel with
  361.                         if (store.getTotalCount() == 0) {
  362.                             store.load({
  363.                                 params: {
  364.                                     start: 0,
  365.                                     limit: pagesize
  366.                                 }
  367.                             });
  368.                         }
  369.                         return true;
  370.                     }
  371.                 },
  372.                 bbar: new Ext.PagingToolbar({
  373.                     pageSize: pagesize,
  374.                     store: store,
  375.                     displayInfo: true,
  376.                     displayMsg: 'Displaying objects {0} - {1} of {2}',
  377.                     emptyMsg: 'no records found'
  378.                 }),
  379.                 sm: new Ext.grid.RowSelectionModel(),
  380.                 tbar: {
  381.                     items: [{
  382.                         text: 'Save Changes',
  383.                         iconCls: 'icon-save',
  384.                         handler: function () {
  385.                             Ext.Msg.confirm('Save Changes?', 'Commit all changes, including deletions, in {!object}?', function (b) {
  386.                                 if (b == 'yes') {
  387.                                     grid.stopEditing();
  388.                                     store.save();
  389.                                 }
  390.                             });
  391.                         }
  392.                     },
  393.                     {
  394.                         text: 'New',
  395.                         iconCls: 'icon-add',
  396.                         handler: function () {
  397.                             grid.stopEditing();
  398.                             var newreccfg = {};
  399.                             for (var i = 0; i < fieldlist.length; i++) {
  400.                                 newreccfg[fieldlist[i]] = '';
  401.                             }
  402.                             store.insert(0, new store.recordType(newreccfg));
  403.                             grid.startEditing(0, 1);
  404.                         }
  405.                     },
  406.                     {
  407.                         text: 'Delete Selected',
  408.                         iconCls: 'icon-delete',
  409.                         handler: function (b, e) { // button handlers get the button and the event passed in
  410.                             // collect list of things that will be deleted
  411.                             var mygrid = b.findParentByType('editorgrid');
  412.                             var selRecs = mygrid.getSelectionModel().getSelections();
  413.                             if (selRecs.length == 0) {
  414.                                 console.log('no records selected');
  415.                                 return;
  416.                             }
  417.                             var delIds = new Array();
  418.                             for (var i = 0; i < selRecs.length; i++) {
  419.                                 delIds.push(selRecs[i].id);
  420.                             }
  421.                             Ext.Msg.confirm('Delete Entries?', 'Temporarily delete entries ' + delIds.join(', ') + '?', function (b) {
  422.                                 if (b == 'yes') {
  423.                                     grid.stopEditing();
  424.                                     store.remove(selRecs);
  425.                                 }
  426.                             });
  427.                         }
  428.                     }]
  429.                 },
  430.                 batchSave: true,
  431.                 renderTo: 'myGrid' // I think this is keeping me from having >1 grid per page. how to fix?
  432.             });
  433.         });
  434.     </script>
  435.     <div id="myGrid"/>
  436. </apex:component>
Advertisement
Add Comment
Please, Sign In to add comment