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