Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / form / CheckboxGroup.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  * A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging
17  * {@link Ext.form.field.Checkbox} controls into columns, and provides convenience {@link Ext.form.field.Field} methods
18  * for {@link #getValue getting}, {@link #setValue setting}, and {@link #validate validating} the group
19  * of checkboxes as a whole.
20  *
21  * # Validation
22  *
23  * Individual checkbox fields themselves have no default validation behavior, but
24  * sometimes you want to require a user to select at least one of a group of checkboxes. CheckboxGroup
25  * allows this by setting the config `{@link #allowBlank}:false`; when the user does not check at
26  * least one of the checkboxes, the entire group will be highlighted as invalid and the
27  * {@link #blankText error message} will be displayed according to the {@link #msgTarget} config.
28  *
29  * # Layout
30  *
31  * The default layout for CheckboxGroup makes it easy to arrange the checkboxes into
32  * columns; see the {@link #columns} and {@link #vertical} config documentation for details. You may also
33  * use a completely different layout by setting the {@link #layout} to one of the other supported layout
34  * types; for instance you may wish to use a custom arrangement of hbox and vbox containers. In that case
35  * the checkbox components at any depth will still be managed by the CheckboxGroup's validation.
36  *
37  * {@img Ext.form.CheckboxGroup/Ext.form.CheckboxGroup.png Ext.form.CheckboxGroup component}
38  *
39  * # Example usage
40  *
41  *     Ext.create('Ext.form.Panel', {
42  *         title: 'Checkbox Group',
43  *         width: 300,
44  *         height: 125,
45  *         bodyPadding: 10,
46  *         renderTo: Ext.getBody(),        
47  *         items:[{            
48  *             xtype: 'checkboxgroup',
49  *             fieldLabel: 'Two Columns',
50  *             // Arrange radio buttons into two columns, distributed vertically
51  *             columns: 2,
52  *             vertical: true,
53  *             items: [
54  *                 {boxLabel: 'Item 1', name: 'rb', inputValue: '1'},
55  *                 {boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true},
56  *                 {boxLabel: 'Item 3', name: 'rb', inputValue: '3'},
57  *                 {boxLabel: 'Item 4', name: 'rb', inputValue: '4'},
58  *                 {boxLabel: 'Item 5', name: 'rb', inputValue: '5'},
59  *                 {boxLabel: 'Item 6', name: 'rb', inputValue: '6'}
60  *             ]
61  *         }]
62  *     });
63  *
64  */
65 Ext.define('Ext.form.CheckboxGroup', {
66     extend:'Ext.form.FieldContainer',
67     mixins: {
68         field: 'Ext.form.field.Field'
69     },
70     alias: 'widget.checkboxgroup',
71     requires: ['Ext.layout.container.CheckboxGroup', 'Ext.form.field.Base'],
72
73     /**
74      * @cfg {String} name
75      * @hide
76      */
77
78     /**
79      * @cfg {Array} items An Array of {@link Ext.form.field.Checkbox Checkbox}es or Checkbox config objects
80      * to arrange in the group.
81      */
82
83     /**
84      * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped
85      * checkbox/radio controls using automatic layout.  This config can take several types of values:
86      * <ul><li><b>'auto'</b> : <p class="sub-desc">The controls will be rendered one per column on one row and the width
87      * of each column will be evenly distributed based on the width of the overall field container. This is the default.</p></li>
88      * <li><b>Number</b> : <p class="sub-desc">If you specific a number (e.g., 3) that number of columns will be
89      * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.</p></li>
90      * <li><b>Array</b> : <p class="sub-desc">You can also specify an array of column widths, mixing integer
91      * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will
92      * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float
93      * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field
94      * container you should do so.</p></li></ul>
95      */
96     columns : 'auto',
97
98     /**
99      * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column
100      * top to bottom before starting on the next column.  The number of controls in each column will be automatically
101      * calculated to keep columns as even as possible.  The default value is false, so that controls will be added
102      * to columns one at a time, completely filling each row left to right before starting on the next row.
103      */
104     vertical : false,
105
106     /**
107      * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true).
108      * If no items are selected at validation time, {@link #blankText} will be used as the error text.
109      */
110     allowBlank : true,
111
112     /**
113      * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must
114      * select at least one item in this group")
115      */
116     blankText : "You must select at least one item in this group",
117
118     // private
119     defaultType : 'checkboxfield',
120
121     // private
122     groupCls : Ext.baseCSSPrefix + 'form-check-group',
123
124     /**
125      * @cfg {String} fieldBodyCls
126      * An extra CSS class to be applied to the body content element in addition to {@link #baseBodyCls}.
127      * Defaults to 'x-form-checkboxgroup-body'.
128      */
129     fieldBodyCls: Ext.baseCSSPrefix + 'form-checkboxgroup-body',
130
131     // private
132     layout: 'checkboxgroup',
133
134     initComponent: function() {
135         var me = this;
136         me.callParent();
137         me.initField();
138     },
139
140     /**
141      * @protected
142      * Initializes the field's value based on the initial config. If the {@link #value} config is specified
143      * then we use that to set the value; otherwise we initialize the originalValue by querying the values of
144      * all sub-checkboxes after they have been initialized.
145      */
146     initValue: function() {
147         var me = this,
148             valueCfg = me.value;
149         me.originalValue = me.lastValue = valueCfg || me.getValue();
150         if (valueCfg) {
151             me.setValue(valueCfg);
152         }
153     },
154
155     /**
156      * @protected
157      * When a checkbox is added to the group, monitor it for changes
158      */
159     onFieldAdded: function(field) {
160         var me = this;
161         if (field.isCheckbox) {
162             me.mon(field, 'change', me.checkChange, me);
163         }
164         me.callParent(arguments);
165     },
166
167     onFieldRemoved: function(field) {
168         var me = this;
169         if (field.isCheckbox) {
170             me.mun(field, 'change', me.checkChange, me);
171         }
172         me.callParent(arguments);
173     },
174
175     // private override - the group value is a complex object, compare using object serialization
176     isEqual: function(value1, value2) {
177         var toQueryString = Ext.Object.toQueryString;
178         return toQueryString(value1) === toQueryString(value2);
179     },
180
181     /**
182      * Runs CheckboxGroup's validations and returns an array of any errors. The only error by default
183      * is if allowBlank is set to true and no items are checked.
184      * @return {Array} Array of all validation errors
185      */
186     getErrors: function() {
187         var errors = [];
188         if (!this.allowBlank && Ext.isEmpty(this.getChecked())) {
189             errors.push(this.blankText);
190         }
191         return errors;
192     },
193
194     /**
195      * @private Returns all checkbox components within the container
196      */
197     getBoxes: function() {
198         return this.query('[isCheckbox]');
199     },
200
201     /**
202      * @private Convenience function which calls the given function for every checkbox in the group
203      * @param {Function} fn The function to call
204      * @param {Object} scope Optional scope object
205      */
206     eachBox: function(fn, scope) {
207         Ext.Array.forEach(this.getBoxes(), fn, scope || this);
208     },
209
210     /**
211      * Returns an Array of all checkboxes in the container which are currently checked
212      * @return {Array} Array of Ext.form.field.Checkbox components
213      */
214     getChecked: function() {
215         return Ext.Array.filter(this.getBoxes(), function(cb) {
216             return cb.getValue();
217         });
218     },
219
220     // private override
221     isDirty: function(){
222         return Ext.Array.some(this.getBoxes(), function(cb) {
223             return cb.isDirty();
224         });
225     },
226
227     // private override
228     setReadOnly: function(readOnly) {
229         this.eachBox(function(cb) {
230             cb.setReadOnly(readOnly);
231         });
232         this.readOnly = readOnly;
233     },
234
235     /**
236      * Resets the checked state of all {@link Ext.form.field.Checkbox checkboxes} in the group to their
237      * originally loaded values and clears any validation messages.
238      * See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}
239      */
240     reset: function() {
241         var me = this,
242             hadError = me.hasActiveError(),
243             preventMark = me.preventMark;
244         me.preventMark = true;
245         me.batchChanges(function() {
246             me.eachBox(function(cb) {
247                 cb.reset();
248             });
249         });
250         me.preventMark = preventMark;
251         me.unsetActiveError();
252         if (hadError) {
253             me.doComponentLayout();
254         }
255     },
256
257     // private override
258     resetOriginalValue: function() {
259         // Defer resetting of originalValue until after all sub-checkboxes have been reset so we get
260         // the correct data from getValue()
261         Ext.defer(function() {
262             this.callParent();
263         }, 1, this);
264     },
265
266
267     /**
268      * <p>Sets the value(s) of all checkboxes in the group. The expected format is an Object of
269      * name-value pairs corresponding to the names of the checkboxes in the group. Each pair can
270      * have either a single or multiple values:</p>
271      * <ul>
272      *   <li>A single Boolean or String value will be passed to the <code>setValue</code> method of the
273      *   checkbox with that name. See the rules in {@link Ext.form.field.Checkbox#setValue} for accepted values.</li>
274      *   <li>An Array of String values will be matched against the {@link Ext.form.field.Checkbox#inputValue inputValue}
275      *   of checkboxes in the group with that name; those checkboxes whose inputValue exists in the array will be
276      *   checked and others will be unchecked.</li>
277      * </ul>
278      * <p>If a checkbox's name is not in the mapping at all, it will be unchecked.</p>
279      * <p>An example:</p>
280      * <pre><code>var myCheckboxGroup = new Ext.form.CheckboxGroup({
281     columns: 3,
282     items: [{
283         name: 'cb1',
284         boxLabel: 'Single 1'
285     }, {
286         name: 'cb2',
287         boxLabel: 'Single 2'
288     }, {
289         name: 'cb3',
290         boxLabel: 'Single 3'
291     }, {
292         name: 'cbGroup',
293         boxLabel: 'Grouped 1'
294         inputValue: 'value1'
295     }, {
296         name: 'cbGroup',
297         boxLabel: 'Grouped 2'
298         inputValue: 'value2'
299     }, {
300         name: 'cbGroup',
301         boxLabel: 'Grouped 3'
302         inputValue: 'value3'
303     }]
304 });
305
306 myCheckboxGroup.setValue({
307     cb1: true,
308     cb3: false,
309     cbGroup: ['value1', 'value3']
310 });</code></pre>
311      * <p>The above code will cause the checkbox named 'cb1' to be checked, as well as the first and third
312      * checkboxes named 'cbGroup'. The other three checkboxes will be unchecked.</p>
313      * @param {Object} value The mapping of checkbox names to values.
314      * @return {Ext.form.CheckboxGroup} this
315      */
316     setValue: function(value) {
317         var me = this;
318         me.batchChanges(function() {
319             me.eachBox(function(cb) {
320                 var name = cb.getName(),
321                     cbValue = false;
322                 if (value && name in value) {
323                     if (Ext.isArray(value[name])) {
324                         cbValue = Ext.Array.contains(value[name], cb.inputValue);
325                     } else {
326                         // single value, let the checkbox's own setValue handle conversion
327                         cbValue = value[name];
328                     }
329                 }
330                 cb.setValue(cbValue);
331             });
332         });
333         return me;
334     },
335
336
337     /**
338      * <p>Returns an object containing the values of all checked checkboxes within the group. Each key-value pair
339      * in the object corresponds to a checkbox {@link Ext.form.field.Checkbox#name name}. If there is only one checked
340      * checkbox with a particular name, the value of that pair will be the String
341      * {@link Ext.form.field.Checkbox#inputValue inputValue} of that checkbox. If there are multiple checked checkboxes
342      * with that name, the value of that pair will be an Array of the selected inputValues.</p>
343      * <p>The object format returned from this method can also be passed directly to the {@link #setValue} method.</p>
344      * <p>NOTE: In Ext 3, this method returned an array of Checkbox components; this was changed to make it more
345      * consistent with other field components and with the {@link #setValue} argument signature. If you need the old
346      * behavior in Ext 4+, use the {@link #getChecked} method instead.</p>
347      */
348     getValue: function() {
349         var values = {};
350         this.eachBox(function(cb) {
351             var name = cb.getName(),
352                 inputValue = cb.inputValue,
353                 bucket;
354             if (cb.getValue()) {
355                 if (name in values) {
356                     bucket = values[name];
357                     if (!Ext.isArray(bucket)) {
358                         bucket = values[name] = [bucket];
359                     }
360                     bucket.push(inputValue);
361                 } else {
362                     values[name] = inputValue;
363                 }
364             }
365         });
366         return values;
367     },
368
369     /*
370      * Don't return any data for submit; the form will get the info from the individual checkboxes themselves.
371      */
372     getSubmitData: function() {
373         return null;
374     },
375
376     /*
377      * Don't return any data for the model; the form will get the info from the individual checkboxes themselves.
378      */
379     getModelData: function() {
380         return null;
381     },
382
383     validate: function() {
384         var me = this,
385             errors = me.getErrors(),
386             isValid = Ext.isEmpty(errors),
387             wasValid = !me.hasActiveError();
388
389         if (isValid) {
390             me.unsetActiveError();
391         } else {
392             me.setActiveError(errors);
393         }
394         if (isValid !== wasValid) {
395             me.fireEvent('validitychange', me, isValid);
396             me.doComponentLayout();
397         }
398
399         return isValid;
400     }
401
402 }, function() {
403
404     this.borrow(Ext.form.field.Base, ['markInvalid', 'clearInvalid']);
405
406 });
407
408