Upgrade to ExtJS 3.2.2 - Released 06/02/2010
[extjs.git] / src / widgets / form / CompositeField.js
1 /*!
2  * Ext JS Library 3.2.2
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.form.CompositeField
9  * @extends Ext.form.Field
10  * Composite field allowing a number of form Fields to be rendered on the same row. The fields are rendered
11  * using an hbox layout internally, so all of the normal HBox layout config items are available. Example usage:
12  * <pre>
13 {
14     xtype: 'compositefield',
15     labelWidth: 120
16     items: [
17         {
18             xtype     : 'textfield',
19             fieldLabel: 'Title',
20             width     : 20
21         },
22         {
23             xtype     : 'textfield',
24             fieldLabel: 'First',
25             flex      : 1
26         },
27         {
28             xtype     : 'textfield',
29             fieldLabel: 'Last',
30             flex      : 1
31         }
32     ]
33 }
34  * </pre>
35  * In the example above the composite's fieldLabel will be set to 'Title, First, Last' as it groups the fieldLabels
36  * of each of its children. This can be overridden by setting a fieldLabel on the compositefield itself:
37  * <pre>
38 {
39     xtype: 'compositefield',
40     fieldLabel: 'Custom label',
41     items: [...]
42 }
43  * </pre>
44  * Any Ext.form.* component can be placed inside a composite field.
45  */
46 Ext.form.CompositeField = Ext.extend(Ext.form.Field, {
47
48     /**
49      * @property defaultMargins
50      * @type String
51      * The margins to apply by default to each field in the composite
52      */
53     defaultMargins: '0 5 0 0',
54
55     /**
56      * @property skipLastItemMargin
57      * @type Boolean
58      * If true, the defaultMargins are not applied to the last item in the composite field set (defaults to true)
59      */
60     skipLastItemMargin: true,
61
62     /**
63      * @property isComposite
64      * @type Boolean
65      * Signifies that this is a Composite field
66      */
67     isComposite: true,
68
69     /**
70      * @property combineErrors
71      * @type Boolean
72      * True to combine errors from the individual fields into a single error message at the CompositeField level (defaults to true)
73      */
74     combineErrors: true,
75     
76     /**
77      * @cfg {String} labelConnector The string to use when joining segments of the built label together (defaults to ', ')
78      */
79     labelConnector: ', ',
80
81     //inherit docs
82     //Builds the composite field label
83     initComponent: function() {
84         var labels = [],
85             items  = this.items,
86             item;
87
88         for (var i=0, j = items.length; i < j; i++) {
89             item = items[i];
90
91             labels.push(item.fieldLabel);
92
93             //apply any defaults
94             Ext.apply(item, this.defaults);
95
96             //apply default margins to each item except the last
97             if (!(i == j - 1 && this.skipLastItemMargin)) {
98                 Ext.applyIf(item, {margins: this.defaultMargins});
99             }
100         }
101
102         this.fieldLabel = this.fieldLabel || this.buildLabel(labels);
103
104         /**
105          * @property fieldErrors
106          * @type Ext.util.MixedCollection
107          * MixedCollection of current errors on the Composite's subfields. This is used internally to track when
108          * to show and hide error messages at the Composite level. Listeners are attached to the MixedCollection's
109          * add, remove and replace events to update the error icon in the UI as errors are added or removed.
110          */
111         this.fieldErrors = new Ext.util.MixedCollection(true, function(item) {
112             return item.field;
113         });
114
115         this.fieldErrors.on({
116             scope  : this,
117             add    : this.updateInvalidMark,
118             remove : this.updateInvalidMark,
119             replace: this.updateInvalidMark
120         });
121
122         Ext.form.CompositeField.superclass.initComponent.apply(this, arguments);
123     },
124
125     /**
126      * @private
127      * Creates an internal container using hbox and renders the fields to it
128      */
129     onRender: function(ct, position) {
130         if (!this.el) {
131             /**
132              * @property innerCt
133              * @type Ext.Container
134              * A container configured with hbox layout which is responsible for laying out the subfields
135              */
136             var innerCt = this.innerCt = new Ext.Container({
137                 layout  : 'hbox',
138                 renderTo: ct,
139                 items   : this.items,
140                 cls     : 'x-form-composite',
141                 defaultMargins: '0 3 0 0'
142             });
143
144             this.el = innerCt.getEl();
145
146             var fields = innerCt.findBy(function(c) {
147                 return c.isFormField;
148             }, this);
149
150             /**
151              * @property items
152              * @type Ext.util.MixedCollection
153              * Internal collection of all of the subfields in this Composite
154              */
155             this.items = new Ext.util.MixedCollection();
156             this.items.addAll(fields);
157
158             //if we're combining subfield errors into a single message, override the markInvalid and clearInvalid
159             //methods of each subfield and show them at the Composite level instead
160             if (this.combineErrors) {
161                 this.eachItem(function(field) {
162                     Ext.apply(field, {
163                         markInvalid : this.onFieldMarkInvalid.createDelegate(this, [field], 0),
164                         clearInvalid: this.onFieldClearInvalid.createDelegate(this, [field], 0)
165                     });
166                 });
167             }
168
169             //set the label 'for' to the first item
170             var l = this.el.parent().parent().child('label', true);
171             if (l) {
172                 l.setAttribute('for', this.items.items[0].id);
173             }
174         }
175
176         Ext.form.CompositeField.superclass.onRender.apply(this, arguments);
177     },
178
179     /**
180      * Called if combineErrors is true and a subfield's markInvalid method is called.
181      * By default this just adds the subfield's error to the internal fieldErrors MixedCollection
182      * @param {Ext.form.Field} field The field that was marked invalid
183      * @param {String} message The error message
184      */
185     onFieldMarkInvalid: function(field, message) {
186         var name  = field.getName(),
187             error = {field: name, error: message};
188
189         this.fieldErrors.replace(name, error);
190
191         field.el.addClass(field.invalidClass);
192     },
193
194     /**
195      * Called if combineErrors is true and a subfield's clearInvalid method is called.
196      * By default this just updates the internal fieldErrors MixedCollection.
197      * @param {Ext.form.Field} field The field that was marked invalid
198      */
199     onFieldClearInvalid: function(field) {
200         this.fieldErrors.removeKey(field.getName());
201
202         field.el.removeClass(field.invalidClass);
203     },
204
205     /**
206      * @private
207      * Called after a subfield is marked valid or invalid, this checks to see if any of the subfields are
208      * currently invalid. If any subfields are invalid it builds a combined error message marks the composite
209      * invalid, otherwise clearInvalid is called
210      */
211     updateInvalidMark: function() {
212         var ieStrict = Ext.isIE6 && Ext.isStrict;
213
214         if (this.fieldErrors.length == 0) {
215             this.clearInvalid();
216
217             //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
218             if (ieStrict) {
219                 this.clearInvalid.defer(50, this);
220             }
221         } else {
222             var message = this.buildCombinedErrorMessage(this.fieldErrors.items);
223
224             this.sortErrors();
225             this.markInvalid(message);
226
227             //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
228             if (ieStrict) {
229                 this.markInvalid(message);
230             }
231         }
232     },
233
234     /**
235      * Performs validation checks on each subfield and returns false if any of them fail validation.
236      * @return {Boolean} False if any subfield failed validation
237      */
238     validateValue: function() {
239         var valid = true;
240
241         this.eachItem(function(field) {
242             if (!field.isValid()) valid = false;
243         });
244
245         return valid;
246     },
247
248     /**
249      * Takes an object containing error messages for contained fields, returning a combined error
250      * string (defaults to just placing each item on a new line). This can be overridden to provide
251      * custom combined error message handling.
252      * @param {Array} errors Array of errors in format: [{field: 'title', error: 'some error'}]
253      * @return {String} The combined error message
254      */
255     buildCombinedErrorMessage: function(errors) {
256         var combined = [],
257             error;
258
259         for (var i = 0, j = errors.length; i < j; i++) {
260             error = errors[i];
261
262             combined.push(String.format("{0}: {1}", error.field, error.error));
263         }
264
265         return combined.join("<br />");
266     },
267
268     /**
269      * Sorts the internal fieldErrors MixedCollection by the order in which the fields are defined.
270      * This is called before displaying errors to ensure that the errors are presented in the expected order.
271      * This function can be overridden to provide a custom sorting order if needed.
272      */
273     sortErrors: function() {
274         var fields = this.items;
275
276         this.fieldErrors.sort("ASC", function(a, b) {
277             var findByName = function(key) {
278                 return function(field) {
279                     return field.getName() == key;
280                 };
281             };
282
283             var aIndex = fields.findIndexBy(findByName(a.field)),
284                 bIndex = fields.findIndexBy(findByName(b.field));
285
286             return aIndex < bIndex ? -1 : 1;
287         });
288     },
289
290     /**
291      * Resets each field in the composite to their previous value
292      */
293     reset: function() {
294         this.eachItem(function(item) {
295             item.reset();
296         });
297
298         // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete.
299         // Important because reset is being called on both the group and the individual items.
300         (function() {
301             this.clearInvalid();
302         }).defer(50, this);
303     },
304     
305     /**
306      * Calls clearInvalid on all child fields. This is a convenience function and should not often need to be called
307      * as fields usually take care of clearing themselves
308      */
309     clearInvalidChildren: function() {
310         this.eachItem(function(item) {
311             item.clearInvalid();
312         });
313     },
314
315     /**
316      * Builds a label string from an array of subfield labels.
317      * By default this just joins the labels together with a comma
318      * @param {Array} segments Array of each of the labels in the composite field's subfields
319      * @return {String} The built label
320      */
321     buildLabel: function(segments) {
322         return Ext.clean(segments).join(this.labelConnector);
323     },
324
325     /**
326      * Checks each field in the composite and returns true if any is dirty
327      * @return {Boolean} True if any field is dirty
328      */
329     isDirty: function(){
330         //override the behaviour to check sub items.
331         if (this.disabled || !this.rendered) {
332             return false;
333         }
334
335         var dirty = false;
336         this.eachItem(function(item){
337             if(item.isDirty()){
338                 dirty = true;
339                 return false;
340             }
341         });
342         return dirty;
343     },
344
345     /**
346      * @private
347      * Convenience function which passes the given function to every item in the composite
348      * @param {Function} fn The function to call
349      * @param {Object} scope Optional scope object
350      */
351     eachItem: function(fn, scope) {
352         if(this.items && this.items.each){
353             this.items.each(fn, scope || this);
354         }
355     },
356
357     /**
358      * @private
359      * Passes the resize call through to the inner panel
360      */
361     onResize: function(adjWidth, adjHeight, rawWidth, rawHeight) {
362         var innerCt = this.innerCt;
363
364         if (this.rendered && innerCt.rendered) {
365             innerCt.setSize(adjWidth, adjHeight);
366         }
367
368         Ext.form.CompositeField.superclass.onResize.apply(this, arguments);
369     },
370
371     /**
372      * @private
373      * Forces the internal container to be laid out again
374      */
375     doLayout: function(shallow, force) {
376         if (this.rendered) {
377             var innerCt = this.innerCt;
378
379             innerCt.forceLayout = this.ownerCt.forceLayout;
380             innerCt.doLayout(shallow, force);
381         }
382     },
383
384     /**
385      * @private
386      */
387     beforeDestroy: function(){
388         Ext.destroy(this.innerCt);
389
390         Ext.form.CompositeField.superclass.beforeDestroy.call(this);
391     },
392
393     //override the behaviour to check sub items.
394     setReadOnly : function(readOnly) {
395         readOnly = readOnly || true;
396
397         if(this.rendered){
398             this.eachItem(function(item){
399                 item.setReadOnly(readOnly);
400             });
401         }
402         this.readOnly = readOnly;
403     },
404
405     onShow : function() {
406         Ext.form.CompositeField.superclass.onShow.call(this);
407         this.doLayout();
408     },
409
410     //override the behaviour to check sub items.
411     onDisable : function(){
412         this.eachItem(function(item){
413             item.disable();
414         });
415     },
416
417     //override the behaviour to check sub items.
418     onEnable : function(){
419         this.eachItem(function(item){
420             item.enable();
421         });
422     }
423 });
424
425 Ext.reg('compositefield', Ext.form.CompositeField);