Index: build/apidoc_config/Menu.txt
===================================================================
--- build/apidoc_config/Menu.txt	(revision 486)
+++ build/apidoc_config/Menu.txt	(working copy)
@@ -59,6 +59,12 @@
 
 File: MapPanel  (GeoExt/widgets/MapPanel.js)
 
+Group: Form {
+   File: SearchAction (GeoExt/widgets/form/Form.js)
+   File: BasicForm (GeoExt/widgets/form/Form.js)
+   File: FormPanel (GeoExt/widgets/form/Form.js)
+   }  # Group: Form
+
 Group: Index  {
 
    Index: Everything
Index: tests/lib/GeoExt/widgets/form/Form.html
===================================================================
--- tests/lib/GeoExt/widgets/form/Form.html	(revision 0)
+++ tests/lib/GeoExt/widgets/form/Form.html	(revision 0)
@@ -0,0 +1,404 @@
+<!DOCTYPE html>
+<html debug="true">
+  <head>
+    <script type="text/javascript" src="../../../../../../openlayers/lib/OpenLayers.js"></script>
+    <script type="text/javascript" src="../../../../../../ext/adapter/ext/ext-base.js"></script>
+    <script type="text/javascript" src="../../../../../../ext/ext-all-debug.js"></script>
+    <script type="text/javascript" src="../../../../../lib/GeoExt.js"></script>
+
+    <script type="text/javascript">
+
+    function test_filterFromForm(t) {
+        t.plan(27);
+
+        /*
+         * Set up
+         */
+
+        var form, filter, fields = [];
+
+        fields.push(new Ext.form.TextField({
+            name: "foo0",
+            value: "bar0"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo1__eq",
+            value: "bar1"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo2__ne",
+            value: "bar2"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo3__lt",
+            value: "bar3"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo4__le",
+            value: "bar4"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo5__gt",
+            value: "bar5"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo6__ge",
+            value: "bar6"
+        }));
+
+        fields.push(new Ext.form.TextField({
+            name: "foo7__like",
+            value: "bar7"
+        }));
+
+        form = new Ext.form.FormPanel({
+            renderTo: "form",
+            items: fields
+        });
+
+        /*
+         * Test
+         */
+
+        // 26 tests
+        filter = GeoExt.form.filterFromForm(form);
+
+        t.ok(filter instanceof OpenLayers.Filter.Logical,
+             "GeoExt.form.filterFormForm returns a logical filter");
+
+        t.eq(filter.type, OpenLayers.Filter.Logical.AND,
+             ["GeoExt.form.filterFormForm returns a logical AND filter if",
+              "logicalOp is undefined"].join(" "));
+
+        t.eq(filter.filters[0].type, OpenLayers.Filter.Comparison.EQUAL_TO,
+             "GeoExt.form.filterFormForm creates correct filter type");
+        t.eq(filter.filters[0].property, "foo0",
+             "GeoExt.form.filterFormForm creates correct filter prop");
+        t.eq(filter.filters[0].value, "bar0",
+             "GeoExt.form.filterFormForm creates correct filter value");
+
+        t.eq(filter.filters[1].type, OpenLayers.Filter.Comparison.EQUAL_TO,
+             "GeoExt.form.filterFormForm creates correct filter type (__eq)");
+        t.eq(filter.filters[1].property, "foo1",
+             "GeoExt.form.filterFormForm creates correct filter prop (__eq)");
+        t.eq(filter.filters[1].value, "bar1",
+             "GeoExt.form.filterFormForm creates correct filter value (__eq)");
+
+        t.eq(filter.filters[2].type, OpenLayers.Filter.Comparison.NOT_EQUAL_TO,
+             "GeoExt.form.filterFormForm creates correct filter type (__ne)");
+        t.eq(filter.filters[2].property, "foo2",
+             "GeoExt.form.filterFormForm creates correct filter prop (__ne)");
+        t.eq(filter.filters[2].value, "bar2",
+             "GeoExt.form.filterFormForm creates correct filter value (__ne)");
+
+        t.eq(filter.filters[3].type, OpenLayers.Filter.Comparison.LESS_THAN,
+             "GeoExt.form.filterFormForm creates correct filter type (__lt)");
+        t.eq(filter.filters[3].property, "foo3",
+             "GeoExt.form.filterFormForm creates correct filter prop (__lt)");
+        t.eq(filter.filters[3].value, "bar3",
+             "GeoExt.form.filterFormForm creates correct filter value (__lt)");
+
+        t.eq(filter.filters[4].type, OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
+             "GeoExt.form.filterFormForm creates correct filter type (__le)");
+        t.eq(filter.filters[4].property, "foo4",
+             "GeoExt.form.filterFormForm creates correct filter prop (__le)");
+        t.eq(filter.filters[4].value, "bar4",
+             "GeoExt.form.filterFormForm creates correct filter value (__le)");
+
+        t.eq(filter.filters[5].type, OpenLayers.Filter.Comparison.GREATER_THAN,
+             "GeoExt.form.filterFormForm creates correct filter type (__gt)");
+        t.eq(filter.filters[5].property, "foo5",
+             "GeoExt.form.filterFormForm creates correct filter prop (__gt)");
+        t.eq(filter.filters[5].value, "bar5",
+             "GeoExt.form.filterFormForm creates correct filter value (__gt)");
+
+        t.eq(filter.filters[6].type, OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO,
+             "GeoExt.form.filterFormForm creates correct filter type (__ge)");
+        t.eq(filter.filters[6].property, "foo6",
+             "GeoExt.form.filterFormForm creates correct filter prop (__ge)");
+        t.eq(filter.filters[6].value, "bar6",
+             "GeoExt.form.filterFormForm creates correct filter value (__ge)");
+
+        t.eq(filter.filters[7].type, OpenLayers.Filter.Comparison.LIKE,
+             "GeoExt.form.filterFormForm creates correct filter type (__like)");
+        t.eq(filter.filters[7].property, "foo7",
+             "GeoExt.form.filterFormForm creates correct filter prop (__like)");
+        t.eq(filter.filters[7].value, "bar7",
+             "GeoExt.form.filterFormForm creates correct filter value (__like)");
+
+        // 1 test
+        filter = GeoExt.form.filterFromForm(form,
+                                            OpenLayers.Filter.Logical.OR);
+
+        t.eq(filter.type, OpenLayers.Filter.Logical.OR,
+             ["GeoExt.form.filterFormForm returns a logical OR filter if",
+              "logicalOp is OpenLayers.Filter.Logical.OR"].join(" "));
+    }
+
+    function test_SearchAction_constructor(t) {
+        t.plan(1);
+
+        /*
+         * Set up
+         */
+
+        var form, action, protocol;
+
+        form = new Ext.form.BasicForm(Ext.get("form"));
+        protocol = new OpenLayers.Protocol();
+        action = new GeoExt.form.SearchAction(form, {protocol: protocol});
+
+        /*
+         * Test
+         */
+
+        t.ok(action.options.protocol == protocol,
+             "SearchAction constructor properly sets protocol in options");
+    }
+
+    function test_SearchAction_run(t) {
+        t.plan(1);
+
+        /*
+         * Set up
+         */
+
+        var field, form, action, protocol;
+
+        var field = new Ext.form.TextField({
+            name: "foo__eq",
+            value: "bar"
+        });
+
+        form = new Ext.form.FormPanel({
+            renderTo: "form",
+            items: [field]
+        });
+
+        protocol = new OpenLayers.Protocol({
+            read: function(options) {
+                t.ok(options.filter instanceof OpenLayers.Filter.Logical,
+                     "run calls protocol.read with a logical filter");
+            }
+        });
+
+        action = new GeoExt.form.SearchAction(form.getForm(), {
+            protocol: protocol,
+            clientValidation: false
+        });
+
+        /*
+         * Test
+         */
+       action.run();
+    }
+
+    function test_BasicForm_constructor(t) {
+        t.plan(1);
+
+        /*
+         * Set up
+         */
+        
+        var protocol, form;
+
+        protocol = new OpenLayers.Protocol();
+
+        form = new GeoExt.form.BasicForm(Ext.get("form"), {
+            protocol: protocol
+        });
+
+        /*
+         * Test
+         */
+
+        t.ok(form.protocol == protocol,
+             "BasicForm constructor properly sets protocol in the instance");
+    }
+
+    function test_BasicForm_doAction(t) {
+        t.plan(3);
+
+        /*
+         * Set up
+         */
+        
+        var protocol, form;
+
+        protocol = new OpenLayers.Protocol({
+            read: function(options) {
+                t.ok(options.filter instanceof OpenLayers.Filter.Logical,
+                     ["doAction calls read on the form's protocol, read",
+                      "is passed a logical filter"].join(" "));
+            }
+        });
+
+        form = new GeoExt.form.BasicForm(Ext.get("form"), {
+            protocol: protocol,
+            getValues: function() {
+                return {"foo0__eq": "bar0", "foo1__like": "bar1"};
+            }
+        });
+
+        /*
+         * Test
+         */
+
+        // 1 test
+        var tmp = form.doAction("search");
+        t.ok(tmp == form,
+             "doAction returns the form instance");
+        t.wait_result(1);
+
+        // 1 test
+        protocol = new OpenLayers.Protocol({
+            read: function(options) {
+                t.ok(options.filter instanceof OpenLayers.Filter.Logical,
+                     ["doAction calls read on the protocol it is given,",
+                      "read is passed a logical filter"].join(" "));
+            }
+        });
+        form.doAction("search", {protocol: protocol});
+        t.wait_result(1);
+    }
+
+    function test_FormPanel_constructor(t) {
+        t.plan(3);
+
+        /*
+         * Set up
+         */
+
+        var protocol, form;
+
+        protocol = new OpenLayers.Protocol();
+
+        form = new GeoExt.form.FormPanel({
+           renderTo: "form",
+           protocol: protocol
+        });
+
+        /*
+         * Test
+         */
+
+        t.ok(form.protocol == protocol,
+             "FormPanel constructor sets protocol in the instance");
+        t.ok(form.getForm() instanceof GeoExt.form.BasicForm,
+             ["FormPanel constructor creates a GeoExt.form.BasicForm as",
+              "its internal form"].join(" "));
+        t.ok(form.getForm().protocol == protocol,
+             "FormPanel constructor sets protocol in internal form");
+    }
+
+    function test_FormPanel_actioncomplete(t) {
+        t.plan(2);
+
+        /*
+         * Set up
+         */
+
+        var response, protocol, field, form;
+        var success = 0;
+
+        response = new OpenLayers.Protocol.Response({
+            code: OpenLayers.Protocol.Response.SUCCESS
+        });
+
+        protocol = new OpenLayers.Protocol({
+            read: function(o) {
+                o.callback.call(o.scope, response);
+                return response;
+            }
+        });
+
+        field = new Ext.form.TextField({
+            name: "foo0",
+            value: "bar0"
+        });
+
+        form = new GeoExt.form.FormPanel({
+           renderTo: "form",
+           protocol: protocol,
+           items: [field],
+           listeners: {
+               actioncomplete: function(form, action) {
+                   t.ok(action.response == response,
+                        "actioncomplete passed expected response");
+                   success++;
+               }
+           }
+        });
+
+        /*
+         * Test
+         */
+
+        form.search();
+        t.delay_call(1, function() {
+            t.eq(success, 1,
+                 "actioncomplete called when search request succeeds");
+        });
+    }
+
+    function test_FormPanel_actionfailed(t) {
+        t.plan(2);
+
+        /*
+         * Set up
+         */
+
+        var response, protocol, field, form;
+        var failure = 0;
+
+        response = new OpenLayers.Protocol.Response({
+            code: OpenLayers.Protocol.Response.FAILURE
+        });
+
+        protocol = new OpenLayers.Protocol({
+            read: function(o) {
+                o.callback.call(o.scope, response);
+                return response;
+            }
+        });
+
+        field = new Ext.form.TextField({
+            name: "foo0",
+            value: "bar0"
+        });
+
+        form = new GeoExt.form.FormPanel({
+           renderTo: "form",
+           protocol: protocol,
+           items: [field],
+           listeners: {
+               actionfailed: function(form, action) {
+                   t.ok(action.response == response,
+                        "actionfailed passed expected response");
+                   failure++;
+               }
+           }
+        });
+
+        /*
+         * Test
+         */
+
+        form.search();
+        t.delay_call(1, function() {
+            t.eq(failure, 1,
+                 "actionfailed called when search request fails");
+        });
+    }
+
+    </script>
+  <body>
+    <div id="form"></div>
+  </body>
+</html>
Index: tests/list-tests.html
===================================================================
--- tests/list-tests.html	(revision 486)
+++ tests/list-tests.html	(working copy)
@@ -10,4 +10,5 @@
   <li>lib/GeoExt/data/WMSCapabilitiesReader.html</li>
   <li>lib/GeoExt/widgets/MapPanel.html</li>
   <li>lib/GeoExt/widgets/Popup.html</li>
+  <li>lib/GeoExt/widgets/form/Form.html</li>
 </ul>
Index: lib/GeoExt.js
===================================================================
--- lib/GeoExt.js	(revision 486)
+++ lib/GeoExt.js	(working copy)
@@ -70,7 +70,8 @@
             "GeoExt/data/WMSCapabilitiesStore.js",
             "GeoExt/data/ProtocolProxy.js",
             "GeoExt/widgets/MapPanel.js",
-            "GeoExt/widgets/Popup.js"
+            "GeoExt/widgets/Popup.js",
+            "GeoExt/widgets/form/Form.js"
         );
 
         var agent = navigator.userAgent;
