esoteric
9 Apr 2008, 6:24 AM
Greetings Community!
After using Tasks v2 for a while, I realized that it would be really cool to have the ability to add a form to the top of a grid with ease. However after diving into the code for Tasks v2, I found that all the code that makes that form was static, so I set out to create a dynamic one, after see Saki's RowAction plugin, I knew how to attack it, so I give you the first version of my first public plugin.
How Does This Plugin Works
Basically this plugin modifies the header template of the view for the Grid before render, then after render dynamically creates form fields based on the column type that it corresponds to, then gets rendered to the header column.
A couple keys points to note are:
Uses AJAX to send values to URL.
Uses JSON to encode values.
Requires JSON return string on success with parameters success: (true|false) and message: (string) optional
As of right now, the ENTER key is the only way to submit the new record.
For more details look to the changelog.
April 29, 2008 - The current version is broken, I just discovered this when trying to deploy in an example other then the array grid in the examples, I am working on fixing it.
Current Version: 0.1
Current Stability: alpha
License: BSD License
Config Options (these are the main ones, see source for all of them)
url (REQUIRED) Where do you want the values of the HeaderForm to be sent.
notifiyAfterSubmit (default: true) MessageBox.Alert Upon Successful Submit.
reloadAfterSubmit (default: true) After Submit Reload Grid Store, No Other Option Exists To Get New Record Yet.
fieldNames (Object) Mapping a column name to a different name for the field. (ie: {changePct:'percent'})
ignoreFields (Object) Fields to Ignore When Creating Form Elements. (ie: {'pctChange':true}) Yes it has to be set to true.
Anyways I hope you all enjoy the plugin, for now if you have problems or find bugs just post them here, I have done testing, but nothing too extensive so there probably will be scenarios that break it.
Here is the source for browsing pleasure, it is also included in the attachments below:
/**
* Global ExtJS
*/
Ext.namespace('Ext.ux.grid');
/**
* @class Ext.ux.grid.HeaderForm
* @extends Ext.util.Observable
*
* @author Erik Kristensen (aka Esoteric)
* @version 0.1
* @license BSD License (see license.txt for entire contents)
* @copyright 2008 All Rights Reserved. SCR Technologies, LLC.
*
* Creates new HeaderForm Plugin
*
* Plugin Inspired by RowActions Plugin by Saki.
*
* @constructor
* @param {Object} config The config object
*/
Ext.ux.grid.HeaderForm = function (config) {
Ext.apply(this, config);
// call parent
Ext.ux.grid.HeaderForm.superclass.constructor.call(this);
}; // eo constructor
Ext.extend(Ext.ux.grid.HeaderForm, Ext.util.Observable, {
/**
* @cfg {String} url Where does the AJAX request go to? REQUIRED
*/
url: null,
/**
* @cfg {Boolean} notifyAfterSubmit Do you want to be notified after successful submit.
*/
notifyAfterSubmit: true,
/**
* @cfg {Boolean} reloadAfterSubmit Reload the Grid Store after successfull submit? If both insertAfterSubmit and this are set to true, reloadAfterSubmit takes precendence
*/
reloadAfterSubmit: true,
/**
* @cfg {String} notifySuccessMsg Upon submit of record and reply of success, display this message, unless variable "message" is returned in JSON string as response from server.
*/
notifySuccessMsg: 'Record Successfully Added',
/**
* @cfg {String} notifyErrorMsg Upon submit of record and reply of error, display this message, unless variable "message" is returned in JSON string as response from server.
*/
notifyErrorMsg: 'Failure Adding Record',
/**
* @cfg {Boolean} dependOnFirst Do The Other Fields Depend On The First Field (this is almost always set to true)
*/
dependOnFirst: true,
/**
* @cfg {String} defaultEmptyText The Default Text That Appears in the First Field of the Header Form
*/
defaultEmptyText: 'Enter in Text ...',
/**
* @cfg {Object} fieldNames Mapping of Field Names to Column Names. Used for when you want different names posted in the AJAX request.
*/
fieldNames: {},
/**
* @cfg {Object} ignoreFields Fields to Ignore When Creating Header Form for Creating New Records
*/
ignoreFields: {},
/**
* @cfg {Object} mapping Mapping of Record types to Field xtypes
*/
mapping: {
'auto':'textfield',
'boolean':'checkbox',
'date':'datefield',
'float':'numberfield',
'int':'numberfield',
'string':'textfield'
},
// private
editing: false,
focused: false,
userTriggered: false,
/**
* Main init function
* @private
* @param {Object} grid
*/
init : function (grid)
{
// save reference to grid
this.grid = grid;
// we need to reconfigure ourselves when grid reconfigures
grid.reconfigure = grid.reconfigure.createSequence(this.reconfigure, this);
grid.afterRender = grid.afterRender.createSequence(this.afterRender, this);
// initial (re)configuration
this.reconfigure();
}, // eo function init
/**
* Create and Modify the header of the Grid View.
* @private
*/
createHeader : function ()
{
var rows = [];
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var store = this.grid.store;
this.body = new Ext.Template(
'<tbody><tr class="new-row">{rows}</tr></tbody>'
);
this.row = new Ext.Template(
'<td><div class="x-small-editor" id="new-row-{id}"></div></td>'
);
fields.each(function(f, i) {
rows[rows.length] = this.row.apply({id: f.name.toLowerCase()});
}, this);
var body = this.body.apply({rows: rows.join("")});
this.grid.getView().templates = {};
this.grid.getView().templates.header = new Ext.Template(
'<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
'<thead><tr class="x-grid3-hd-row">{cells}</tr></thead>',
body,
'</table>'
);
},
/**
* Creates form fields configuration. Then they are rendered to the header.
* @private
*/
createFormFields : function ()
{
this.forms = [];
this.formFields = [];
this.formFieldConfigs = [];
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var store = this.grid.store;
var x=0;
fields.each(function(f, i) {
var id = f.name.toLowerCase();
if (x==0)
{
this.firstField = f.name;
}
if (this.ignoreFields[f.name] == true)
{
this.formFields[x] = false;
x++;
return;
}
this.formFieldConfigs[x] = {};
Ext.apply(this.formFieldConfigs[x], {
name: (this.fieldNames[f.name] ? this.fieldNames[f.name] : f.name),
tabIndex: x + 1,
xtype: this.mapping[f.type] || 'textfield',
//fieldLabel: f.name,
disabled: ( this.dependOnFirst ? ( (x > 0) ? true : false ) : false ),
emptyText: (x == 0 ? this.defaultEmptyText : ''),
dateFormat: ( (f.type === 'date' && f.dateFormat) ? f.dateFormat : null ),
renderTo: 'new-row-' + id,
listeners : {
scope: this,
'focus' : function ()
{
this.editing = true;
if (this.firstField == f.name && this.dependOnFirst)
{
this.editing = true;
for (var i=0; i<this.formFields.length; i++)
{
this.formFields[i].enable();
}
}
},
'blur' : function ()
{
//this.focused = false;
if (this.editing && !this.focused)
{
var title = this.formFields[0].getValue();
if (!title)
{
this.formFields[0].setValue('');
if (this.userTriggered)
{ // if they entered to add the task, then go to a new add automatically
this.userTriggered = false;
this.formFields[0].focus.defer(100, this.formFields[0]);
}
for (var i=1; i<this.formFields.length; i++)
{
this.formFields[i].disable();
}
}
this.editing = false;
}
},
'specialKey' : function ( f , e )
{
if (e.getKey()==e.ENTER && (!f.isExpanded || !f.isExpanded()))
{
this.userTriggered = true;
e.stopEvent();
f.el.blur();
if (f.triggerBlur)
{
f.triggerBlur();
}
this.submitValues();
}
}
}
});
if (this.mapping[f.type] == 'textfield')
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'checkbox')
{
this.formFields[x] = new Ext.form.Checkbox(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'datefield')
{
this.formFields[x] = new Ext.form.DateField(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'numberfield')
{
this.formFields[x] = new Ext.form.NumberField(this.formFieldConfigs[x]);
}
else
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
x++;
}, this);
this.syncFields();
},
/**
* Reconfigures the plugin - deletes old form and creates new one
* Runs also after grid reconfigure call
* @private
*/
syncFields : function ()
{
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var x=0;
var z=0;
fields.each(function(f) {
if (this.ignoreFields[f.name] == true) {
z = z + 1;
}
else
{
this.formFields[x].setSize(cm.getColumnWidth(x) + 2 + z);
}
x++;
}, this);
},
/**
* After submitting the values, this resets the form.
* @private
*/
resetFields : function ()
{
for (var i=0; i<this.formFields.length; i++)
{
this.formFields[i].setValue('');
if (i > 0)
{
this.formFields[i].disable();
}
}
},
/**
* Gets the values from all the fields and encodes,
* then submits them via an AJAX call.
* @private
*/
submitValues : function ()
{
if (this.url == null) return;
var values = [];
var entry = {};
for (var i=0; i<this.formFields.length; i++)
{
var name = this.formFields[i].getName();
var value = this.formFields[i].getValue();
entry = {name: name, value: value};
values[values.length] = entry;
}
var json = Ext.util.JSON.encode(values);
Ext.Ajax.request({
url: this.url,
params: json,
scope: this,
success : function ( response, result )
{
var answer = Ext.util.JSON.decode(response.responseText);
if (answer.success === true)
{
if (this.notifyAfterSubmit === true)
{
Ext.MessageBox.alert('Success', answer.message ? answer.message : this.notifySuccessMsg);
}
this.grid.store.reload();
this.resetFields();
}
else
{
Ext.MessageBox.alert('Error', answer.message ? answer.message : this.notifyErrorMsg);
}
},
failure : function ( response, result )
{
Ext.MessageBox.alert('Failure', 'Communication Failure');
}
});
},
/**
* Reconfigures the plugin - recreates the header.
* Runs also after grid reconfigure call
* @private
*/
reconfigure : function ()
{
this.createHeader();
},
/**
* (Re)Renders the Form Fields for the plugin.
* Runs after the grids afterRender, so that we know the grid has been rendered.
* @private
*/
afterRender : function ()
{
this.createFormFields();
}
});
Enjoy!
After using Tasks v2 for a while, I realized that it would be really cool to have the ability to add a form to the top of a grid with ease. However after diving into the code for Tasks v2, I found that all the code that makes that form was static, so I set out to create a dynamic one, after see Saki's RowAction plugin, I knew how to attack it, so I give you the first version of my first public plugin.
How Does This Plugin Works
Basically this plugin modifies the header template of the view for the Grid before render, then after render dynamically creates form fields based on the column type that it corresponds to, then gets rendered to the header column.
A couple keys points to note are:
Uses AJAX to send values to URL.
Uses JSON to encode values.
Requires JSON return string on success with parameters success: (true|false) and message: (string) optional
As of right now, the ENTER key is the only way to submit the new record.
For more details look to the changelog.
April 29, 2008 - The current version is broken, I just discovered this when trying to deploy in an example other then the array grid in the examples, I am working on fixing it.
Current Version: 0.1
Current Stability: alpha
License: BSD License
Config Options (these are the main ones, see source for all of them)
url (REQUIRED) Where do you want the values of the HeaderForm to be sent.
notifiyAfterSubmit (default: true) MessageBox.Alert Upon Successful Submit.
reloadAfterSubmit (default: true) After Submit Reload Grid Store, No Other Option Exists To Get New Record Yet.
fieldNames (Object) Mapping a column name to a different name for the field. (ie: {changePct:'percent'})
ignoreFields (Object) Fields to Ignore When Creating Form Elements. (ie: {'pctChange':true}) Yes it has to be set to true.
Anyways I hope you all enjoy the plugin, for now if you have problems or find bugs just post them here, I have done testing, but nothing too extensive so there probably will be scenarios that break it.
Here is the source for browsing pleasure, it is also included in the attachments below:
/**
* Global ExtJS
*/
Ext.namespace('Ext.ux.grid');
/**
* @class Ext.ux.grid.HeaderForm
* @extends Ext.util.Observable
*
* @author Erik Kristensen (aka Esoteric)
* @version 0.1
* @license BSD License (see license.txt for entire contents)
* @copyright 2008 All Rights Reserved. SCR Technologies, LLC.
*
* Creates new HeaderForm Plugin
*
* Plugin Inspired by RowActions Plugin by Saki.
*
* @constructor
* @param {Object} config The config object
*/
Ext.ux.grid.HeaderForm = function (config) {
Ext.apply(this, config);
// call parent
Ext.ux.grid.HeaderForm.superclass.constructor.call(this);
}; // eo constructor
Ext.extend(Ext.ux.grid.HeaderForm, Ext.util.Observable, {
/**
* @cfg {String} url Where does the AJAX request go to? REQUIRED
*/
url: null,
/**
* @cfg {Boolean} notifyAfterSubmit Do you want to be notified after successful submit.
*/
notifyAfterSubmit: true,
/**
* @cfg {Boolean} reloadAfterSubmit Reload the Grid Store after successfull submit? If both insertAfterSubmit and this are set to true, reloadAfterSubmit takes precendence
*/
reloadAfterSubmit: true,
/**
* @cfg {String} notifySuccessMsg Upon submit of record and reply of success, display this message, unless variable "message" is returned in JSON string as response from server.
*/
notifySuccessMsg: 'Record Successfully Added',
/**
* @cfg {String} notifyErrorMsg Upon submit of record and reply of error, display this message, unless variable "message" is returned in JSON string as response from server.
*/
notifyErrorMsg: 'Failure Adding Record',
/**
* @cfg {Boolean} dependOnFirst Do The Other Fields Depend On The First Field (this is almost always set to true)
*/
dependOnFirst: true,
/**
* @cfg {String} defaultEmptyText The Default Text That Appears in the First Field of the Header Form
*/
defaultEmptyText: 'Enter in Text ...',
/**
* @cfg {Object} fieldNames Mapping of Field Names to Column Names. Used for when you want different names posted in the AJAX request.
*/
fieldNames: {},
/**
* @cfg {Object} ignoreFields Fields to Ignore When Creating Header Form for Creating New Records
*/
ignoreFields: {},
/**
* @cfg {Object} mapping Mapping of Record types to Field xtypes
*/
mapping: {
'auto':'textfield',
'boolean':'checkbox',
'date':'datefield',
'float':'numberfield',
'int':'numberfield',
'string':'textfield'
},
// private
editing: false,
focused: false,
userTriggered: false,
/**
* Main init function
* @private
* @param {Object} grid
*/
init : function (grid)
{
// save reference to grid
this.grid = grid;
// we need to reconfigure ourselves when grid reconfigures
grid.reconfigure = grid.reconfigure.createSequence(this.reconfigure, this);
grid.afterRender = grid.afterRender.createSequence(this.afterRender, this);
// initial (re)configuration
this.reconfigure();
}, // eo function init
/**
* Create and Modify the header of the Grid View.
* @private
*/
createHeader : function ()
{
var rows = [];
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var store = this.grid.store;
this.body = new Ext.Template(
'<tbody><tr class="new-row">{rows}</tr></tbody>'
);
this.row = new Ext.Template(
'<td><div class="x-small-editor" id="new-row-{id}"></div></td>'
);
fields.each(function(f, i) {
rows[rows.length] = this.row.apply({id: f.name.toLowerCase()});
}, this);
var body = this.body.apply({rows: rows.join("")});
this.grid.getView().templates = {};
this.grid.getView().templates.header = new Ext.Template(
'<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
'<thead><tr class="x-grid3-hd-row">{cells}</tr></thead>',
body,
'</table>'
);
},
/**
* Creates form fields configuration. Then they are rendered to the header.
* @private
*/
createFormFields : function ()
{
this.forms = [];
this.formFields = [];
this.formFieldConfigs = [];
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var store = this.grid.store;
var x=0;
fields.each(function(f, i) {
var id = f.name.toLowerCase();
if (x==0)
{
this.firstField = f.name;
}
if (this.ignoreFields[f.name] == true)
{
this.formFields[x] = false;
x++;
return;
}
this.formFieldConfigs[x] = {};
Ext.apply(this.formFieldConfigs[x], {
name: (this.fieldNames[f.name] ? this.fieldNames[f.name] : f.name),
tabIndex: x + 1,
xtype: this.mapping[f.type] || 'textfield',
//fieldLabel: f.name,
disabled: ( this.dependOnFirst ? ( (x > 0) ? true : false ) : false ),
emptyText: (x == 0 ? this.defaultEmptyText : ''),
dateFormat: ( (f.type === 'date' && f.dateFormat) ? f.dateFormat : null ),
renderTo: 'new-row-' + id,
listeners : {
scope: this,
'focus' : function ()
{
this.editing = true;
if (this.firstField == f.name && this.dependOnFirst)
{
this.editing = true;
for (var i=0; i<this.formFields.length; i++)
{
this.formFields[i].enable();
}
}
},
'blur' : function ()
{
//this.focused = false;
if (this.editing && !this.focused)
{
var title = this.formFields[0].getValue();
if (!title)
{
this.formFields[0].setValue('');
if (this.userTriggered)
{ // if they entered to add the task, then go to a new add automatically
this.userTriggered = false;
this.formFields[0].focus.defer(100, this.formFields[0]);
}
for (var i=1; i<this.formFields.length; i++)
{
this.formFields[i].disable();
}
}
this.editing = false;
}
},
'specialKey' : function ( f , e )
{
if (e.getKey()==e.ENTER && (!f.isExpanded || !f.isExpanded()))
{
this.userTriggered = true;
e.stopEvent();
f.el.blur();
if (f.triggerBlur)
{
f.triggerBlur();
}
this.submitValues();
}
}
}
});
if (this.mapping[f.type] == 'textfield')
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'checkbox')
{
this.formFields[x] = new Ext.form.Checkbox(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'datefield')
{
this.formFields[x] = new Ext.form.DateField(this.formFieldConfigs[x]);
}
else if (this.mapping[f.type] == 'numberfield')
{
this.formFields[x] = new Ext.form.NumberField(this.formFieldConfigs[x]);
}
else
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
x++;
}, this);
this.syncFields();
},
/**
* Reconfigures the plugin - deletes old form and creates new one
* Runs also after grid reconfigure call
* @private
*/
syncFields : function ()
{
var cm = this.grid.getColumnModel();
var fields = this.grid.store.recordType.prototype.fields;
var x=0;
var z=0;
fields.each(function(f) {
if (this.ignoreFields[f.name] == true) {
z = z + 1;
}
else
{
this.formFields[x].setSize(cm.getColumnWidth(x) + 2 + z);
}
x++;
}, this);
},
/**
* After submitting the values, this resets the form.
* @private
*/
resetFields : function ()
{
for (var i=0; i<this.formFields.length; i++)
{
this.formFields[i].setValue('');
if (i > 0)
{
this.formFields[i].disable();
}
}
},
/**
* Gets the values from all the fields and encodes,
* then submits them via an AJAX call.
* @private
*/
submitValues : function ()
{
if (this.url == null) return;
var values = [];
var entry = {};
for (var i=0; i<this.formFields.length; i++)
{
var name = this.formFields[i].getName();
var value = this.formFields[i].getValue();
entry = {name: name, value: value};
values[values.length] = entry;
}
var json = Ext.util.JSON.encode(values);
Ext.Ajax.request({
url: this.url,
params: json,
scope: this,
success : function ( response, result )
{
var answer = Ext.util.JSON.decode(response.responseText);
if (answer.success === true)
{
if (this.notifyAfterSubmit === true)
{
Ext.MessageBox.alert('Success', answer.message ? answer.message : this.notifySuccessMsg);
}
this.grid.store.reload();
this.resetFields();
}
else
{
Ext.MessageBox.alert('Error', answer.message ? answer.message : this.notifyErrorMsg);
}
},
failure : function ( response, result )
{
Ext.MessageBox.alert('Failure', 'Communication Failure');
}
});
},
/**
* Reconfigures the plugin - recreates the header.
* Runs also after grid reconfigure call
* @private
*/
reconfigure : function ()
{
this.createHeader();
},
/**
* (Re)Renders the Form Fields for the plugin.
* Runs after the grids afterRender, so that we know the grid has been rendered.
* @private
*/
afterRender : function ()
{
this.createFormFields();
}
});
Enjoy!