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