Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / FieldContainer.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  * FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the
17  * {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is rendered with
18  * a {@link #fieldLabel field label} and optional {@link #msgTarget error message} around its sub-items.
19  * This is useful for arranging a group of fields or other components within a single item in a form, so
20  * that it lines up nicely with other fields. A common use is for grouping a set of related fields under
21  * a single label in a form.
22  * 
23  * The container's configured {@link #items} will be layed out within the field body area according to the
24  * configured {@link #layout} type. The default layout is `'autocontainer'`.
25  * 
26  * Like regular fields, FieldContainer can inherit its decoration configuration from the
27  * {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition,
28  * FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields}
29  * it may itself contain.
30  * 
31  * If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or {@link Ext.form.field.Radio Radio}
32  * fields in a single labeled container, consider using a {@link Ext.form.CheckboxGroup}
33  * or {@link Ext.form.RadioGroup} instead as they are specialized for handling those types.
34  *
35  * # Example
36  * 
37  *     @example
38  *     Ext.create('Ext.form.Panel', {
39  *         title: 'FieldContainer Example',
40  *         width: 550,
41  *         bodyPadding: 10,
42  * 
43  *         items: [{
44  *             xtype: 'fieldcontainer',
45  *             fieldLabel: 'Last Three Jobs',
46  *             labelWidth: 100,
47  * 
48  *             // The body area will contain three text fields, arranged
49  *             // horizontally, separated by draggable splitters.
50  *             layout: 'hbox',
51  *             items: [{
52  *                 xtype: 'textfield',
53  *                 flex: 1
54  *             }, {
55  *                 xtype: 'splitter'
56  *             }, {
57  *                 xtype: 'textfield',
58  *                 flex: 1
59  *             }, {
60  *                 xtype: 'splitter'
61  *             }, {
62  *                 xtype: 'textfield',
63  *                 flex: 1
64  *             }]
65  *         }],
66  *         renderTo: Ext.getBody()
67  *     });
68  * 
69  * # Usage of fieldDefaults
70  *
71  *     @example
72  *     Ext.create('Ext.form.Panel', {
73  *         title: 'FieldContainer Example',
74  *         width: 350,
75  *         bodyPadding: 10,
76  * 
77  *         items: [{
78  *             xtype: 'fieldcontainer',
79  *             fieldLabel: 'Your Name',
80  *             labelWidth: 75,
81  *             defaultType: 'textfield',
82  * 
83  *             // Arrange fields vertically, stretched to full width
84  *             layout: 'anchor',
85  *             defaults: {
86  *                 layout: '100%'
87  *             },
88  * 
89  *             // These config values will be applied to both sub-fields, except
90  *             // for Last Name which will use its own msgTarget.
91  *             fieldDefaults: {
92  *                 msgTarget: 'under',
93  *                 labelAlign: 'top'
94  *             },
95  * 
96  *             items: [{
97  *                 fieldLabel: 'First Name',
98  *                 name: 'firstName'
99  *             }, {
100  *                 fieldLabel: 'Last Name',
101  *                 name: 'lastName',
102  *                 msgTarget: 'under'
103  *             }]
104  *         }],
105  *         renderTo: Ext.getBody()
106  *     });
107  * 
108  * @docauthor Jason Johnston <jason@sencha.com>
109  */
110 Ext.define('Ext.form.FieldContainer', {
111     extend: 'Ext.container.Container',
112     mixins: {
113         labelable: 'Ext.form.Labelable',
114         fieldAncestor: 'Ext.form.FieldAncestor'
115     },
116     alias: 'widget.fieldcontainer',
117
118     componentLayout: 'field',
119
120     /**
121      * @cfg {Boolean} combineLabels
122      * If set to true, and there is no defined {@link #fieldLabel}, the field container will automatically
123      * generate its label by combining the labels of all the fields it contains. Defaults to false.
124      */
125     combineLabels: false,
126
127     /**
128      * @cfg {String} labelConnector
129      * The string to use when joining the labels of individual sub-fields, when {@link #combineLabels} is
130      * set to true. Defaults to ', '.
131      */
132     labelConnector: ', ',
133
134     /**
135      * @cfg {Boolean} combineErrors
136      * If set to true, the field container will automatically combine and display the validation errors from
137      * all the fields it contains as a single error on the container, according to the configured
138      * {@link #msgTarget}. Defaults to false.
139      */
140     combineErrors: false,
141
142     maskOnDisable: false,
143
144     initComponent: function() {
145         var me = this,
146             onSubCmpAddOrRemove = me.onSubCmpAddOrRemove;
147
148         // Init mixins
149         me.initLabelable();
150         me.initFieldAncestor();
151
152         me.callParent();
153     },
154
155     /**
156      * @protected Called when a {@link Ext.form.Labelable} instance is added to the container's subtree.
157      * @param {Ext.form.Labelable} labelable The instance that was added
158      */
159     onLabelableAdded: function(labelable) {
160         var me = this;
161         me.mixins.fieldAncestor.onLabelableAdded.call(this, labelable);
162         me.updateLabel();
163     },
164
165     /**
166      * @protected Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree.
167      * @param {Ext.form.Labelable} labelable The instance that was removed
168      */
169     onLabelableRemoved: function(labelable) {
170         var me = this;
171         me.mixins.fieldAncestor.onLabelableRemoved.call(this, labelable);
172         me.updateLabel();
173     },
174
175     onRender: function() {
176         var me = this;
177
178         me.onLabelableRender();
179
180         me.callParent(arguments);
181     },
182
183     initRenderTpl: function() {
184         var me = this;
185         if (!me.hasOwnProperty('renderTpl')) {
186             me.renderTpl = me.getTpl('labelableRenderTpl');
187         }
188         return me.callParent();
189     },
190
191     initRenderData: function() {
192         return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
193     },
194
195     /**
196      * Returns the combined field label if {@link #combineLabels} is set to true and if there is no
197      * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override
198      * this method to provide a custom generated label.
199      */
200     getFieldLabel: function() {
201         var label = this.fieldLabel || '';
202         if (!label && this.combineLabels) {
203             label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) {
204                 return field.getFieldLabel();
205             }).join(this.labelConnector);
206         }
207         return label;
208     },
209
210     /**
211      * @private Updates the content of the labelEl if it is rendered
212      */
213     updateLabel: function() {
214         var me = this,
215             label = me.labelEl;
216         if (label) {
217             label.update(me.getFieldLabel());
218         }
219     },
220
221
222     /**
223      * @private Fired when the error message of any field within the container changes, and updates the
224      * combined error message to match.
225      */
226     onFieldErrorChange: function(field, activeError) {
227         if (this.combineErrors) {
228             var me = this,
229                 oldError = me.getActiveError(),
230                 invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) {
231                     return field.hasActiveError();
232                 }),
233                 newErrors = me.getCombinedErrors(invalidFields);
234
235             if (newErrors) {
236                 me.setActiveErrors(newErrors);
237             } else {
238                 me.unsetActiveError();
239             }
240
241             if (oldError !== me.getActiveError()) {
242                 me.doComponentLayout();
243             }
244         }
245     },
246
247     /**
248      * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list of error
249      * messages from them. Defaults to prepending each message by the field name and a colon. This
250      * can be overridden to provide custom combined error message handling, for instance changing
251      * the format of each message or sorting the array (it is sorted in order of appearance by default).
252      * @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently invalid.
253      * @return {String[]} The combined list of error messages
254      */
255     getCombinedErrors: function(invalidFields) {
256         var forEach = Ext.Array.forEach,
257             errors = [];
258         forEach(invalidFields, function(field) {
259             forEach(field.getActiveErrors(), function(error) {
260                 var label = field.getFieldLabel();
261                 errors.push((label ? label + ': ' : '') + error);
262             });
263         });
264         return errors;
265     },
266
267     getTargetEl: function() {
268         return this.bodyEl || this.callParent();
269     }
270 });
271