Roo/form/BasicForm.js
[roojs1] / Roo / form / BasicForm.js
1 /*
2  * Based on:
3  * Ext JS Library 1.1.1
4  * Copyright(c) 2006-2007, Ext JS, LLC.
5  *
6  * Originally Released Under LGPL - original licence link has changed is not relivant.
7  *
8  * Fork - LGPL
9  * <script type="text/javascript">
10  */
11  
12 /**
13  * @class Roo.form.BasicForm
14  * @extends Roo.util.Observable
15  * Supplies the functionality to do "actions" on forms and initialize Roo.form.Field types on existing markup.
16  * @constructor
17  * @param {String/HTMLElement/Roo.Element} el The form element or its id
18  * @param {Object} config Configuration options
19  */
20 Roo.form.BasicForm = function(el, config){
21     this.allItems = [];
22     this.childForms = [];
23     Roo.apply(this, config);
24     /*
25      * The Roo.form.Field items in this form.
26      * @type MixedCollection
27      */
28      
29      
30     this.items = new Roo.util.MixedCollection(false, function(o){
31         return o.id || (o.id = Roo.id());
32     });
33     this.addEvents({
34         /**
35          * @event beforeaction
36          * Fires before any action is performed. Return false to cancel the action.
37          * @param {Form} this
38          * @param {Action} action The action to be performed
39          */
40         beforeaction: true,
41         /**
42          * @event actionfailed
43          * Fires when an action fails.
44          * @param {Form} this
45          * @param {Action} action The action that failed
46          */
47         actionfailed : true,
48         /**
49          * @event actioncomplete
50          * Fires when an action is completed.
51          * @param {Form} this
52          * @param {Action} action The action that completed
53          */
54         actioncomplete : true
55     });
56     if(el){
57         this.initEl(el);
58     }
59     Roo.form.BasicForm.superclass.constructor.call(this);
60 };
61
62 Roo.extend(Roo.form.BasicForm, Roo.util.Observable, {
63     /**
64      * @cfg {String} method
65      * The request method to use (GET or POST) for form actions if one isn't supplied in the action options.
66      */
67     /**
68      * @cfg {DataReader} reader
69      * An Roo.data.DataReader (e.g. {@link Roo.data.XmlReader}) to be used to read data when executing "load" actions.
70      * This is optional as there is built-in support for processing JSON.
71      */
72     /**
73      * @cfg {DataReader} errorReader
74      * An Roo.data.DataReader (e.g. {@link Roo.data.XmlReader}) to be used to read data when reading validation errors on "submit" actions.
75      * This is completely optional as there is built-in support for processing JSON.
76      */
77     /**
78      * @cfg {String} url
79      * The URL to use for form actions if one isn't supplied in the action options.
80      */
81     /**
82      * @cfg {Boolean} fileUpload
83      * Set to true if this form is a file upload.
84      */
85      
86     /**
87      * @cfg {Object} baseParams
88      * Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.
89      */
90      /**
91      
92     /**
93      * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds).
94      */
95     timeout: 30,
96
97     // private
98     activeAction : null,
99
100     /**
101      * @cfg {Boolean} trackResetOnLoad If set to true, form.reset() resets to the last loaded
102      * or setValues() data instead of when the form was first created.
103      */
104     trackResetOnLoad : false,
105     
106     
107     /**
108      * childForms - used for multi-tab forms
109      * @type {Array}
110      */
111     childForms : false,
112     
113     /**
114      * allItems - full list of fields.
115      * @type {Array}
116      */
117     allItems : false,
118     
119     /**
120      * By default wait messages are displayed with Roo.MessageBox.wait. You can target a specific
121      * element by passing it or its id or mask the form itself by passing in true.
122      * @type Mixed
123      */
124     waitMsgTarget : false,
125
126     // private
127     initEl : function(el){
128         this.el = Roo.get(el);
129         this.id = this.el.id || Roo.id();
130         this.el.on('submit', this.onSubmit, this);
131         this.el.addClass('x-form');
132     },
133
134     // private
135     onSubmit : function(e){
136         e.stopEvent();
137     },
138
139     /**
140      * Returns true if client-side validation on the form is successful.
141      * @return Boolean
142      */
143     isValid : function(){
144         var valid = true;
145         this.items.each(function(f){
146            if(!f.validate()){
147                valid = false;
148            }
149         });
150         return valid;
151     },
152
153     /**
154      * Returns true if any fields in this form have changed since their original load.
155      * @return Boolean
156      */
157     isDirty : function(){
158         var dirty = false;
159         this.items.each(function(f){
160            if(f.isDirty()){
161                dirty = true;
162                return false;
163            }
164         });
165         return dirty;
166     },
167
168     /**
169      * Performs a predefined action (submit or load) or custom actions you define on this form.
170      * @param {String} actionName The name of the action type
171      * @param {Object} options (optional) The options to pass to the action.  All of the config options listed
172      * below are supported by both the submit and load actions unless otherwise noted (custom actions could also
173      * accept other config options):
174      * <pre>
175 Property          Type             Description
176 ----------------  ---------------  ----------------------------------------------------------------------------------
177 url               String           The url for the action (defaults to the form's url)
178 method            String           The form method to use (defaults to the form's method, or POST if not defined)
179 params            String/Object    The params to pass (defaults to the form's baseParams, or none if not defined)
180 clientValidation  Boolean          Applies to submit only.  Pass true to call form.isValid() prior to posting to
181                                    validate the form on the client (defaults to false)
182      * </pre>
183      * @return {BasicForm} this
184      */
185     doAction : function(action, options){
186         if(typeof action == 'string'){
187             action = new Roo.form.Action.ACTION_TYPES[action](this, options);
188         }
189         if(this.fireEvent('beforeaction', this, action) !== false){
190             this.beforeAction(action);
191             action.run.defer(100, action);
192         }
193         return this;
194     },
195
196     /**
197      * Shortcut to do a submit action.
198      * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
199      * @return {BasicForm} this
200      */
201     submit : function(options){
202         this.doAction('submit', options);
203         return this;
204     },
205
206     /**
207      * Shortcut to do a load action.
208      * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
209      * @return {BasicForm} this
210      */
211     load : function(options){
212         this.doAction('load', options);
213         return this;
214     },
215
216     /**
217      * Persists the values in this form into the passed Roo.data.Record object in a beginEdit/endEdit block.
218      * @param {Record} record The record to edit
219      * @return {BasicForm} this
220      */
221     updateRecord : function(record){
222         record.beginEdit();
223         var fs = record.fields;
224         fs.each(function(f){
225             var field = this.findField(f.name);
226             if(field){
227                 record.set(f.name, field.getValue());
228             }
229         }, this);
230         record.endEdit();
231         return this;
232     },
233
234     /**
235      * Loads an Roo.data.Record into this form.
236      * @param {Record} record The record to load
237      * @return {BasicForm} this
238      */
239     loadRecord : function(record){
240         this.setValues(record.data);
241         return this;
242     },
243
244     // private
245     beforeAction : function(action){
246         var o = action.options;
247         
248        
249         if(this.waitMsgTarget === true){
250             this.el.mask(o.waitMsg || "Sending", 'x-mask-loading');
251         }else if(this.waitMsgTarget){
252             this.waitMsgTarget = Roo.get(this.waitMsgTarget);
253             this.waitMsgTarget.mask(o.waitMsg || "Sending", 'x-mask-loading');
254         }else {
255             Roo.MessageBox.wait(o.waitMsg || "Sending", o.waitTitle || this.waitTitle || 'Please Wait...');
256         }
257          
258     },
259
260     // private
261     afterAction : function(action, success){
262         this.activeAction = null;
263         var o = action.options;
264         
265         if(this.waitMsgTarget === true){
266             this.el.unmask();
267         }else if(this.waitMsgTarget){
268             this.waitMsgTarget.unmask();
269         }else{
270             Roo.MessageBox.updateProgress(1);
271             Roo.MessageBox.hide();
272         }
273          
274         if(success){
275             if(o.reset){
276                 this.reset();
277             }
278             Roo.callback(o.success, o.scope, [this, action]);
279             this.fireEvent('actioncomplete', this, action);
280             
281         }else{
282             
283             // failure condition..
284             // we have a scenario where updates need confirming.
285             // eg. if a locking scenario exists..
286             // we look for { errors : { needs_confirm : true }} in the response.
287             if (
288                 (typeof(action.result) != 'undefined')  &&
289                 (typeof(action.result.errors) != 'undefined')  &&
290                 (typeof(action.result.errors.needs_confirm) != 'undefined')
291            ){
292                 var _t = this;
293                 Roo.MessageBox.confirm(
294                     "Change requires confirmation",
295                     action.result.errorMsg,
296                     function(r) {
297                         if (r != 'yes') {
298                             return;
299                         }
300                         _t.doAction('submit', { params :  { _submit_confirmed : 1 } }  );
301                     }
302                     
303                 );
304                 
305                 
306                 
307                 return;
308             }
309             
310             Roo.callback(o.failure, o.scope, [this, action]);
311             // show an error message if no failed handler is set..
312             if (!this.hasListener('actionfailed')) {
313                 Roo.MessageBox.alert("Error",
314                     (typeof(action.result) != 'undefined' && typeof(action.result.errorMsg) != 'undefined') ?
315                         action.result.errorMsg :
316                         "Saving Failed, please check your entries or try again"
317                 );
318             }
319             
320             this.fireEvent('actionfailed', this, action);
321         }
322         
323     },
324
325     /**
326      * Find a Roo.form.Field in this form by id, dataIndex, name or hiddenName
327      * @param {String} id The value to search for
328      * @return Field
329      */
330     findField : function(id){
331         var field = this.items.get(id);
332         if(!field){
333             this.items.each(function(f){
334                 if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){
335                     field = f;
336                     return false;
337                 }
338             });
339         }
340         return field || null;
341     },
342
343     /**
344      * Add a secondary form to this one, 
345      * Used to provide tabbed forms. One form is primary, with hidden values 
346      * which mirror the elements from the other forms.
347      * 
348      * @param {Roo.form.Form} form to add.
349      * 
350      */
351     addForm : function(form)
352     {
353        
354         if (this.childForms.indexOf(form) > -1) {
355             // already added..
356             return;
357         }
358         this.childForms.push(form);
359         var n = '';
360         Roo.each(form.allItems, function (fe) {
361             
362             n = typeof(fe.getName) == 'undefined' ? fe.name : fe.getName();
363             if (this.findField(n)) { // already added..
364                 return;
365             }
366             var add = new Roo.form.Hidden({
367                 name : n
368             });
369             add.render(this.el);
370             
371             this.add( add );
372         }, this);
373         
374     },
375     /**
376      * Mark fields in this form invalid in bulk.
377      * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2}
378      * @return {BasicForm} this
379      */
380     markInvalid : function(errors){
381         if(errors instanceof Array){
382             for(var i = 0, len = errors.length; i < len; i++){
383                 var fieldError = errors[i];
384                 var f = this.findField(fieldError.id);
385                 if(f){
386                     f.markInvalid(fieldError.msg);
387                 }
388             }
389         }else{
390             var field, id;
391             for(id in errors){
392                 if(typeof errors[id] != 'function' && (field = this.findField(id))){
393                     field.markInvalid(errors[id]);
394                 }
395             }
396         }
397         Roo.each(this.childForms || [], function (f) {
398             f.markInvalid(errors);
399         });
400         
401         return this;
402     },
403
404     /**
405      * Set values for fields in this form in bulk.
406      * @param {Array/Object} values Either an array in the form [{id:'fieldId', value:'foo'},...] or an object hash of {id: value, id2: value2}
407      * @return {BasicForm} this
408      */
409     setValues : function(values){
410         if(values instanceof Array){ // array of objects
411             for(var i = 0, len = values.length; i < len; i++){
412                 var v = values[i];
413                 var f = this.findField(v.id);
414                 if(f){
415                     f.setValue(v.value);
416                     if(this.trackResetOnLoad){
417                         if(typeof(field.getValue()) == 'undefined'){
418                             Roo.log(field);
419                         }
420                         f.originalValue = f.getValue();
421                     }
422                 }
423             }
424         }else{ // object hash
425             var field, id;
426             for(id in values){
427                 if(typeof values[id] != 'function' && (field = this.findField(id))){
428                     
429                     if (field.setFromData && 
430                         field.valueField && 
431                         field.displayField &&
432                         // combos' with local stores can 
433                         // be queried via setValue()
434                         // to set their value..
435                         (field.store && !field.store.isLocal)
436                         ) {
437                         // it's a combo
438                         var sd = { };
439                         sd[field.valueField] = typeof(values[field.hiddenName]) == 'undefined' ? '' : values[field.hiddenName];
440                         sd[field.displayField] = typeof(values[field.name]) == 'undefined' ? '' : values[field.name];
441                         field.setFromData(sd);
442                         
443                     } else {
444                         field.setValue(values[id]);
445                     }
446                     
447                     
448                     if(this.trackResetOnLoad){
449                         if(typeof(field.getValue()) == 'undefined'){
450                             Roo.log(field);
451                         }
452                         field.originalValue = field.getValue();
453                     }
454                 }
455             }
456         }
457          
458         Roo.each(this.childForms || [], function (f) {
459             f.setValues(values);
460         });
461                 
462         return this;
463     },
464
465     /**
466      * Returns the fields in this form as an object with key/value pairs. If multiple fields exist with the same name
467      * they are returned as an array.
468      * @param {Boolean} asString
469      * @return {Object}
470      */
471     getValues : function(asString){
472         if (this.childForms) {
473             // copy values from the child forms
474             Roo.each(this.childForms, function (f) {
475                 this.setValues(f.getValues());
476             }, this);
477         }
478         
479         
480         
481         var fs = Roo.lib.Ajax.serializeForm(this.el.dom);
482         if(asString === true){
483             return fs;
484         }
485         return Roo.urlDecode(fs);
486     },
487     
488     /**
489      * Returns the fields in this form as an object with key/value pairs. 
490      * This differs from getValues as it calls getValue on each child item, rather than using dom data.
491      * @return {Object}
492      */
493     getFieldValues : function(with_hidden)
494     {
495         if (this.childForms) {
496             // copy values from the child forms
497             // should this call getFieldValues - probably not as we do not currently copy
498             // hidden fields when we generate..
499             Roo.each(this.childForms, function (f) {
500                 this.setValues(f.getValues());
501             }, this);
502         }
503         
504         var ret = {};
505         this.items.each(function(f){
506             if (!f.getName()) {
507                 return;
508             }
509             var v = f.getValue();
510             if (f.inputType =='radio') {
511                 if (typeof(ret[f.getName()]) == 'undefined') {
512                     ret[f.getName()] = ''; // empty..
513                 }
514                 
515                 if (!f.el.dom.checked) {
516                     return;
517                     
518                 }
519                 v = f.el.dom.value;
520                 
521             }
522             
523             // not sure if this supported any more..
524             if ((typeof(v) == 'object') && f.getRawValue) {
525                 v = f.getRawValue() ; // dates..
526             }
527             // combo boxes where name != hiddenName...
528             if (f.name != f.getName()) {
529                 ret[f.name] = f.getRawValue();
530             }
531             ret[f.getName()] = v;
532         });
533         
534         return ret;
535     },
536
537     /**
538      * Clears all invalid messages in this form.
539      * @return {BasicForm} this
540      */
541     clearInvalid : function(){
542         this.items.each(function(f){
543            f.clearInvalid();
544         });
545         
546         Roo.each(this.childForms || [], function (f) {
547             f.clearInvalid();
548         });
549         
550         
551         return this;
552     },
553
554     /**
555      * Resets this form.
556      * @return {BasicForm} this
557      */
558     reset : function(){
559         this.items.each(function(f){
560             f.reset();
561         });
562         
563         Roo.each(this.childForms || [], function (f) {
564             f.reset();
565         });
566        
567         
568         return this;
569     },
570
571     /**
572      * Add Roo.form components to this form.
573      * @param {Field} field1
574      * @param {Field} field2 (optional)
575      * @param {Field} etc (optional)
576      * @return {BasicForm} this
577      */
578     add : function(){
579         this.items.addAll(Array.prototype.slice.call(arguments, 0));
580         return this;
581     },
582
583
584     /**
585      * Removes a field from the items collection (does NOT remove its markup).
586      * @param {Field} field
587      * @return {BasicForm} this
588      */
589     remove : function(field){
590         this.items.remove(field);
591         return this;
592     },
593
594     /**
595      * Looks at the fields in this form, checks them for an id attribute,
596      * and calls applyTo on the existing dom element with that id.
597      * @return {BasicForm} this
598      */
599     render : function(){
600         this.items.each(function(f){
601             if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists
602                 f.applyTo(f.id);
603             }
604         });
605         return this;
606     },
607
608     /**
609      * Calls {@link Ext#apply} for all fields in this form with the passed object.
610      * @param {Object} values
611      * @return {BasicForm} this
612      */
613     applyToFields : function(o){
614         this.items.each(function(f){
615            Roo.apply(f, o);
616         });
617         return this;
618     },
619
620     /**
621      * Calls {@link Ext#applyIf} for all field in this form with the passed object.
622      * @param {Object} values
623      * @return {BasicForm} this
624      */
625     applyIfToFields : function(o){
626         this.items.each(function(f){
627            Roo.applyIf(f, o);
628         });
629         return this;
630     }
631 });
632
633 // back compat
634 Roo.BasicForm = Roo.form.BasicForm;