Index: lib/GeoExt/widgets/form/Form.js
===================================================================
--- lib/GeoExt/widgets/form/Form.js	(revision 0)
+++ lib/GeoExt/widgets/form/Form.js	(revision 0)
@@ -0,0 +1,334 @@
+/* Copyright (C) 2008-2009 The Open Source Geospatial Foundation
+ * Published under the BSD license.
+ * See http://geoext.org/svn/geoext/core/trunk/license.txt for the full text
+ * of the license.
+ * 
+ * pending approval */
+
+Ext.namespace("GeoExt.form");
+
+/**
+ * Function: GeoExt.form.filterFromForm
+ * Create an {OpenLayers.Filter} object from a {Ext.form.BasicForm}
+ *     instance or a {Ext.form.FormPanel}.
+ *
+ * Parameters:
+ * form - {Ext.form.BasicForm|Ext.form.FormPanel}
+ * logicalOp - {String} Either {OpenLayers.Filter.Logical.AND}
+ *     or {OpenLayers.Filter.Logical.OR}, set to
+ *     {OpenLayers.Filter.Logical.AND} if null or
+ *     undefined.
+ *
+ * Returns:
+ * {OpenLayers.Filter}
+ */
+GeoExt.form.filterFromForm = function(form, logicalOp) {
+    if(form instanceof Ext.form.FormPanel) {
+        form = form.getForm();
+    }
+    var filters = [], values = form.getValues(false);
+    for(var prop in values) {
+        var s = prop.split("__");
+
+        var value = values[prop], type;
+
+        if(s.length > 1 && 
+           (type = GeoExt.form.filterFromForm.FILTER_MAP[s[1]]) !== undefined) {
+            prop = s[0];
+        } else {
+            type = OpenLayers.Filter.Comparison.EQUAL_TO;
+        }
+
+        filters.push(
+            new OpenLayers.Filter.Comparison({
+                type: type,
+                value: value,
+                property: prop
+            })
+        );
+    }
+
+    return new OpenLayers.Filter.Logical({
+        type: logicalOp || OpenLayers.Filter.Logical.AND,
+        filters: filters
+    });
+};
+
+/**
+ * Constant: GeoExt.form.filterFromForm.FILTER_MAP
+ * An object mapping operator strings as found in field names to
+ *     {OpenLayers.Filter.Comparison} types.
+ */
+GeoExt.form.filterFromForm.FILTER_MAP = {
+    "eq": OpenLayers.Filter.Comparison.EQUAL_TO,
+    "ne": OpenLayers.Filter.Comparison.NOT_EQUAL_TO,
+    "lt": OpenLayers.Filter.Comparison.LESS_THAN,
+    "le": OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
+    "gt": OpenLayers.Filter.Comparison.GREATER_THAN,
+    "ge": OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO,
+    "like": OpenLayers.Filter.Comparison.LIKE
+};
+
+/**
+ * Class: GeoExt.form.SearchAction
+ * An specific {Ext.form.Action} to be used when using a form to do
+ * trigger search requests througn an {OpenLayers.Protocol}.
+ *
+ * This action builds an {OpenLayers.Filter} from the form and passes
+ * this filter to its protocol's read method. The form fields must be
+ * named after a specific convention so that an appropriate 
+ * {OpenLayers.Filter.Comparison} filter is created for each
+ * field. For example a field with the name "foo__like" would
+ * result in an {OpenLayers.Filter.Comparison} of type
+ * {OpenLayers.Filter.Comparison.LIKE}. Here is the convention:
+ * * <name>__eq: {OpenLayers.Filter.Comparison.EQUAL_TO}
+ * * <name>__ne: {OpenLayers.Filter.Comparison.NOT_EQUAL_TO}
+ * * <name>__lt: {OpenLayers.Filter.Comparison.LESS_THAN}
+ * * <name>__le: {OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO}
+ * * <name>__gt: {OpenLayers.Filter.Comparison.GREATER_THAN}
+ * * <name>__ge: {OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO}
+ * * <name>__like: {OpenLayers.Filter.Comparison.LIKE}
+ *
+ * In most cases your would not directly create {GeoExt.form.SearchAction}
+ * objects, but use {<GeoExt.form.FormPanel>} instead.
+ *
+ * Example:
+ *
+ * (start code)
+ * var formPanel = new Ext.form.Panel({
+ *     renderTo: "formpanel",
+ *     items: [{
+ *         xtype: "textfield",
+ *         name: "name__like",
+ *         value: "mont"
+ *     }, {
+ *         xtype: "textfield",
+ *         name: "elevation__ge",
+ *         value: "2000"
+ *     }]
+ * });
+ *
+ * var searchAction = new GeoExt.form.SearchAction(formPanel.getForm(), {
+ *     protocol: new OpenLayers.Protocol.WFS({
+ *         url: "http://publicus.opengeo.org/geoserver/wfs",
+ *         featureType: "tasmania_roads",
+ *         featureNS: "http://www.openplans.org/topp"
+ *     })
+ * });
+ *
+ * formPanel.getForm().doAction(searchAction, {
+ *     callback: function(response) {
+ *         // response.features includes the features read
+ *         // from the server through the protocol
+ *     }
+ * });
+ * (end)
+ *
+ */
+GeoExt.form.SearchAction = Ext.extend(Ext.form.Action, {
+    /**
+     * Property: type
+     * {String} The action type string.
+     */
+    type: "search",
+
+    /**
+     * APIProperty: response
+     * {OpenLayers.Protocol.Response} A read-only reference to the
+     *     OpenLayers.Protocol.Response object resulting from the
+     *     search request.
+     */
+    response: null,
+
+    /**
+     * Constructor: GeoExt.form.SearchAction
+     * Creates a SearchAction instance.
+     *
+     * Parameters:
+     * form - {Ext.form.BasicForm} The form.
+     * options - {Object} The options passed to the internal
+     *     protocol's read method.
+     */
+    constructor: function(form, options) {
+        GeoExt.form.SearchAction.superclass.constructor.call(this, form, options);
+    },
+
+    /**
+     * Method: run
+     * Run the action.
+     */
+    run: function() {
+        var o = this.options;
+        var f = GeoExt.form.filterFromForm(this.form);
+        if(o.clientValidation === false || this.form.isValid()){
+            this.response = o.protocol.read(
+                Ext.applyIf({
+                    filter: f,
+                    callback: this.handleResponse,
+                    scope: this
+                }, o)
+            );
+        } else if(o.clientValidation !== false){
+            // client validation failed
+            this.failureType = Ext.form.Action.CLIENT_INVALID;
+            this.form.afterAction(this, false);
+        }
+    },
+
+    /**
+     * Method: handleResponse
+     * Handle the response to the search query.
+     *
+     * Parameters:
+     * response - {OpenLayers.Protocol.Response} The OpenLayers response
+     *     object.
+     */
+    handleResponse: function(response) {
+        this.response = response;
+        if(response.success()) {
+            this.form.afterAction(this, true);
+        } else {
+            this.form.afterAction(this, false);
+        }
+        var o = this.options;
+        if(o.callback) {
+            o.callback.call(o.scope, response);
+        }
+    }
+});
+
+/**
+ * Class: GeoExt.form.BasicForm
+ * A specific {Ext.form.BasicForm} whose doAction method creates
+ * a {<GeoExt.form.SearchOption>} if it is passed the string
+ * "search" as its first argument.
+ *
+ * In most cases one would not use this class directly, one
+ * would use {<GeoExt.form.FormPanel>} instead.
+ */
+GeoExt.form.BasicForm = Ext.extend(Ext.form.BasicForm, {
+    /**
+     * Property: protocol
+     * {OpenLayers.Protocol} The protocol configured in this
+     *     instance.
+     */
+    protocol: null,
+
+    /**
+     * APIMethod: doAction
+     * Performs the action, if the string "search" is passed as the
+     * first argument then a {<GeoExt.form.SearchAction>} is created.
+     * 
+     * Parameters:
+     * action - {String|Ext.form.Action} Either the name of the action
+     *     or a {Ext.form.Action} instance.
+     * options - {Object} The options passed to the Action constructor.
+     *
+     * Returns:
+     * {<GeoExt.form.BasicForm>} This form.
+     */
+    doAction: function(action, options) {
+        if(action == "search") {
+            options = Ext.applyIf(options || {}, {protocol: this.protocol});
+            action = new GeoExt.form.SearchAction(this, options);
+        }
+        return GeoExt.form.BasicForm.superclass.doAction.call(
+            this, action, options
+        );
+    },
+
+    /**
+     * APIMethod: search
+     * Shortcut to do a search action.
+     *
+     * Parameters:
+     * options - {Object} The options passed to the Action constructor.
+     */
+    search: function(options) {
+        return this.doAction("search", options);
+    }
+});
+
+/**
+ * Class: GeoExt.form.FormPanel
+ *
+ * A specific {Ext.form.FormPanel} whose internal form is a
+ * {<GeoExt.form.BasicForm>} instead of {Ext.form.BasicForm}.
+ * One would use this form to do search requests through
+ * an {OpenLayers.Protocol} object ({OpenLayers.Protocol.WFS}
+ * for example).
+ *
+ * Look at {<GeoExt.form.SearchAction>} to understand how
+ * form fields must be named for appropriate filters to be
+ * passed to the protocol.
+ *
+ * Example:
+ *
+ * (start code)
+ * var formPanel = new GeoExt.form.Panel({
+ *     renderTo: "formpanel",
+ *     protocol: new OpenLayers.Protocol.WFS({
+ *         url: "http://publicus.opengeo.org/geoserver/wfs",
+ *         featureType: "tasmania_roads",
+ *         featureNS: "http://www.openplans.org/topp"
+ *     })
+ *     items: [{
+ *         xtype: "textfield",
+ *         name: "name__ilike",
+ *         value: "mont"
+ *     }, {
+ *         xtype: "textfield",
+ *         name: "elevation__ge",
+ *         value: "2000"
+ *     }],
+ *     listeners: {
+ *         actioncomplete: function(form, action) {
+ *             // this listener triggers when the search request
+ *             // is complete, the OpenLayers.Protocol.Response
+ *             // resulting from the request is available
+ *             // in "action.response"
+ *         }
+ *     }
+ * });
+ *
+ * formPanel.addButton({
+ *     text: "search",
+ *     handler: function() {
+ *         this.search();
+ *     },
+ *     scope: formPanel
+ * });
+ * (end)
+ */
+GeoExt.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
+    /**
+     * APIProperty: protocol
+     * {OpenLayers.Protocol} The protocol instance this form panel
+     *     is configured with; actions resulting from this form
+     *     will be performed through the protocol.
+     */
+    protocol: null,
+
+    /**
+     * Method: createForm
+     * Create the internal {<GeoExt.form.BasicForm>} instance.
+     */
+    createForm: function() {
+        delete this.initialConfig.listeners;
+        return new GeoExt.form.BasicForm(null, this.initialConfig);
+    },
+
+    /**
+     * APIMethod: search
+     * Shortcut to the internal form's search method.
+     *
+     * Parameters:
+     * options - {Object} The options passed to the GeoExt.form.SearchAction
+     *     constructor, see {<GeoExt.form.SearchAction>}.
+     */
+    search: function(options) {
+        this.getForm().search(options);
+    }
+});
+
+Ext.reg("gx_formpanel", GeoExt.form.FormPanel);
Index: examples/search-form.js
===================================================================
--- examples/search-form.js	(revision 0)
+++ examples/search-form.js	(revision 0)
@@ -0,0 +1,65 @@
+
+var formPanel;
+
+Ext.onReady(function() {
+
+    // create a protocol, this protocol is used by the form
+    // to send the search request, this protocol's read
+    // method received an OpenLayers.Filter instance,
+    // which is derived from the content of the form
+    var protocol = new OpenLayers.Protocol({
+        read: function(options) {
+            var f,
+            f = options.filter;
+            OpenLayers.Console.log(f.CLASS_NAME, ",", f.type);
+            f = options.filter.filters[0];
+            OpenLayers.Console.log(f.CLASS_NAME, ",", f.type, ",", f.property, ":", f.value);
+            f = options.filter.filters[1];
+            OpenLayers.Console.log(f.CLASS_NAME, ",", f.type, ",", f.property, ":", f.value);
+        }
+    });
+
+    // create a GeoExt form panel (configured with an OpenLayers.Protocol
+    // instance)
+    formPanel = new GeoExt.form.FormPanel({
+        width: 300,
+        height: 200,
+        protocol: protocol,
+        items: [{
+            xtype: "textfield",
+            name: "name__like",
+            value: "foo",
+            fieldLabel: "name"
+        }, {
+            xtype: "textfield",
+            name: "elevation__ge",
+            value: "1200",
+            fieldLabel: "maximum elevation"
+        }],
+        listeners: {
+            actioncomplete: function(form, action) {
+                // this listener triggers when the search request
+                // is complete, the OpenLayers.Protocol.Response
+                // resulting from the request is available
+                // through "action.response"
+            }
+        }
+    });
+
+    formPanel.addButton({
+        text: "search",
+        handler: function() {
+            // trigger search request, the options passed to doAction
+            // are passed to the protocol's read method, so one
+            // can register a read callback here
+            var o = {
+                callback: function(response) {
+                }
+            };
+            this.search(o);
+        },
+        scope: formPanel
+    });
+
+    formPanel.render("formpanel");
+});
Index: examples/search-form.html
===================================================================
--- examples/search-form.html	(revision 0)
+++ examples/search-form.html	(revision 0)
@@ -0,0 +1,30 @@
+<html>
+    <head>
+        <link rel="stylesheet" type="text/css" href="../../ext/resources/css/ext-all.css"></link>
+        <link rel="stylesheet" type="text/css" href="../../ext/examples/shared/examples.css"></link>
+        <script type="text/javascript" src="../../openlayers/lib/Firebug/firebug.js"></script>
+        <script type="text/javascript" src="../../openlayers/lib/OpenLayers.js"></script>
+        <script type="text/javascript" src="../../ext/adapter/ext/ext-base.js"></script>
+        <script type="text/javascript" src="../../ext/ext-all.js"></script>
+        <script type="text/javascript" src="../lib/GeoExt.js"></script>
+        <script type="text/javascript" src="search-form.js"></script>
+    </head>
+    <body>
+        <h1>Example of a search form using GeoExt.form.FormPanel</h1>
+
+        <p>This example shows how to create a search form based on
+        GeoExt.form.FormPanel. GeoExt.form.FormPanel itself uses an
+        OpenLayers.Protocol object for sending search requests. In this
+        example a fake protocol is used, in a real-life scenario a specific
+        protocol like OpenLayers.Protocol.WFS would be used.</p>
+
+        <p>If you use FireBug open the console to see the OpenLayers.Filter
+        objects created when the search button is hit.</p>
+
+        <p>The js is not minified so it is readable. See <a
+        href="search-form.js">search-form.js</a></p>
+
+        <div id="formpanel"></div>
+
+    </body>
+</html>
