PDA

View Full Version : [2.x] Plugin : Ext.ux.grid.HeaderForm



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!

esoteric
9 Apr 2008, 10:40 AM
I updated the attachment files and added a screenshot so it would be easier to understand what this plugin is all about.

MeDavid
9 Apr 2008, 1:04 PM
Looks nice. Could also be usefull for searching (as opposed to other grid search plugins that require various clicks)

esoteric
9 Apr 2008, 1:14 PM
Good suggestion, I might look into incorporating that eventually.

Arthur.Blake
21 Apr 2008, 8:38 AM
I just discovered your post. I have been needing the same thing... I started with the similar plugin that was originally written by cocorossello and beefed it up a little bit.

(see cocorossello's original posts here http://extjs.com/forum/showthread.php?p=106262#post106262 and here (en espanol) http://extjs.com/forum/showthread.php?t=23967&page=2)

Here is a demo of my latest incarnation:

http://jabsorb.org/jabsorb-trunk/projectmetrics.html

Click on "FilteredGridPanel.js" at the bottom left to see the source code behind it.

The only major difference I see so far is that your filters don't resize when I resize columns... Does your plugin support combo boxes?

Maybe we can combine our work somehow to provide a truly great plugin??

What do you think?

esoteric
21 Apr 2008, 8:53 AM
I need to clarify, mine is not for filtering, though I suppose it could be adapted to do so, but it is for adding new records to the "database" that runs the grid. The current version does have some bugs, but the latest I am using has addressed and fixed several issues. I am working currently on getting it ready to update this thread.

Arthur.Blake
21 Apr 2008, 8:55 AM
Thanks for the clarification.
That makes sense-- I didn't realize that.

JorisA
21 Apr 2008, 3:07 PM
Looks cool. It would be nice to have the option to add the new data directly to the grid's store (without submitting first).

azbok
27 Apr 2008, 1:12 PM
Here is a demo of my latest incarnation:

http://arthurblake.thruhere.net:8084/projectmetrics.html

Maybe we can combine our work somehow to provide a truly great plugin??

What do you think?

I agree with combing efforts! Speaking of which, Arthur, I can't seem to connect to your website, so I can't check out what you have done.

Previously I found this thread: http://extjs.com/forum/showthread.php?t=31878 where mw-flow was looking for a filter widget, then I found this thread, then I started using esoteric's widget as a base for a filtered grid widget.

As a side note, I'd sure appreciate help on how to do this stuff the proper Ext way. I've only spent about 2.5 days (really solid days!), but it was mostly API research since this is my first attempt at monkeying up a widget.

Example: http://tehsux.net/ext/gridfilter.html

Hopefully soon I can publish the updated code to a test site, but here's the docs of what I've done so far:

Misc Items
Filtering
Upon submit, instead of using a separate ajax call, I use the
baseParams of the HttpProxy object within the data store.

Column Updates
Column Move, Column Hide / Show, Column Resizing

Viewport Height
Due to the extra height incurred from the header form, the viewport
(grid.scroller) part of the grid is made smaller. This brings the
scroll bar down button into view.

Header Form Key Mappings
ESC
Clears out the header form fields and also resets the grid view with
the original data shown (just does data store reload with all blank
baseParams parameter values)

DOWN Arrow
Focuses the viewport and selects the first row.

Page-DOWN
Focuses the viewport and selects the first row on
the 2nd page.

CTRL-DOWN
Focuses the viewport and re-scrolls (if necessary) to the last
selected row. For example, lets suppose you click the very last row,
then scroll up with the mouse, then click in the header field. If you
hit ctrl-down, it'll auto scroll the viewport so you can see the selected
row.

Grid Key Mappings
UP Arrow
Selects the previous row, but if the first row is already selected,
the last focused header form field element is then focused.

CTRL-UP
Focuses last focused header form field element.

Page-DOWN
Forces the selection to change to the first row on the next 'page'
where page is the next segment of rows within the viewport itself.
Example: If there are 100 rows but only 10 viewable and the first one
is currently in view, then the start of the next page down is row 11.
Row 11 is then shown at the top of the viewport.

Page-UP
Similar to Page-DOWN with respect to paging the viewport, but also
similar to UP where if already at the top, the last focused header
form field element is then focused.

HOME
Goes to the top of the viewport and selects the first row.

END
Goes to the end of the viewport and selects the last row.

Arthur.Blake
27 Apr 2008, 1:34 PM
Hi, sorry that URL was a very temporary URL.
The more permanent URL is here:

http://jabsorb.org/jabsorb-trunk/projectmetrics.html

I forgot to mention that here, but the main thread for this demo is here:

http://extjs.com/forum/showthread.php?p=158738

The demo is really for jabsorb, but it also happens to demonstrate the modified FilteredGridPanel in action as well.

azbok
28 Apr 2008, 7:35 PM
I edited my previous post and added an example link (although the filtering is not setup though). A bunch of the work I put in has nothing much to do with the filtering itself, it's the key mappings (which are definitely useful if you like to use the keyboard with web apps!).

In the near future, I'll further check out how you update and build your form filter fields and refine the code I currently have.

In the mean time, does anyone know of any examples of making your own custom RowSelectionModel?

rloco
14 Aug 2008, 8:23 AM
esoteric,

I solve this:
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.

To build the form, we just need the object ColumnModel, instead the field definiton of the object Store, because the form will only show this values.

In my propose, I create the form using the definition of the fields with the information that appears in the config object columnModel instead the store field definition.


createHeader : function ()
{
var rows = [];

var cm = this.grid.getColumnModel();
var fields = cm.config;
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>'
);

for(var x=0; x<fields.length; ++x) {
rows[rows.length] = this.row.apply({id: fields[x].dataIndex.toLowerCase()});
}

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>'
);
},
createFormFields : function ()
{
this.forms = [];
this.formFields = [];
this.formFieldConfigs = [];

var cm = this.grid.getColumnModel();
var fields = cm.config;
var store = this.grid.store;

for(var x=0, tabIndex=0; x<fields.length; ++x) {
var f=fields[x], storeField=store.fields.get(f.dataIndex);

var id = f.dataIndex.toLowerCase();

if (x==0)
{
this.firstField = f.dataIndex;
}

if (this.ignoreFields[f.dataIndex] == true)
{
this.formFields[x] = false;
continue;
}

this.formFieldConfigs[x] = {};
Ext.apply(this.formFieldConfigs[x], {
dataIndex: (this.fieldNames[f.dataIndex] ? this.fieldNames[f.dataIndex] : f.dataIndex),
tabIndex: ++tabIndex,
xtype: this.mapping[f.type] || 'textfield',
//fieldLabel: f.name,
disabled: ( this.dependOnFirst ? ( (x > 0) ? true : false ) : false ),
emptyText: (x == 0 ? this.defaultEmptyText : ''),
dateFormat: ( (storeField.type === 'date' && storeField.dateFormat) ? storeField.dateFormat : null ),
renderTo: 'new-row-' + id,
listeners : {
scope: this,
'focus' : function ()
{
this.editing = true;

if (this.firstField == f.dataIndex && 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[storeField.type] == 'textfield')
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
else if (this.mapping[storeField.type] == 'checkbox')
{
this.formFields[x] = new Ext.form.Checkbox(this.formFieldConfigs[x]);
}
else if (this.mapping[storeField.type] == 'datefield')
{
this.formFields[x] = new Ext.form.DateField(this.formFieldConfigs[x]);
}
else if (this.mapping[storeField.type] == 'numberfield')
{
this.formFields[x] = new Ext.form.NumberField(this.formFieldConfigs[x]);
}
else
{
this.formFields[x] = new Ext.form.TextField(this.formFieldConfigs[x]);
}
}

this.syncFields();
},
syncFields : function ()
{
var cm = this.grid.getColumnModel();
var fields = cm.config;

for(var x=0, z=0; x<fields.length; ++x){
if (this.ignoreFields[fields[x].dataIndex] == true) {
z = z + 1;
}
else {
this.formFields[x].setSize(cm.getColumnWidth(x) + 2 + z);
}
}
},

darkrift
17 Nov 2008, 5:54 PM
nice plugin, I had in mind to develop one and posted on the forum about it and Condor redirected me here.

I would like to congratulate you for it and I would not like to sound a bit harsh on your work but I'd like to help on a few problems I've encountered. A few "bugs" that I wouldn't even consider a bug rather then forgotten checks.

1) When the blur even is fired on the fields, you forget to validate if the field was at first rendered/created in your config (if the field is not in the ignoreFields) and you also forgot to check if the dependOnFirst to see if you have to disable them.



'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]);
}

if (this.dependOnFirst) {
for (var i = 1; i < this.formFields.length; i++) {
if (this.formFields[i] == false) continue;
this.formFields[i].disable();
}
}

}

this.editing = false;
}
},


