PDA

View Full Version : Form request/Error json format



Ronaldo
23 Jul 2009, 3:53 AM
Hi all,

I have 2 remarks/requests:

Form should use a Store
While diving into the form/submit code (I have some special needs), I find it a pity that a form doesn't use a store for saving/loading data.
True, a store for a single record might be overkill, but (for example) it would be easy to hook up the form's store to a connecting grid store, and thus walk through records while being able to bulk-update everything in the grid's store.
A form is in fact 'only' a view of the record in the store.

Moreover, the save/load mechanism of the form would be exact the same as in the grid.
Now I'm finding some slight differences for grid crud actions compared to a form (ie Action[Submit]).
(I'm trying to use the same grid-bulk server side controllers for saving and loading the grid, so I won't have to explicitely write them too).

For example, when returning errors when saving a form, you need to write:


{
success: false,
errors: [{id:'field','msg:'you have an error'}]
}

But when loading a grid, success=false means the request has gone wrong (ie 404 or something) Am I right here?

Also, I can only return max 1 error message per field... While this seams logical, I'd like to inform the user that a) An email address is required, and b) An empty email address doesn't conform to the email address syntax (x@y.z).
Now this example is simple, but there are more complex businessrules out there.
(this field cannot be empty because that field is set and ...)

Standardize JSON results
While I'm here, I'd also make a request to even more standardize the expected json results to look like this:


{
success: true,
data: [{id:1, ....}, .... {id:5, ...}], (Always an array)
errors: [{
id: 1, field: 'email', msg:'field is required'
},
... More errors for the same id, or for the same field possible
]
}

For processing, I find it very handy to always return an array, even if it contains just one item. Thus I can always use a loop, that is skipped if the array is empty.

If there are no errors, the errors parameter can me omitted or simply be an empty array ([]). If it's omitted or empty, there are no errors and the request was successful (Otherwise you should return an error message!)

If the errors[x].field parameter is omitted, it means that the error targets the record, not a specific field, as in:


{
...
errors: [{
id: 1, msg:'Referential Integrity Error: You cannot delete a customer that have orders attached.'
}
}

The success property should have a single meaning as described above.
With this setup, I can return multiple errors per field or per record, and it's suitable for grid-batch actions and form-actions.

Best regards,
Ronaldo

Condor
23 Jul 2009, 4:04 AM
Funny, I've already done that in my project:
I extended Ext.action.Load and Submit to handle both data and errors and added support for 'global' (not related to field) errors.

ps. Wel een domain (http://www.twensoc.nl/), maar geen website?

Ronaldo
23 Jul 2009, 4:44 AM
Hej Condor,

U Dutch too :) ? And yes, I didn't take the time to put up my own website yet.
Too busy :)

I'm in the process of debugging the code, see what I need and yes, I did override some things too. So far I have:



/**
* The default implementation expects a data property.
* If a rows property is returned, copy that to the data property
* so everything else works as expected.
*/
Ext.override(Ext.form.Action.Load, {
// private
success : function(response){
var result = this.processResponse(response);
if(result.rows) {
result.data = Ext.isArray(result.rows) ? result.rows[0] : result.rows;
delete(result.rows);
}
if(result === true || !result.success || !result.data){
this.failureType = Ext.form.Action.LOAD_FAILURE;
this.form.afterAction(this, false);
return;
}
this.form.clearInvalid();
this.form.setValues(result.data);
this.form.afterAction(this, true);
}
});




Ext.override(Ext.form.Action.Submit, {
// private

// Changed to suppres the form parameters, as I only want a json string to be posted.
run : function(){
var o = this.options;
var method = this.getMethod();
var isGet = method == 'GET';
if(o.clientValidation === false || this.form.isValid()){
Ext.Ajax.request(Ext.apply(this.createCallback(o), {
//form:this.form.el.dom,
url:this.getUrl(isGet),
method: method,
headers: o.headers,
params:!isGet ? this.getParams() : null,
isUpload: this.form.fileUpload
}));
}else if (o.clientValidation !== false){ // client validation failed
this.failureType = Ext.form.Action.CLIENT_INVALID;
this.form.afterAction(this, false);
}
},

// private
success : function(response){
var result = this.processResponse(response);
// Modified to handle result.rows too and copy its contents to result.data
if(result.rows) {
result.data = Ext.isArray(result.rows) ? result.rows[0] : result.rows;
delete(result.rows);
}
//
if(result === true || result.success){
this.form.afterAction(this, true);
return;
}
if(result.errors){
this.form.markInvalid(result.errors);
this.failureType = Ext.form.Action.SERVER_INVALID;
}
this.form.afterAction(this, false);
}
});




/**
* The default implementation expects
* {id:'fieldname', msg:'errormessgae'}
*
* Adapt this code to process
* {id:12, field:'fieldname', msg:'errormessage'}
* where id is the id of the record.
*
* For a form,
* the id is not needed, but it's the same server-side
* code that returns this json and that code is also
* able to save multiple records in a batch-process.
*/

Ext.override(Ext.form.BasicForm, {
markInvalid : function(errors){
if(Ext.isArray(errors)){
for(var i = 0, len = errors.length; i < len; i++){
var fieldError = errors[i];
var f = this.findField(fieldError.field);
if(f){
f.markInvalid(fieldError.msg);
}
}
}
return this;
}
});


And all that just to get all the basic form functionality:



Ext.twensoc.FormPanel = Ext.extend(Ext.FormPanel, {
labelWidth: 75,
frame:true,
title: 'Form',
bodyStyle:'padding:5px 5px 0',
width: 350,
defaults: {width: 230},
defaultType: 'textfield',
trackResetOnLoad: true,
//name:'unknownForm',
initComponent: function() {
this.items = this.createContent();
Ext.twensoc.FormPanel.superclass.initComponent.call(this);
this.addEvents("beforesave","beforeload","afterload","aftersave");
this.on({
render:{scope:this, fn:this.onFormRender, delay: 100},
beforedestroy:{scope:this, fn:this.onBeforeDestroy},
afterload:{scope:this, fn:this.onAfterLoad},
aftersave:{scope:this, fn:this.onAfterSave}
});
this.form.on({
actioncomplete:{scope:this, fn:this.onActionComplete},
actionfailed:{scope:this, fn:this.onActionFailed}
});
this.primeRecord();
this.primeForm();
},
onBeforeDestroy: function(form) {
},
onAfterLoad: function(form, cfg) {
this.updateTitle();
this.tabId = this.name+this.record.id;
},
onAfterSave: function(form, cfg) {
this.updateTitle();
this.tabId = this.name+this.record.id;
},
updateTitle: function(txt) {
txt = txt || this.getTitle();
if(txt) {
this.setTitle(txt);
}
},
getTitle: function() {
return this.name+(this.isNew() ? '' : ' '+this.record.id);
},
onFormRender: function() {
this.load();
},
onActionComplete: function(form, action) {
if(action.type==='load') {
this.record = action.result.data;
this.fireEvent("afterload", this, this.record);
} else if(action.type==='submit') {
if(this.isNew()) {
var f = this.form.findField('id');
var newId = 0;
if(Ext.isArray(action.result.data) && action.result.data.length > 0) {
newId = action.result.data[0].id;
} else {
newId = action.result.data.id;
}
if(newId) {
f.setValue(newId);
f.originalValue = f.getValue();
}

}
var v = this.form.getValues();
for(var i in v) {
this.record[i] = v[i];
}
this.fireEvent("aftersave", this, this.record, this.entityName);
}
},
onActionFailed: function(form, action) {
if(action.type==='submit' && action.result.errors) {
// Copy only error messages without a field (record messages).
var errs=[], e = action.result.errors;
for(var i=0; i<e.length; i++) {
if(!e[i].field) {
errs.push(e[i]);
}
}
if(errs.length) {
Ext.twensoc.ShowError('Error', errs, true);
}
}
},
isNew : function() {
return !this.record || !this.record.id;
},
isDirty: function(){
return this.skipDirty === true ? false : this.getDirtyFields().length > 0;// || !this.record || !this.record.id
},
getDirtyFields: function(){
var flds = [];
this.form.items.eachKey(function(key, f){
if (f.isFormField && f.isDirty()) {
flds[flds.length] = f;
}
}, this);
return flds;
},
load: function(cfg, force){
if ((this.record && this.record.id) && (force || !this.isLoaded)) {
if(!cfg) cfg = {};
Ext.apply(cfg, {
url: cfg.url || ((this.urlprefix ? this.urlprefix : "") + this.entity + '/'+this.record.id),
method:'GET',
waitTitle:'Please wait',
waitMsg:'Loading data',
params: Ext.apply(cfg.params || {}, this.passedParams)
});
this.fireEvent("beforeload", this, cfg);
this.form.load(cfg);
this.isLoaded = true;
}
},
save: function() {
var flds = this.getDirtyFields();
var data = {id: this.isNew() ? 0 : this.record.id};
Ext.each(flds, function(fld) {
data[fld.name] = fld.getValue();
}, this);
var jsonData = Ext.encode(data);

this.form.submit({
url: (this.urlprefix ? this.urlprefix : "") + this.entity + (this.isNew() ? '/create' : '/save/'+this.record.id),
params: {
rows: jsonData,
isForm: true
},
waitTitle:'Please wait',
waitMsg:'Saving data',
});
},
primeRecord: function() {
// placeholder
},
primeForm: function() {
this.applyFormBusinessRules();
},
applyFormBusinessRules: function() {
// placeholder
},
createContent : function() {
// placeholder
}
});


So that any form I need in my app looks like, this, which is basically only the form definition ;).



Ext.app.UserForm = Ext.extend(Ext.twensoc.FormPanel, {
urlprefix: 'admin/',
entity: 'user',
name: 'Ext.app.UserForm',
initComponent : function() {
Ext.app.UserForm.superclass.initComponent.call(this);
},
createContent : function() {
this.buttons=[{
text:'Save',
scope: this,
handler:function(){
this.save();
}
}];

return [ {
name : 'id',
xtype : 'hidden'
}, {
name : 'username',
fieldLabel : 'Username',
allowBlank : false
}, {
name : 'email',
fieldLabel : 'Email',
allowBlank : false
}, {
name : 'firstname',
fieldLabel : 'Firstname'
}, {
name : 'infix',
fieldLabel : 'Infix'
}, {
name : 'lastname',
fieldLabel : 'Lastname'
} ];
}
});


Cheers!
Ronald

Ronaldo
23 Jul 2009, 4:48 AM
One more remark;

I strongly feel that I'm not the only one that needs this basic behaviour... You admitted that yourself when saying you'd done stuff like this too.
Could we somehow not integrate this as a 'bigger' standard component in Ext? It should be basic behaviout to be used out of the box :)

Same goes for the StaticParentCombo (https://extjs.com/forum/showthread.php?t=75213) and the GridBatchErrorDisplay (https://extjs.com/forum/showthread.php?t=72204) extensions.

Ronaldo