Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / form / Basic.js
1 /**
2  * @class Ext.form.Basic
3  * @extends Ext.util.Observable
4
5 Provides input field management, validation, submission, and form loading services for the collection
6 of {@link Ext.form.field.Field Field} instances within a {@link Ext.container.Container}. It is recommended
7 that you use a {@link Ext.form.Panel} as the form container, as that has logic to automatically
8 hook up an instance of {@link Ext.form.Basic} (plus other conveniences related to field configuration.)
9
10 #Form Actions#
11
12 The Basic class delegates the handling of form loads and submits to instances of {@link Ext.form.action.Action}.
13 See the various Action implementations for specific details of each one's functionality, as well as the
14 documentation for {@link #doAction} which details the configuration options that can be specified in
15 each action call.
16
17 The default submit Action is {@link Ext.form.action.Submit}, which uses an Ajax request to submit the
18 form's values to a configured URL. To enable normal browser submission of an Ext form, use the
19 {@link #standardSubmit} config option.
20
21 Note: File uploads are not performed using normal 'Ajax' techniques; see the description for
22 {@link #hasUpload} for details.
23
24 #Example usage:#
25
26     Ext.create('Ext.form.Panel', {
27         title: 'Basic Form',
28         renderTo: Ext.getBody(),
29         bodyPadding: 5,
30         width: 350,
31
32         // Any configuration items here will be automatically passed along to
33         // the Ext.form.Basic instance when it gets created.
34
35         // The form will submit an AJAX request to this URL when submitted
36         url: 'save-form.php',
37
38         items: [{
39             fieldLabel: 'Field',
40             name: 'theField'
41         }],
42
43         buttons: [{
44             text: 'Submit',
45             handler: function() {
46                 // The getForm() method returns the Ext.form.Basic instance:
47                 var form = this.up('form').getForm();
48                 if (form.isValid()) {
49                     // Submit the Ajax request and handle the response
50                     form.submit({
51                         success: function(form, action) {
52                            Ext.Msg.alert('Success', action.result.msg);
53                         },
54                         failure: function(form, action) {
55                             Ext.Msg.alert('Failed', action.result.msg);
56                         }
57                     });
58                 }
59             }
60         }]
61     });
62
63  * @constructor
64  * @param {Ext.container.Container} owner The component that is the container for the form, usually a {@link Ext.form.Panel}
65  * @param {Object} config Configuration options. These are normally specified in the config to the
66  * {@link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically.
67  *
68  * @markdown
69  * @docauthor Jason Johnston <jason@sencha.com>
70  */
71
72
73
74 Ext.define('Ext.form.Basic', {
75     extend: 'Ext.util.Observable',
76     alternateClassName: 'Ext.form.BasicForm',
77     requires: ['Ext.util.MixedCollection', 'Ext.form.action.Load', 'Ext.form.action.Submit',
78                'Ext.window.MessageBox', 'Ext.data.Errors', 'Ext.util.DelayedTask'],
79
80     constructor: function(owner, config) {
81         var me = this,
82             onItemAddOrRemove = me.onItemAddOrRemove;
83
84         /**
85          * @property owner
86          * @type Ext.container.Container
87          * The container component to which this BasicForm is attached.
88          */
89         me.owner = owner;
90
91         // Listen for addition/removal of fields in the owner container
92         me.mon(owner, {
93             add: onItemAddOrRemove,
94             remove: onItemAddOrRemove,
95             scope: me
96         });
97
98         Ext.apply(me, config);
99
100         // Normalize the paramOrder to an Array
101         if (Ext.isString(me.paramOrder)) {
102             me.paramOrder = me.paramOrder.split(/[\s,|]/);
103         }
104
105         me.checkValidityTask = Ext.create('Ext.util.DelayedTask', me.checkValidity, me);
106
107         me.addEvents(
108             /**
109              * @event beforeaction
110              * Fires before any action is performed. Return false to cancel the action.
111              * @param {Ext.form.Basic} this
112              * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} to be performed
113              */
114             'beforeaction',
115             /**
116              * @event actionfailed
117              * Fires when an action fails.
118              * @param {Ext.form.Basic} this
119              * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that failed
120              */
121             'actionfailed',
122             /**
123              * @event actioncomplete
124              * Fires when an action is completed.
125              * @param {Ext.form.Basic} this
126              * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that completed
127              */
128             'actioncomplete',
129             /**
130              * @event validitychange
131              * Fires when the validity of the entire form changes.
132              * @param {Ext.form.Basic} this
133              * @param {Boolean} valid <tt>true</tt> if the form is now valid, <tt>false</tt> if it is now invalid.
134              */
135             'validitychange',
136             /**
137              * @event dirtychange
138              * Fires when the dirty state of the entire form changes.
139              * @param {Ext.form.Basic} this
140              * @param {Boolean} dirty <tt>true</tt> if the form is now dirty, <tt>false</tt> if it is no longer dirty.
141              */
142             'dirtychange'
143         );
144         me.callParent();
145     },
146
147     /**
148      * Do any post constructor initialization
149      * @private
150      */
151     initialize: function(){
152         this.initialized = true;
153         this.onValidityChange(!this.hasInvalidField());
154     },
155
156     /**
157      * @cfg {String} method
158      * The request method to use (GET or POST) for form actions if one isn't supplied in the action options.
159      */
160     /**
161      * @cfg {Ext.data.reader.Reader} reader
162      * An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to read
163      * data when executing 'load' actions. This is optional as there is built-in
164      * support for processing JSON responses.
165      */
166     /**
167      * @cfg {Ext.data.reader.Reader} errorReader
168      * <p>An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to
169      * read field error messages returned from 'submit' actions. This is optional
170      * as there is built-in support for processing JSON responses.</p>
171      * <p>The Records which provide messages for the invalid Fields must use the
172      * Field name (or id) as the Record ID, and must contain a field called 'msg'
173      * which contains the error message.</p>
174      * <p>The errorReader does not have to be a full-blown implementation of a
175      * Reader. It simply needs to implement a <tt>read(xhr)</tt> function
176      * which returns an Array of Records in an object with the following
177      * structure:</p><pre><code>
178 {
179     records: recordArray
180 }
181 </code></pre>
182      */
183
184     /**
185      * @cfg {String} url
186      * The URL to use for form actions if one isn't supplied in the
187      * {@link #doAction doAction} options.
188      */
189
190     /**
191      * @cfg {Object} baseParams
192      * <p>Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.</p>
193      * <p>Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.</p>
194      */
195
196     /**
197      * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds).
198      */
199     timeout: 30,
200
201     /**
202      * @cfg {Object} api (Optional) If specified, load and submit actions will be handled
203      * with {@link Ext.form.action.DirectLoad} and {@link Ext.form.action.DirectLoad}.
204      * Methods which have been imported by {@link Ext.direct.Manager} can be specified here to load and submit
205      * forms.
206      * Such as the following:<pre><code>
207 api: {
208     load: App.ss.MyProfile.load,
209     submit: App.ss.MyProfile.submit
210 }
211 </code></pre>
212      * <p>Load actions can use <code>{@link #paramOrder}</code> or <code>{@link #paramsAsHash}</code>
213      * to customize how the load method is invoked.
214      * Submit actions will always use a standard form submit. The <tt>formHandler</tt> configuration must
215      * be set on the associated server-side method which has been imported by {@link Ext.direct.Manager}.</p>
216      */
217
218     /**
219      * @cfg {Array/String} paramOrder <p>A list of params to be executed server side.
220      * Defaults to <tt>undefined</tt>. Only used for the <code>{@link #api}</code>
221      * <code>load</code> configuration.</p>
222      * <p>Specify the params in the order in which they must be executed on the
223      * server-side as either (1) an Array of String values, or (2) a String of params
224      * delimited by either whitespace, comma, or pipe. For example,
225      * any of the following would be acceptable:</p><pre><code>
226 paramOrder: ['param1','param2','param3']
227 paramOrder: 'param1 param2 param3'
228 paramOrder: 'param1,param2,param3'
229 paramOrder: 'param1|param2|param'
230      </code></pre>
231      */
232
233     /**
234      * @cfg {Boolean} paramsAsHash Only used for the <code>{@link #api}</code>
235      * <code>load</code> configuration. If <tt>true</tt>, parameters will be sent as a
236      * single hash collection of named arguments (defaults to <tt>false</tt>). Providing a
237      * <tt>{@link #paramOrder}</tt> nullifies this configuration.
238      */
239     paramsAsHash: false,
240
241     /**
242      * @cfg {String} waitTitle
243      * The default title to show for the waiting message box (defaults to <tt>'Please Wait...'</tt>)
244      */
245     waitTitle: 'Please Wait...',
246
247     /**
248      * @cfg {Boolean} trackResetOnLoad If set to <tt>true</tt>, {@link #reset}() resets to the last loaded
249      * or {@link #setValues}() data instead of when the form was first created.  Defaults to <tt>false</tt>.
250      */
251     trackResetOnLoad: false,
252
253     /**
254      * @cfg {Boolean} standardSubmit
255      * <p>If set to <tt>true</tt>, a standard HTML form submit is used instead
256      * of a XHR (Ajax) style form submission. Defaults to <tt>false</tt>. All of
257      * the field values, plus any additional params configured via {@link #baseParams}
258      * and/or the <code>options</code> to {@link #submit}, will be included in the
259      * values submitted in the form.</p>
260      */
261
262     /**
263      * @cfg {Mixed} waitMsgTarget
264      * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific
265      * element by passing it or its id or mask the form itself by passing in true. Defaults to <tt>undefined</tt>.
266      */
267
268
269     // Private
270     wasDirty: false,
271
272
273     /**
274      * Destroys this object.
275      */
276     destroy: function() {
277         this.clearListeners();
278         this.checkValidityTask.cancel();
279     },
280
281     /**
282      * @private
283      * Handle addition or removal of descendant items. Invalidates the cached list of fields
284      * so that {@link #getFields} will do a fresh query next time it is called. Also adds listeners
285      * for state change events on added fields, and tracks components with formBind=true.
286      */
287     onItemAddOrRemove: function(parent, child) {
288         var me = this,
289             isAdding = !!child.ownerCt,
290             isContainer = child.isContainer;
291
292         function handleField(field) {
293             // Listen for state change events on fields
294             me[isAdding ? 'mon' : 'mun'](field, {
295                 validitychange: me.checkValidity,
296                 dirtychange: me.checkDirty,
297                 scope: me,
298                 buffer: 100 //batch up sequential calls to avoid excessive full-form validation
299             });
300             // Flush the cached list of fields
301             delete me._fields;
302         }
303
304         if (child.isFormField) {
305             handleField(child);
306         }
307         else if (isContainer) {
308             // Walk down
309             Ext.Array.forEach(child.query('[isFormField]'), handleField);
310         }
311
312         // Flush the cached list of formBind components
313         delete this._boundItems;
314
315         // Check form bind, but only after initial add. Batch it to prevent excessive validation
316         // calls when many fields are being added at once.
317         if (me.initialized) {
318             me.checkValidityTask.delay(10);
319         }
320     },
321
322     /**
323      * Return all the {@link Ext.form.field.Field} components in the owner container.
324      * @return {Ext.util.MixedCollection} Collection of the Field objects
325      */
326     getFields: function() {
327         var fields = this._fields;
328         if (!fields) {
329             fields = this._fields = Ext.create('Ext.util.MixedCollection');
330             fields.addAll(this.owner.query('[isFormField]'));
331         }
332         return fields;
333     },
334
335     getBoundItems: function() {
336         var boundItems = this._boundItems;
337         if (!boundItems) {
338             boundItems = this._boundItems = Ext.create('Ext.util.MixedCollection');
339             boundItems.addAll(this.owner.query('[formBind]'));
340         }
341         return boundItems;
342     },
343
344     /**
345      * Returns true if the form contains any invalid fields. No fields will be marked as invalid
346      * as a result of calling this; to trigger marking of fields use {@link #isValid} instead.
347      */
348     hasInvalidField: function() {
349         return !!this.getFields().findBy(function(field) {
350             var preventMark = field.preventMark,
351                 isValid;
352             field.preventMark = true;
353             isValid = field.isValid();
354             field.preventMark = preventMark;
355             return !isValid;
356         });
357     },
358
359     /**
360      * Returns true if client-side validation on the form is successful. Any invalid fields will be
361      * marked as invalid. If you only want to determine overall form validity without marking anything,
362      * use {@link #hasInvalidField} instead.
363      * @return Boolean
364      */
365     isValid: function() {
366         var me = this,
367             invalid;
368         me.batchLayouts(function() {
369             invalid = me.getFields().filterBy(function(field) {
370                 return !field.validate();
371             });
372         });
373         return invalid.length < 1;
374     },
375
376     /**
377      * Check whether the validity of the entire form has changed since it was last checked, and
378      * if so fire the {@link #validitychange validitychange} event. This is automatically invoked
379      * when an individual field's validity changes.
380      */
381     checkValidity: function() {
382         var me = this,
383             valid = !me.hasInvalidField();
384         if (valid !== me.wasValid) {
385             me.onValidityChange(valid);
386             me.fireEvent('validitychange', me, valid);
387             me.wasValid = valid;
388         }
389     },
390
391     /**
392      * @private
393      * Handle changes in the form's validity. If there are any sub components with
394      * formBind=true then they are enabled/disabled based on the new validity.
395      * @param {Boolean} valid
396      */
397     onValidityChange: function(valid) {
398         var boundItems = this.getBoundItems();
399         if (boundItems) {
400             boundItems.each(function(cmp) {
401                 if (cmp.disabled === valid) {
402                     cmp.setDisabled(!valid);
403                 }
404             });
405         }
406     },
407
408     /**
409      * <p>Returns true if any fields in this form have changed from their original values.</p>
410      * <p>Note that if this BasicForm was configured with {@link #trackResetOnLoad} then the
411      * Fields' <em>original values</em> are updated when the values are loaded by {@link #setValues}
412      * or {@link #loadRecord}.</p>
413      * @return Boolean
414      */
415     isDirty: function() {
416         return !!this.getFields().findBy(function(f) {
417             return f.isDirty();
418         });
419     },
420
421     /**
422      * Check whether the dirty state of the entire form has changed since it was last checked, and
423      * if so fire the {@link #dirtychange dirtychange} event. This is automatically invoked
424      * when an individual field's dirty state changes.
425      */
426     checkDirty: function() {
427         var dirty = this.isDirty();
428         if (dirty !== this.wasDirty) {
429             this.fireEvent('dirtychange', this, dirty);
430             this.wasDirty = dirty;
431         }
432     },
433
434     /**
435      * <p>Returns true if the form contains a file upload field. This is used to determine the
436      * method for submitting the form: File uploads are not performed using normal 'Ajax' techniques,
437      * that is they are <b>not</b> performed using XMLHttpRequests. Instead a hidden <tt>&lt;form></tt>
438      * element containing all the fields is created temporarily and submitted with its
439      * <a href="http://www.w3.org/TR/REC-html40/present/frames.html#adef-target">target</a> set to refer
440      * to a dynamically generated, hidden <tt>&lt;iframe></tt> which is inserted into the document
441      * but removed after the return data has been gathered.</p>
442      * <p>The server response is parsed by the browser to create the document for the IFRAME. If the
443      * server is using JSON to send the return object, then the
444      * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17">Content-Type</a> header
445      * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.</p>
446      * <p>Characters which are significant to an HTML parser must be sent as HTML entities, so encode
447      * "&lt;" as "&amp;lt;", "&amp;" as "&amp;amp;" etc.</p>
448      * <p>The response text is retrieved from the document, and a fake XMLHttpRequest object
449      * is created containing a <tt>responseText</tt> property in order to conform to the
450      * requirements of event handlers and callbacks.</p>
451      * <p>Be aware that file upload packets are sent with the content type <a href="http://www.faqs.org/rfcs/rfc2388.html">multipart/form</a>
452      * and some server technologies (notably JEE) may require some custom processing in order to
453      * retrieve parameter names and parameter values from the packet content.</p>
454      * @return Boolean
455      */
456     hasUpload: function() {
457         return !!this.getFields().findBy(function(f) {
458             return f.isFileUpload();
459         });
460     },
461
462     /**
463      * Performs a predefined action (an implementation of {@link Ext.form.action.Action})
464      * to perform application-specific processing.
465      * @param {String/Ext.form.action.Action} action The name of the predefined action type,
466      * or instance of {@link Ext.form.action.Action} to perform.
467      * @param {Object} options (optional) The options to pass to the {@link Ext.form.action.Action}
468      * that will get created, if the <tt>action</tt> argument is a String.
469      * <p>All of the config options listed below are supported by both the
470      * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load}
471      * actions unless otherwise noted (custom actions could also accept
472      * other config options):</p><ul>
473      *
474      * <li><b>url</b> : String<div class="sub-desc">The url for the action (defaults
475      * to the form's {@link #url}.)</div></li>
476      *
477      * <li><b>method</b> : String<div class="sub-desc">The form method to use (defaults
478      * to the form's method, or POST if not defined)</div></li>
479      *
480      * <li><b>params</b> : String/Object<div class="sub-desc"><p>The params to pass
481      * (defaults to the form's baseParams, or none if not defined)</p>
482      * <p>Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.</p></div></li>
483      *
484      * <li><b>headers</b> : Object<div class="sub-desc">Request headers to set for the action.</div></li>
485      *
486      * <li><b>success</b> : Function<div class="sub-desc">The callback that will
487      * be invoked after a successful response (see top of
488      * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load}
489      * for a description of what constitutes a successful response).
490      * The function is passed the following parameters:<ul>
491      * <li><tt>form</tt> : The {@link Ext.form.Basic} that requested the action.</li>
492      * <li><tt>action</tt> : The {@link Ext.form.action.Action Action} object which performed the operation.
493      * <div class="sub-desc">The action object contains these properties of interest:<ul>
494      * <li><tt>{@link Ext.form.action.Action#response response}</tt></li>
495      * <li><tt>{@link Ext.form.action.Action#result result}</tt> : interrogate for custom postprocessing</li>
496      * <li><tt>{@link Ext.form.action.Action#type type}</tt></li>
497      * </ul></div></li></ul></div></li>
498      *
499      * <li><b>failure</b> : Function<div class="sub-desc">The callback that will be invoked after a
500      * failed transaction attempt. The function is passed the following parameters:<ul>
501      * <li><tt>form</tt> : The {@link Ext.form.Basic} that requested the action.</li>
502      * <li><tt>action</tt> : The {@link Ext.form.action.Action Action} object which performed the operation.
503      * <div class="sub-desc">The action object contains these properties of interest:<ul>
504      * <li><tt>{@link Ext.form.action.Action#failureType failureType}</tt></li>
505      * <li><tt>{@link Ext.form.action.Action#response response}</tt></li>
506      * <li><tt>{@link Ext.form.action.Action#result result}</tt> : interrogate for custom postprocessing</li>
507      * <li><tt>{@link Ext.form.action.Action#type type}</tt></li>
508      * </ul></div></li></ul></div></li>
509      *
510      * <li><b>scope</b> : Object<div class="sub-desc">The scope in which to call the
511      * callback functions (The <tt>this</tt> reference for the callback functions).</div></li>
512      *
513      * <li><b>clientValidation</b> : Boolean<div class="sub-desc">Submit Action only.
514      * Determines whether a Form's fields are validated in a final call to
515      * {@link Ext.form.Basic#isValid isValid} prior to submission. Set to <tt>false</tt>
516      * to prevent this. If undefined, pre-submission field validation is performed.</div></li></ul>
517      *
518      * @return {Ext.form.Basic} this
519      */
520     doAction: function(action, options) {
521         if (Ext.isString(action)) {
522             action = Ext.ClassManager.instantiateByAlias('formaction.' + action, Ext.apply({}, options, {form: this}));
523         }
524         if (this.fireEvent('beforeaction', this, action) !== false) {
525             this.beforeAction(action);
526             Ext.defer(action.run, 100, action);
527         }
528         return this;
529     },
530
531     /**
532      * Shortcut to {@link #doAction do} a {@link Ext.form.action.Submit submit action}. This will use the
533      * {@link Ext.form.action.Submit AJAX submit action} by default. If the {@link #standardsubmit} config is
534      * enabled it will use a standard form element to submit, or if the {@link #api} config is present it will
535      * use the {@link Ext.form.action.DirectLoad Ext.direct.Direct submit action}.
536      * @param {Object} options The options to pass to the action (see {@link #doAction} for details).<br>
537      * <p>The following code:</p><pre><code>
538 myFormPanel.getForm().submit({
539     clientValidation: true,
540     url: 'updateConsignment.php',
541     params: {
542         newStatus: 'delivered'
543     },
544     success: function(form, action) {
545        Ext.Msg.alert('Success', action.result.msg);
546     },
547     failure: function(form, action) {
548         switch (action.failureType) {
549             case Ext.form.action.Action.CLIENT_INVALID:
550                 Ext.Msg.alert('Failure', 'Form fields may not be submitted with invalid values');
551                 break;
552             case Ext.form.action.Action.CONNECT_FAILURE:
553                 Ext.Msg.alert('Failure', 'Ajax communication failed');
554                 break;
555             case Ext.form.action.Action.SERVER_INVALID:
556                Ext.Msg.alert('Failure', action.result.msg);
557        }
558     }
559 });
560 </code></pre>
561      * would process the following server response for a successful submission:<pre><code>
562 {
563     "success":true, // note this is Boolean, not string
564     "msg":"Consignment updated"
565 }
566 </code></pre>
567      * and the following server response for a failed submission:<pre><code>
568 {
569     "success":false, // note this is Boolean, not string
570     "msg":"You do not have permission to perform this operation"
571 }
572 </code></pre>
573      * @return {Ext.form.Basic} this
574      */
575     submit: function(options) {
576         return this.doAction(this.standardSubmit ? 'standardsubmit' : this.api ? 'directsubmit' : 'submit', options);
577     },
578
579     /**
580      * Shortcut to {@link #doAction do} a {@link Ext.form.action.Load load action}.
581      * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
582      * @return {Ext.form.Basic} this
583      */
584     load: function(options) {
585         return this.doAction(this.api ? 'directload' : 'load', options);
586     },
587
588     /**
589      * Persists the values in this form into the passed {@link Ext.data.Model} object in a beginEdit/endEdit block.
590      * @param {Ext.data.Record} record The record to edit
591      * @return {Ext.form.Basic} this
592      */
593     updateRecord: function(record) {
594         var fields = record.fields,
595             values = this.getFieldValues(),
596             name,
597             obj = {};
598
599         fields.each(function(f) {
600             name = f.name;
601             if (name in values) {
602                 obj[name] = values[name];
603             }
604         });
605
606         record.beginEdit();
607         record.set(obj);
608         record.endEdit();
609
610         return this;
611     },
612
613     /**
614      * Loads an {@link Ext.data.Model} into this form by calling {@link #setValues} with the
615      * {@link Ext.data.Model#data record data}.
616      * See also {@link #trackResetOnLoad}.
617      * @param {Ext.data.Model} record The record to load
618      * @return {Ext.form.Basic} this
619      */
620     loadRecord: function(record) {
621         this._record = record;
622         return this.setValues(record.data);
623     },
624     
625     /**
626      * Returns the last Ext.data.Model instance that was loaded via {@link #loadRecord}
627      * @return {Ext.data.Model} The record
628      */
629     getRecord: function() {
630         return this._record;
631     },
632
633     /**
634      * @private
635      * Called before an action is performed via {@link #doAction}.
636      * @param {Ext.form.action.Action} action The Action instance that was invoked
637      */
638     beforeAction: function(action) {
639         var waitMsg = action.waitMsg,
640             maskCls = Ext.baseCSSPrefix + 'mask-loading',
641             waitMsgTarget;
642
643         // Call HtmlEditor's syncValue before actions
644         this.getFields().each(function(f) {
645             if (f.isFormField && f.syncValue) {
646                 f.syncValue();
647             }
648         });
649
650         if (waitMsg) {
651             waitMsgTarget = this.waitMsgTarget;
652             if (waitMsgTarget === true) {
653                 this.owner.el.mask(waitMsg, maskCls);
654             } else if (waitMsgTarget) {
655                 waitMsgTarget = this.waitMsgTarget = Ext.get(waitMsgTarget);
656                 waitMsgTarget.mask(waitMsg, maskCls);
657             } else {
658                 Ext.MessageBox.wait(waitMsg, action.waitTitle || this.waitTitle);
659             }
660         }
661     },
662
663     /**
664      * @private
665      * Called after an action is performed via {@link #doAction}.
666      * @param {Ext.form.action.Action} action The Action instance that was invoked
667      * @param {Boolean} success True if the action completed successfully, false, otherwise.
668      */
669     afterAction: function(action, success) {
670         if (action.waitMsg) {
671             var MessageBox = Ext.MessageBox,
672                 waitMsgTarget = this.waitMsgTarget;
673             if (waitMsgTarget === true) {
674                 this.owner.el.unmask();
675             } else if (waitMsgTarget) {
676                 waitMsgTarget.unmask();
677             } else {
678                 MessageBox.updateProgress(1);
679                 MessageBox.hide();
680             }
681         }
682         if (success) {
683             if (action.reset) {
684                 this.reset();
685             }
686             Ext.callback(action.success, action.scope || action, [this, action]);
687             this.fireEvent('actioncomplete', this, action);
688         } else {
689             Ext.callback(action.failure, action.scope || action, [this, action]);
690             this.fireEvent('actionfailed', this, action);
691         }
692     },
693
694
695     /**
696      * Find a specific {@link Ext.form.field.Field} in this form by id or name.
697      * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or
698      * {@link Ext.form.field.Field#getName name or hiddenName}).
699      * @return Ext.form.field.Field The first matching field, or <tt>null</tt> if none was found.
700      */
701     findField: function(id) {
702         return this.getFields().findBy(function(f) {
703             return f.id === id || f.getName() === id;
704         });
705     },
706
707
708     /**
709      * Mark fields in this form invalid in bulk.
710      * @param {Array/Object} errors Either an array in the form <code>[{id:'fieldId', msg:'The message'}, ...]</code>,
711      * an object hash of <code>{id: msg, id2: msg2}</code>, or a {@link Ext.data.Errors} object.
712      * @return {Ext.form.Basic} this
713      */
714     markInvalid: function(errors) {
715         var me = this;
716
717         function mark(fieldId, msg) {
718             var field = me.findField(fieldId);
719             if (field) {
720                 field.markInvalid(msg);
721             }
722         }
723
724         if (Ext.isArray(errors)) {
725             Ext.each(errors, function(err) {
726                 mark(err.id, err.msg);
727             });
728         }
729         else if (errors instanceof Ext.data.Errors) {
730             errors.each(function(err) {
731                 mark(err.field, err.message);
732             });
733         }
734         else {
735             Ext.iterate(errors, mark);
736         }
737         return this;
738     },
739
740     /**
741      * Set values for fields in this form in bulk.
742      * @param {Array/Object} values Either an array in the form:<pre><code>
743 [{id:'clientName', value:'Fred. Olsen Lines'},
744  {id:'portOfLoading', value:'FXT'},
745  {id:'portOfDischarge', value:'OSL'} ]</code></pre>
746      * or an object hash of the form:<pre><code>
747 {
748     clientName: 'Fred. Olsen Lines',
749     portOfLoading: 'FXT',
750     portOfDischarge: 'OSL'
751 }</code></pre>
752      * @return {Ext.form.Basic} this
753      */
754     setValues: function(values) {
755         var me = this;
756
757         function setVal(fieldId, val) {
758             var field = me.findField(fieldId);
759             if (field) {
760                 field.setValue(val);
761                 if (me.trackResetOnLoad) {
762                     field.resetOriginalValue();
763                 }
764             }
765         }
766
767         if (Ext.isArray(values)) {
768             // array of objects
769             Ext.each(values, function(val) {
770                 setVal(val.id, val.value);
771             });
772         } else {
773             // object hash
774             Ext.iterate(values, setVal);
775         }
776         return this;
777     },
778
779     /**
780      * Retrieves the fields in the form as a set of key/value pairs, using their
781      * {@link Ext.form.field.Field#getSubmitData getSubmitData()} method to collect the values.
782      * If multiple fields return values under the same name those values will be combined into an Array.
783      * This is similar to {@link #getFieldValues} except that this method collects only String values for
784      * submission, while getFieldValues collects type-specific data values (e.g. Date objects for date fields.)
785      * @param {Boolean} asString (optional) If true, will return the key/value collection as a single
786      * URL-encoded param string. Defaults to false.
787      * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result.
788      * Defaults to false.
789      * @param {Boolean} includeEmptyText (optional) If true, the configured emptyText of empty fields will be used.
790      * Defaults to false.
791      * @return {String/Object}
792      */
793     getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
794         var values = {};
795
796         this.getFields().each(function(field) {
797             if (!dirtyOnly || field.isDirty()) {
798                 var data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
799                 if (Ext.isObject(data)) {
800                     Ext.iterate(data, function(name, val) {
801                         if (includeEmptyText && val === '') {
802                             val = field.emptyText || '';
803                         }
804                         if (name in values) {
805                             var bucket = values[name],
806                                 isArray = Ext.isArray;
807                             if (!isArray(bucket)) {
808                                 bucket = values[name] = [bucket];
809                             }
810                             if (isArray(val)) {
811                                 values[name] = bucket.concat(val);
812                             } else {
813                                 bucket.push(val);
814                             }
815                         } else {
816                             values[name] = val;
817                         }
818                     });
819                 }
820             }
821         });
822
823         if (asString) {
824             values = Ext.Object.toQueryString(values);
825         }
826         return values;
827     },
828
829     /**
830      * Retrieves the fields in the form as a set of key/value pairs, using their
831      * {@link Ext.form.field.Field#getModelData getModelData()} method to collect the values.
832      * If multiple fields return values under the same name those values will be combined into an Array.
833      * This is similar to {@link #getValues} except that this method collects type-specific data values
834      * (e.g. Date objects for date fields) while getValues returns only String values for submission.
835      * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result.
836      * Defaults to false.
837      * @return {Object}
838      */
839     getFieldValues: function(dirtyOnly) {
840         return this.getValues(false, dirtyOnly, false, true);
841     },
842
843     /**
844      * Clears all invalid field messages in this form.
845      * @return {Ext.form.Basic} this
846      */
847     clearInvalid: function() {
848         var me = this;
849         me.batchLayouts(function() {
850             me.getFields().each(function(f) {
851                 f.clearInvalid();
852             });
853         });
854         return me;
855     },
856
857     /**
858      * Resets all fields in this form.
859      * @return {Ext.form.Basic} this
860      */
861     reset: function() {
862         var me = this;
863         me.batchLayouts(function() {
864             me.getFields().each(function(f) {
865                 f.reset();
866             });
867         });
868         return me;
869     },
870
871     /**
872      * Calls {@link Ext#apply Ext.apply} for all fields in this form with the passed object.
873      * @param {Object} obj The object to be applied
874      * @return {Ext.form.Basic} this
875      */
876     applyToFields: function(obj) {
877         this.getFields().each(function(f) {
878             Ext.apply(f, obj);
879         });
880         return this;
881     },
882
883     /**
884      * Calls {@link Ext#applyIf Ext.applyIf} for all field in this form with the passed object.
885      * @param {Object} obj The object to be applied
886      * @return {Ext.form.Basic} this
887      */
888     applyIfToFields: function(obj) {
889         this.getFields().each(function(f) {
890             Ext.applyIf(f, obj);
891         });
892         return this;
893     },
894
895     /**
896      * @private
897      * Utility wrapper that suspends layouts of all field parent containers for the duration of a given
898      * function. Used during full-form validation and resets to prevent huge numbers of layouts.
899      * @param {Function} fn
900      */
901     batchLayouts: function(fn) {
902         var me = this,
903             suspended = new Ext.util.HashMap();
904
905         // Temporarily suspend layout on each field's immediate owner so we don't get a huge layout cascade
906         me.getFields().each(function(field) {
907             var ownerCt = field.ownerCt;
908             if (!suspended.contains(ownerCt)) {
909                 suspended.add(ownerCt);
910                 ownerCt.oldSuspendLayout = ownerCt.suspendLayout;
911                 ownerCt.suspendLayout = true;
912             }
913         });
914
915         // Invoke the function
916         fn();
917
918         // Un-suspend the container layouts
919         suspended.each(function(id, ct) {
920             ct.suspendLayout = ct.oldSuspendLayout;
921             delete ct.oldSuspendLayout;
922         });
923
924         // Trigger a single layout
925         me.owner.doComponentLayout();
926     }
927 });