3 * Copyright(c) 2006-2010 Ext JS, Inc.
5 * http://www.extjs.com/license
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:
14 xtype: 'compositefield',
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:
39 xtype: 'compositefield',
40 fieldLabel: 'Custom label',
44 * Any Ext.form.* component can be placed inside a composite field.
46 Ext.form.CompositeField = Ext.extend(Ext.form.Field, {
49 * @property defaultMargins
51 * The margins to apply by default to each field in the composite
53 defaultMargins: '0 5 0 0',
56 * @property skipLastItemMargin
58 * If true, the defaultMargins are not applied to the last item in the composite field set (defaults to true)
60 skipLastItemMargin: true,
63 * @property isComposite
65 * Signifies that this is a Composite field
70 * @property combineErrors
72 * True to combine errors from the individual fields into a single error message at the CompositeField level (defaults to true)
77 * @cfg {String} labelConnector The string to use when joining segments of the built label together (defaults to ', ')
82 * @cfg {Object} defaults Any default properties to assign to the child fields.
86 //Builds the composite field label
87 initComponent: function() {
92 for (var i=0, j = items.length; i < j; i++) {
95 labels.push(item.fieldLabel);
98 Ext.applyIf(item, this.defaults);
100 //apply default margins to each item except the last
101 if (!(i == j - 1 && this.skipLastItemMargin)) {
102 Ext.applyIf(item, {margins: this.defaultMargins});
106 this.fieldLabel = this.fieldLabel || this.buildLabel(labels);
109 * @property fieldErrors
110 * @type Ext.util.MixedCollection
111 * MixedCollection of current errors on the Composite's subfields. This is used internally to track when
112 * to show and hide error messages at the Composite level. Listeners are attached to the MixedCollection's
113 * add, remove and replace events to update the error icon in the UI as errors are added or removed.
115 this.fieldErrors = new Ext.util.MixedCollection(true, function(item) {
119 this.fieldErrors.on({
121 add : this.updateInvalidMark,
122 remove : this.updateInvalidMark,
123 replace: this.updateInvalidMark
126 Ext.form.CompositeField.superclass.initComponent.apply(this, arguments);
128 this.innerCt = new Ext.Container({
131 cls : 'x-form-composite',
132 defaultMargins: '0 3 0 0'
135 var fields = this.innerCt.findBy(function(c) {
136 return c.isFormField;
141 * @type Ext.util.MixedCollection
142 * Internal collection of all of the subfields in this Composite
144 this.items = new Ext.util.MixedCollection();
145 this.items.addAll(fields);
151 * Creates an internal container using hbox and renders the fields to it
153 onRender: function(ct, position) {
157 * @type Ext.Container
158 * A container configured with hbox layout which is responsible for laying out the subfields
160 var innerCt = this.innerCt;
163 this.el = innerCt.getEl();
165 //if we're combining subfield errors into a single message, override the markInvalid and clearInvalid
166 //methods of each subfield and show them at the Composite level instead
167 if (this.combineErrors) {
168 this.eachItem(function(field) {
170 markInvalid : this.onFieldMarkInvalid.createDelegate(this, [field], 0),
171 clearInvalid: this.onFieldClearInvalid.createDelegate(this, [field], 0)
176 //set the label 'for' to the first item
177 var l = this.el.parent().parent().child('label', true);
179 l.setAttribute('for', this.items.items[0].id);
183 Ext.form.CompositeField.superclass.onRender.apply(this, arguments);
187 * Called if combineErrors is true and a subfield's markInvalid method is called.
188 * By default this just adds the subfield's error to the internal fieldErrors MixedCollection
189 * @param {Ext.form.Field} field The field that was marked invalid
190 * @param {String} message The error message
192 onFieldMarkInvalid: function(field, message) {
193 var name = field.getName(),
196 errorName: field.fieldLabel || name,
200 this.fieldErrors.replace(name, error);
202 field.el.addClass(field.invalidClass);
206 * Called if combineErrors is true and a subfield's clearInvalid method is called.
207 * By default this just updates the internal fieldErrors MixedCollection.
208 * @param {Ext.form.Field} field The field that was marked invalid
210 onFieldClearInvalid: function(field) {
211 this.fieldErrors.removeKey(field.getName());
213 field.el.removeClass(field.invalidClass);
218 * Called after a subfield is marked valid or invalid, this checks to see if any of the subfields are
219 * currently invalid. If any subfields are invalid it builds a combined error message marks the composite
220 * invalid, otherwise clearInvalid is called
222 updateInvalidMark: function() {
223 var ieStrict = Ext.isIE6 && Ext.isStrict;
225 if (this.fieldErrors.length == 0) {
228 //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
230 this.clearInvalid.defer(50, this);
233 var message = this.buildCombinedErrorMessage(this.fieldErrors.items);
236 this.markInvalid(message);
238 //IE6 in strict mode has a layout bug when using 'under' as the error message target. This fixes it
240 this.markInvalid(message);
246 * Performs validation checks on each subfield and returns false if any of them fail validation.
247 * @return {Boolean} False if any subfield failed validation
249 validateValue: function() {
252 this.eachItem(function(field) {
253 if (!field.isValid()) valid = false;
260 * Takes an object containing error messages for contained fields, returning a combined error
261 * string (defaults to just placing each item on a new line). This can be overridden to provide
262 * custom combined error message handling.
263 * @param {Array} errors Array of errors in format: [{field: 'title', error: 'some error'}]
264 * @return {String} The combined error message
266 buildCombinedErrorMessage: function(errors) {
270 for (var i = 0, j = errors.length; i < j; i++) {
273 combined.push(String.format("{0}: {1}", error.errorName, error.error));
276 return combined.join("<br />");
280 * Sorts the internal fieldErrors MixedCollection by the order in which the fields are defined.
281 * This is called before displaying errors to ensure that the errors are presented in the expected order.
282 * This function can be overridden to provide a custom sorting order if needed.
284 sortErrors: function() {
285 var fields = this.items;
287 this.fieldErrors.sort("ASC", function(a, b) {
288 var findByName = function(key) {
289 return function(field) {
290 return field.getName() == key;
294 var aIndex = fields.findIndexBy(findByName(a.field)),
295 bIndex = fields.findIndexBy(findByName(b.field));
297 return aIndex < bIndex ? -1 : 1;
302 * Resets each field in the composite to their previous value
305 this.eachItem(function(item) {
309 // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete.
310 // Important because reset is being called on both the group and the individual items.
317 * Calls clearInvalid on all child fields. This is a convenience function and should not often need to be called
318 * as fields usually take care of clearing themselves
320 clearInvalidChildren: function() {
321 this.eachItem(function(item) {
327 * Builds a label string from an array of subfield labels.
328 * By default this just joins the labels together with a comma
329 * @param {Array} segments Array of each of the labels in the composite field's subfields
330 * @return {String} The built label
332 buildLabel: function(segments) {
333 return Ext.clean(segments).join(this.labelConnector);
337 * Checks each field in the composite and returns true if any is dirty
338 * @return {Boolean} True if any field is dirty
341 //override the behaviour to check sub items.
342 if (this.disabled || !this.rendered) {
347 this.eachItem(function(item){
358 * Convenience function which passes the given function to every item in the composite
359 * @param {Function} fn The function to call
360 * @param {Object} scope Optional scope object
362 eachItem: function(fn, scope) {
363 if(this.items && this.items.each){
364 this.items.each(fn, scope || this);
370 * Passes the resize call through to the inner panel
372 onResize: function(adjWidth, adjHeight, rawWidth, rawHeight) {
373 var innerCt = this.innerCt;
375 if (this.rendered && innerCt.rendered) {
376 innerCt.setSize(adjWidth, adjHeight);
379 Ext.form.CompositeField.superclass.onResize.apply(this, arguments);
384 * Forces the internal container to be laid out again
386 doLayout: function(shallow, force) {
388 var innerCt = this.innerCt;
390 innerCt.forceLayout = this.ownerCt.forceLayout;
391 innerCt.doLayout(shallow, force);
398 beforeDestroy: function(){
399 Ext.destroy(this.innerCt);
401 Ext.form.CompositeField.superclass.beforeDestroy.call(this);
404 //override the behaviour to check sub items.
405 setReadOnly : function(readOnly) {
406 if (readOnly == undefined) {
409 readOnly = !!readOnly;
412 this.eachItem(function(item){
413 item.setReadOnly(readOnly);
416 this.readOnly = readOnly;
419 onShow : function() {
420 Ext.form.CompositeField.superclass.onShow.call(this);
424 //override the behaviour to check sub items.
425 onDisable : function(){
426 this.eachItem(function(item){
431 //override the behaviour to check sub items.
432 onEnable : function(){
433 this.eachItem(function(item){
439 Ext.reg('compositefield', Ext.form.CompositeField);