/** * FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the * {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is rendered with * a {@link #fieldLabel field label} and optional {@link #msgTarget error message} around its sub-items. * This is useful for arranging a group of fields within a single item in a form, so * that it lines up nicely with other fields. A common use is for grouping a set of related fields under * a single label in a form. * * The container's configured {@link #cfg-items} will be layed out within the field body area according to the * configured {@link #layout} type. The default layout is `'autocontainer'`. * * Like regular fields, FieldContainer can inherit its decoration configuration from the * {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition, * FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields} * it may itself contain. * * If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or {@link Ext.form.field.Radio Radio} * fields in a single labeled container, consider using a {@link Ext.form.CheckboxGroup} * or {@link Ext.form.RadioGroup} instead as they are specialized for handling those types. * * # Example * * @example * Ext.create('Ext.form.Panel', { * title: 'FieldContainer Example', * width: 550, * bodyPadding: 10, * * items: [{ * xtype: 'fieldcontainer', * fieldLabel: 'Last Three Jobs', * labelWidth: 100, * * // The body area will contain three text fields, arranged * // horizontally, separated by draggable splitters. * layout: 'hbox', * items: [{ * xtype: 'textfield', * flex: 1 * }, { * xtype: 'splitter' * }, { * xtype: 'textfield', * flex: 1 * }, { * xtype: 'splitter' * }, { * xtype: 'textfield', * flex: 1 * }] * }], * renderTo: Ext.getBody() * }); * * # Usage of fieldDefaults * * @example * Ext.create('Ext.form.Panel', { * title: 'FieldContainer Example', * width: 350, * bodyPadding: 10, * * items: [{ * xtype: 'fieldcontainer', * fieldLabel: 'Your Name', * labelWidth: 75, * defaultType: 'textfield', * * // Arrange fields vertically, stretched to full width * layout: 'anchor', * defaults: { * layout: '100%' * }, * * // These config values will be applied to both sub-fields, except * // for Last Name which will use its own msgTarget. * fieldDefaults: { * msgTarget: 'under', * labelAlign: 'top' * }, * * items: [{ * fieldLabel: 'First Name', * name: 'firstName' * }, { * fieldLabel: 'Last Name', * name: 'lastName', * msgTarget: 'under' * }] * }], * renderTo: Ext.getBody() * }); */Ext.define('Ext.form.FieldContainer', { extend: 'Ext.container.Container', mixins: { labelable: 'Ext.form.Labelable', fieldAncestor: 'Ext.form.FieldAncestor' }, requires: 'Ext.layout.component.field.FieldContainer', alias: 'widget.fieldcontainer', componentLayout: 'fieldcontainer', componentCls: Ext.baseCSSPrefix + 'form-fieldcontainer', shrinkWrap: true, autoEl: { tag: 'div', role: 'presentation' }, childEls: [ 'containerEl' ], /** * @cfg {Boolean} combineLabels * If set to true, and there is no defined {@link #fieldLabel}, the field container will automatically * generate its label by combining the labels of all the fields it contains. Defaults to false. */ combineLabels: false, //<locale> /** * @cfg {String} labelConnector * The string to use when joining the labels of individual sub-fields, when {@link #combineLabels} is * set to true. Defaults to ', '. */ labelConnector: ', ', //</locale> /** * @cfg {Boolean} combineErrors * If set to true, the field container will automatically combine and display the validation errors from * all the fields it contains as a single error on the container, according to the configured * {@link #msgTarget}. Defaults to false. */ combineErrors: false, maskOnDisable: false, // If we allow this to mark with the invalidCls it will cascade to all // child fields, let them handle themselves invalidCls: '', fieldSubTpl: [ '<div id="{id}-containerEl" data-ref="containerEl" class="{containerElCls}"', '<tpl if="ariaAttributes">', '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>', '<tpl else>', ' role="presentation"', '</tpl>', '>', '{%this.renderContainer(out,values)%}', '</div>' ], initComponent: function() { var me = this; // Init mixins me.initLabelable(); me.initFieldAncestor(); me.callParent(); me.initMonitor(); }, onAdd: function(labelItem) { var me = this; // Fix for https://sencha.jira.com/browse/EXTJSIV-6424 Which was *sneakily* fixed fixed in version 37 // In FF < 37, positioning absolutely within a TD positions relative to the TR! // So we must add the width of a visible, left-aligned label cell to the x coordinate. if (labelItem.isLabelable && Ext.isGecko && Ext.firefoxVersion < 37 && me.layout.type === 'absolute' && !me.hideLabel && me.labelAlign !== 'top') { labelItem.x += (me.labelWidth + me.labelPad); } me.callParent(arguments); if (labelItem.isLabelable && me.combineLabels) { labelItem.oldHideLabel = labelItem.hideLabel; labelItem.hideLabel = true; } me.updateLabel(); }, onRemove: function(labelItem, isDestroying) { var me = this; me.callParent(arguments); if (!isDestroying) { if (labelItem.isLabelable && me.combineLabels) { labelItem.hideLabel = labelItem.oldHideLabel; } me.updateLabel(); } }, initRenderData: function() { var me = this, data = me.callParent(); data.containerElCls = me.containerElCls; data = Ext.applyIf(data, me.getLabelableRenderData()); if (me.labelAlign === 'top' || me.msgTarget === 'under') { data.extraFieldBodyCls += ' ' + Ext.baseCSSPrefix + 'field-container-body-vertical'; } data.tipAnchorTarget = me.id + '-containerEl'; return data; }, /** * Returns the combined field label if {@link #combineLabels} is set to true and if there is no * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override * this method to provide a custom generated label. * @template * @return {String} The label, or empty string if none. */ getFieldLabel: function() { var label = this.fieldLabel || ''; if (!label && this.combineLabels) { label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) { return field.getFieldLabel(); }).join(this.labelConnector); } return label; }, getSubTplData: function() { var ret = this.initRenderData(); Ext.apply(ret, this.subTplData); return ret; }, getSubTplMarkup: function(fieldData) { var me = this, tpl = me.lookupTpl('fieldSubTpl'), html; if (!tpl.renderContent) { me.setupRenderTpl(tpl); } html = tpl.apply(me.getSubTplData(fieldData)); return html; }, /** * @private * Updates the content of the labelEl if it is rendered */ updateLabel: function() { var me = this, label = me.labelEl; if (label) { me.setFieldLabel(me.getFieldLabel()); } }, /** * @private * Fired when the error message of any field within the container changes, and updates the * combined error message to match. */ onFieldErrorChange: function() { if (this.combineErrors) { var me = this, oldError = me.getActiveError(), invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) { return field.hasActiveError(); }), newErrors = me.getCombinedErrors(invalidFields); if (newErrors) { me.setActiveErrors(newErrors); } else { me.unsetActiveError(); } if (oldError !== me.getActiveError()) { me.updateLayout(); } } }, /** * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list of error * messages from them. Defaults to prepending each message by the field name and a colon. This * can be overridden to provide custom combined error message handling, for instance changing * the format of each message or sorting the array (it is sorted in order of appearance by default). * @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently invalid. * @return {String[]} The combined list of error messages */ getCombinedErrors: function(invalidFields) { var errors = [], f, fLen = invalidFields.length, field, activeErrors, a, aLen, error, label; for (f = 0; f < fLen; f++) { field = invalidFields[f]; activeErrors = field.getActiveErrors(); aLen = activeErrors.length; for (a = 0; a < aLen; a++) { error = activeErrors[a]; label = field.getFieldLabel(); errors.push((label ? label + ': ' : '') + error); } } return errors; }, privates: { applyTargetCls: function(targetCls) { var containerElCls = this.containerElCls; this.containerElCls = containerElCls ? containerElCls + ' ' + targetCls : targetCls; }, getTargetEl: function() { return this.containerEl; }, initRenderTpl: function() { var me = this; if (!me.hasOwnProperty('renderTpl')) { me.renderTpl = me.lookupTpl('labelableRenderTpl'); } return me.callParent(); } }});