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