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