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