Upgrade to ExtJS 4.0.7 - Released 10/19/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
18  * {@link Ext.form.field.Field} methods for {@link #getValue getting}, {@link #setValue setting},
19  * and {@link #validate validating} the group 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  *     @example
38  *     Ext.create('Ext.form.Panel', {
39  *         title: 'Checkbox Group',
40  *         width: 300,
41  *         height: 125,
42  *         bodyPadding: 10,
43  *         renderTo: Ext.getBody(),
44  *         items:[{
45  *             xtype: 'checkboxgroup',
46  *             fieldLabel: 'Two Columns',
47  *             // Arrange radio buttons into two columns, distributed vertically
48  *             columns: 2,
49  *             vertical: true,
50  *             items: [
51  *                 { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
52  *                 { boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true },
53  *                 { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
54  *                 { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
55  *                 { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
56  *                 { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
57  *             ]
58  *         }]
59  *     });
60  */
61 Ext.define('Ext.form.CheckboxGroup', {
62     extend:'Ext.form.FieldContainer',
63     mixins: {
64         field: 'Ext.form.field.Field'
65     },
66     alias: 'widget.checkboxgroup',
67     requires: ['Ext.layout.container.CheckboxGroup', 'Ext.form.field.Base'],
68
69     /**
70      * @cfg {String} name
71      * @hide
72      */
73
74     /**
75      * @cfg {Ext.form.field.Checkbox[]/Object[]} items
76      * An Array of {@link Ext.form.field.Checkbox Checkbox}es or Checkbox config objects to arrange in the group.
77      */
78
79     /**
80      * @cfg {String/Number/Number[]} columns
81      * Specifies the number of columns to use when displaying grouped checkbox/radio controls using automatic layout.
82      * This config can take several types of values:
83      *
84      * - 'auto' - The controls will be rendered one per column on one row and the width of each column will be evenly
85      *   distributed based on the width of the overall field container. This is the default.
86      * - Number - If you specific a number (e.g., 3) that number of columns will be created and the contained controls
87      *   will be automatically distributed based on the value of {@link #vertical}.
88      * - Array - You can also specify an array of column widths, mixing integer (fixed width) and float (percentage
89      *   width) values as needed (e.g., [100, .25, .75]). Any integer values will be rendered first, then any float
90      *   values will be calculated as a percentage of the remaining space. Float values do not have to add up to 1
91      *   (100%) although if you want the controls to take up the entire field container you should do so.
92      */
93     columns : 'auto',
94
95     /**
96      * @cfg {Boolean} vertical
97      * True to distribute contained controls across columns, completely filling each column top to bottom before
98      * starting on the next column. The number of controls in each column will be automatically calculated to keep
99      * columns as even as possible. The default value is false, so that controls will be added to columns one at a time,
100      * completely filling each row left to right before starting on the next row.
101      */
102     vertical : false,
103
104     /**
105      * @cfg {Boolean} allowBlank
106      * False to validate that at least one item in the group is checked. If no items are selected at
107      * validation time, {@link #blankText} will be used as the error text.
108      */
109     allowBlank : true,
110
111     /**
112      * @cfg {String} blankText
113      * Error text to display if the {@link #allowBlank} validation fails
114      */
115     blankText : "You must select at least one item in this group",
116
117     // private
118     defaultType : 'checkboxfield',
119
120     // private
121     groupCls : Ext.baseCSSPrefix + 'form-check-group',
122
123     /**
124      * @cfg {String} fieldBodyCls
125      * An extra CSS class to be applied to the body content element in addition to {@link #baseBodyCls}.
126      * Defaults to 'x-form-checkboxgroup-body'.
127      */
128     fieldBodyCls: Ext.baseCSSPrefix + 'form-checkboxgroup-body',
129
130     // private
131     layout: 'checkboxgroup',
132
133     initComponent: function() {
134         var me = this;
135         me.callParent();
136         me.initField();
137     },
138
139     /**
140      * Initializes the field's value based on the initial config. If the {@link #value} config is specified then we use
141      * that to set the value; otherwise we initialize the originalValue by querying the values of all sub-checkboxes
142      * after they have been initialized.
143      * @protected
144      */
145     initValue: function() {
146         var me = this,
147             valueCfg = me.value;
148         me.originalValue = me.lastValue = valueCfg || me.getValue();
149         if (valueCfg) {
150             me.setValue(valueCfg);
151         }
152     },
153
154     /**
155      * When a checkbox is added to the group, monitor it for changes
156      * @param {Object} field
157      * @protected
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 is if allowBlank
183      * is set to true and no items are checked.
184      * @return {String[]} 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 {Ext.form.field.Checkbox[]} 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 originally
237      * 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      * Sets the value(s) of all checkboxes in the group. The expected format is an Object of name-value pairs
269      * corresponding to the names of the checkboxes in the group. Each pair can have either a single or multiple values:
270      *
271      *   - A single Boolean or String value will be passed to the `setValue` method of the checkbox with that name.
272      *     See the rules in {@link Ext.form.field.Checkbox#setValue} for accepted values.
273      *   - An Array of String values will be matched against the {@link Ext.form.field.Checkbox#inputValue inputValue}
274      *     of checkboxes in the group with that name; those checkboxes whose inputValue exists in the array will be
275      *     checked and others will be unchecked.
276      *
277      * If a checkbox's name is not in the mapping at all, it will be unchecked.
278      *
279      * An example:
280      *
281      *     var myCheckboxGroup = new Ext.form.CheckboxGroup({
282      *         columns: 3,
283      *         items: [{
284      *             name: 'cb1',
285      *             boxLabel: 'Single 1'
286      *         }, {
287      *             name: 'cb2',
288      *             boxLabel: 'Single 2'
289      *         }, {
290      *             name: 'cb3',
291      *             boxLabel: 'Single 3'
292      *         }, {
293      *             name: 'cbGroup',
294      *             boxLabel: 'Grouped 1'
295      *             inputValue: 'value1'
296      *         }, {
297      *             name: 'cbGroup',
298      *             boxLabel: 'Grouped 2'
299      *             inputValue: 'value2'
300      *         }, {
301      *             name: 'cbGroup',
302      *             boxLabel: 'Grouped 3'
303      *             inputValue: 'value3'
304      *         }]
305      *     });
306      *
307      *     myCheckboxGroup.setValue({
308      *         cb1: true,
309      *         cb3: false,
310      *         cbGroup: ['value1', 'value3']
311      *     });
312      *
313      * The above code will cause the checkbox named 'cb1' to be checked, as well as the first and third checkboxes named
314      * 'cbGroup'. The other three checkboxes will be unchecked.
315      *
316      * @param {Object} value The mapping of checkbox names to values.
317      * @return {Ext.form.CheckboxGroup} this
318      */
319     setValue: function(value) {
320         var me = this;
321         me.batchChanges(function() {
322             me.eachBox(function(cb) {
323                 var name = cb.getName(),
324                     cbValue = false;
325                 if (value && name in value) {
326                     if (Ext.isArray(value[name])) {
327                         cbValue = Ext.Array.contains(value[name], cb.inputValue);
328                     } else {
329                         // single value, let the checkbox's own setValue handle conversion
330                         cbValue = value[name];
331                     }
332                 }
333                 cb.setValue(cbValue);
334             });
335         });
336         return me;
337     },
338
339
340     /**
341      * Returns an object containing the values of all checked checkboxes within the group. Each key-value pair in the
342      * object corresponds to a checkbox {@link Ext.form.field.Checkbox#name name}. If there is only one checked checkbox
343      * with a particular name, the value of that pair will be the String {@link Ext.form.field.Checkbox#inputValue
344      * inputValue} of that checkbox. If there are multiple checked checkboxes with that name, the value of that pair
345      * will be an Array of the selected inputValues.
346      *
347      * The object format returned from this method can also be passed directly to the {@link #setValue} method.
348      *
349      * NOTE: In Ext 3, this method returned an array of Checkbox components; this was changed to make it more consistent
350      * with other field components and with the {@link #setValue} argument signature. If you need the old behavior in
351      * Ext 4+, use the {@link #getChecked} method instead.
352      */
353     getValue: function() {
354         var values = {};
355         this.eachBox(function(cb) {
356             var name = cb.getName(),
357                 inputValue = cb.inputValue,
358                 bucket;
359             if (cb.getValue()) {
360                 if (name in values) {
361                     bucket = values[name];
362                     if (!Ext.isArray(bucket)) {
363                         bucket = values[name] = [bucket];
364                     }
365                     bucket.push(inputValue);
366                 } else {
367                     values[name] = inputValue;
368                 }
369             }
370         });
371         return values;
372     },
373
374     /*
375      * Don't return any data for submit; the form will get the info from the individual checkboxes themselves.
376      */
377     getSubmitData: function() {
378         return null;
379     },
380
381     /*
382      * Don't return any data for the model; the form will get the info from the individual checkboxes themselves.
383      */
384     getModelData: function() {
385         return null;
386     },
387
388     validate: function() {
389         var me = this,
390             errors = me.getErrors(),
391             isValid = Ext.isEmpty(errors),
392             wasValid = !me.hasActiveError();
393
394         if (isValid) {
395             me.unsetActiveError();
396         } else {
397             me.setActiveError(errors);
398         }
399         if (isValid !== wasValid) {
400             me.fireEvent('validitychange', me, isValid);
401             me.doComponentLayout();
402         }
403
404         return isValid;
405     }
406
407 }, function() {
408
409     this.borrow(Ext.form.field.Base, ['markInvalid', 'clearInvalid']);
410
411 });
412
413