Continuing on with this series of blog posts it naturally follows that next we would look at form handling. In this post I will present a very lightweight and pragmatic solution to integrate Seam with the very nice ExtJS form (Ext.form) framework. However it can (and will) provide a foundation for more sophisticated solutions. For those that are unfamiliar with the Ext form API, I recommend you go and review it before proceeding with this read.
The Ext form API provides an Ajax based solution for both form submission and form loading. The focus of this entry will be on form submission, loading will be covered in the next installment after I get back from my trip to Montreal for the Candian GP. My design goals for this effort are pretty standard:
1. Minimize custom glue code.
2. Retain all the features of the Ext form API. Simply replace the native Ajax layer with Seam Remoting.
3. Allow forms to represent a graph of objects.
4. Thin server side Action tier to translate (decode) the form data into a domain model. This includes any type conversion that maybe required. This will be covered in the next installment.
So let's get started. Ext forms are created by defining a root level Ext.form.FormPanel (xtype='form'). The FormPanel is primarily responsible for defining and managing the layout of the form. The FormPanel is backed by an instance of Ext.form.BasicForm, which really represents the collection of fields that we normally think of as a form but without the actual form DOM element, it's Ajax remember. The BasicForm's primary responsibility is to support and coordinate validation activities both client & server. It delegates the actually Ajax handling to the Ext.form.Action class and its subtypes, Submit and Load. This Action layer is where we will target extensions to replace the existing submit logic via the Ext.Ajax with the seamComponent and remoteMethod solution from the previous posts.
To start with I defined a new component Ext.ux.form.SeamFormPanel (xtype="seamForm"). This class a provides a simple extension by overriding the createForm method. In the original an instance of Ext.form.BasicForm was constructed, I want to return a new instance of a new class Ext.ux.form.SeamBasicForm:
/**
* @class Ext.form.ux.SeamFormPanel
* @extends Ext.form.FormPanel
* Seam form panel. An extension of the Ext.form.FormPanel class for
* form handling via Seam remoting. The override here is for
* the creation of the SeamBasicForm instead of the BasicForm.
* the config passed to the constructor must specify the Seam
* component, target method and the request component. (see SeamRemotingProxy)
*
* Form fields are passed as arguments to the remote method in the
* order in which they were defined. The seam specific config options are
* seamComponent and remoteMethod. Optionally the 'useMap' config
* provides a more flexible solution for passing forms to Seam by mapping
* the form fields to a Seam.Remoting.Map. Remote methods that accept
* maps can merge form state into a graph of objects using the beanutils
* expression langauge for the the form field names.
*/
Ext.ux.form.SeamFormPanel = Ext.extend(Ext.form.FormPanel, {
// private
createForm: function(){
delete this.initialConfig.listeners;
return new Ext.ux.form.SeamBasicForm(null, this.initialConfig);
}
});
Ext.reg('seamForm', Ext.ux.form.SeamFormPanel);
The definition of the Ext.ux.form.SeamBasicForm is also very simple with overrides for the submit and load methods. These methods will employ the custom Actions that carry out the submission and loading of the form via Seam Remoting. The seamComponent and remoteMethod are applied to the SeamBasicForm via the config options that are passed to it at construction time, ie the SeamFormPanel's initialConfig object.
/**
* @class Ext.ux.form.SeamBasicForm
* @extends Ext.form.BasicForm
* Supplies the functionality to do "actions" on forms and initialize
* Ext.form.Field types on existing markup.
* By default, Ext Forms are submitted through Ajax, using {@link Ext.form.Action}. However
* this version will use the Seam Remoting abstraction to Ajax.
* @constructor
* @param {Mixed} el The form element or its id
* @param {Object} config Configuration options
*/
Ext.ux.form.SeamBasicForm = function(el, config) {
Ext.ux.form.SeamBasicForm.superclass.constructor.call(this, el, config);
};
Ext.extend(Ext.ux.form.SeamBasicForm, Ext.form.BasicForm, {
/**
* Shortcut to do a submit action. This override calls the doAction with 'seamSubmit'.
* @param {Object} options The options to pass to the action (see {@link #doAction}
* for details)
* @return {BasicForm} this
*/
submit: function(options){
this.doAction('seamSubmit', options);
return this;
},
/**
* Shortcut to do a load action. This override calls the doAction with 'seamLoad'.
* @param {Object} options The options to pass to the action (see {@link #doAction} for
* details)
* @return {BasicForm} this
*/
load: function(options){
this.doAction('seamLoad', options);
return this;
}
});
And now we get to the meat of the solution for this installment, the Ext.form.Action.SeamSubmit class. This class is where the interaction with the Seam Remoting framework takes place. I contains overrides for getParams, run and processSubmitResponse. Get params, by default, will return an array of the form fields in the order in which they were defined on the form. If the 'useMap' config option was specified on the SeamFormPanel then a Seam.Remoting.Map will be populated with the fields with the form field name as the key and the value as the form field value. The processSubmitResponse adds the exception prototcol handling that we developed in the Integrating ExtJS with Seam: Error Handling post. The run method does all the work.
/**
* @class Ext.form.Action.SeamSubmit
* @extends Ext.form.Action.Submit
* A class which handles submission of data from {@link Ext.form.BasicForm Form}s
* and processes the returned response.
* Instances of this class are only created by a {@link Ext.form.BasicForm Form} when
* submitting.
* A response packet must contain a boolean success property, and, optionally
* an errors property. The errors property contains error
* messages for invalid fields.
* By default, response packets are assumed to be JSON, so a typical response
* packet may look like this:
* {success: false, errors: {
* clientCode: "Client not found",
* portOfLoading: "This field must not be null"
* }}
* Other data may be placed into the response for processing the the
* {@link Ext.form.BasicForm}'s callback
* or event handler methods. The object decoded from this JSON is available in the
* {@link #result} property.
*/
Ext.form.Action.SeamSubmit = function(form, options){
Ext.form.Action.SeamSubmit.superclass.constructor.call(this, form, options);
};
Ext.extend(Ext.form.Action.SeamSubmit, Ext.form.Action.Submit, {
/**
* Gets the value form form fields. There are a copule of ways in which the
* request can be constructed. 1. Using the order in which the fields were
* defined on the form this solution works well for forms with a few fields.
* 2. Using a map for the request params. This solution provides a means to
* merge in complete object graph.
*/
getParams: function() {
return this.form.useMap === true ? this.getMapParams() : this.getFieldParams();
},
/**
* Wtih this strategy the fields registers on the associated form are added
* to the arg array in the order that they were defined at construction time.
*/
getFieldParams: function() {
var params = [];
var values = this.form.getValues(false);
for (value in values) {
params.push(values[value]);
};
return params;
},
/**
* This strategy uses a map of name/value pairs using a Seam.Remoting.Map.
* With this approach forms can be created that represent an object graph
* of arbitrary depth. The solution assumes that form field names adhere
* to the org.apache.BeanUtil property expressions.
*/
getMapParams: function() {
var map = new Seam.Remoting.Map();
this.form.items.each(function(field){
map.put(field.name, field.getValue());
});
var params = [];
params.push(map);
return params;
},
// private
run: function() {
var o = this.options;
if (o.clientValidation === false || this.form.isValid()) {
var args = this.getParams();
args.push(this.processSubmitResponse.createDelegate(this));
var form = this.form;
form.remoteMethod.apply(form.seamComponent, args);
} else if (o.clientValidation !== false) {
// client validation failed
this.failureType = Ext.form.Action.CLIENT_INVALID;
this.form.afterAction(this, false);
}
},
/**
* This override performs a little protocol translation.
*/
processSubmitResponse: function(response){
var result = Ext.decode(response);
if (result.exception) {
this.failure(response);
return;
}
return Ext.form.Action.SeamSubmit.superclass.success.call(this, {
responseText: response
});
}
});
The last bit of code on the client is to register our new action with the Ext Action types.
/**
* Add Ext.form.Action.SeamSubmit to the Action types array.
*/
Ext.apply(Ext.form.Action.ACTION_TYPES, {
'seamSubmit': Ext.form.Action.SeamSubmit
});
Finally here is an example that uses the new SeamForm.
var userAction = Seam.Component.getInstance('userAction');
var loginPanel = new Ext.ux.form.SeamFormPanel({
seamComponent: userAction,
remoteMethod: userAction.authenticate,
id: 'login-form',
baseCls: 'x-plain',
region: 'south',
labelWidth: 120,
defaults: {
width: 200,
validationEvent: false,
allowBlank: false
},
frame: false,
height: 70,
items:[{
xtype: 'emailTextField'
},{
xtype: 'textfield',
fieldLabel:'Password',
inputType: 'password',
name: 'password',
}]
});
Click here to download the SeamForm.js file.