2. you forgot the same check for the focus event when you enable it



'focus' : function ()
{
this.editing = true;

if (this.firstField == f.dataIndex && this.dependOnFirst)
{
this.editing = true;

for (var i=0; i<this.formFields.length; i++)
{
if (this.formFields[i] == false) continue;
this.formFields[i].enable();
}
}
},



One last thing I'd like to ask you is your logic on the setSize when you render the fields

you use a z value to determine the number of pixel to add to the field, but I don't see why you'd need it because on the 5th control, you'd add 5 more pixel to it and it would overlap on the next control after it.

I made this little change and it looks like when you add an editor field in the grid:



syncFields : function ()
{
var cm = this.grid.getColumnModel();
var fields = cm.config;

for(var x=0; x<fields.length; ++x){
if (this.ignoreFields[fields[x].dataIndex] !== true) {
this.formFields[x].setSize(cm.getColumnWidth(x) -1);
}
}
},


One last thing I'd change in the plugin, is rather than forcing the behavior to add a new record to the store with an ajax request, fire an event to the grid and let the user choose what he'd like to do.



submitValues: function(){
var values = {};

for (var i = 0; i < this.formFields.length; i++) {
if (this.formFields[i] == false)
continue;
values[this.formFields[i].getName()] = this.formFields[i].getValue();
}
this.grid.fireEvent('headertrigger', values);
},

then you can hook on the headertrigger event and do what you want with those fields: filter, inserting data to store, etc...


In the overall, you did pretty well to incorporate this new plugin.

one last thing, and this one will need a few tries to determine how too make it work: when you change the order of the columns by draging the column to another place, the headers disappear. I checked on the view to see if the header template was changed, but it seems like it is not update like if the getView() returned a copy of the iew and not the real reference to the real object.

My principal goal was to change the gridview to incorporate when I made my try on addins this feature but I fell on the same bug as this one.

Still, this plugin is really good.

Thank you for this plugin and keep up your work.

dawesi
26 Nov 2009, 2:45 PM
@darkrift... can you post your full code so that we can grab it for use... thanks