Base、Field、Labelable三个类是ExtJS表单字段体系的核心。其中,Field定义了核心接口函数,Base是一个抽象基类,Labelable处理字段文字标签、错误信息等相关的逻辑。
作为所有表单字段的抽象基类,它提供了:
字段体(Field body)的HTML内容由 XTemplate:fieldSubTpl来定义,该模板的占位符数据由getSubTplData()方法提供,覆盖这两个成员可以定制自己的字段渲染方式。
Ext.form.field.Base提供的配置项/属性/方法包括:
配置项/属性/方法 | 说明 |
{checkChangeBuffer} | Number,checkChangeEvents对应的HTML事件发生时,触发checkChange()的缓冲时间,默认50ms |
{checkChangeEvents} | String[],在inputEl对应的HTML元素上需要侦听的原生HTML事件的列表。这些事件会导致checkChange()。默认值: IE:['change', 'propertychange'] 其他浏览器:['change', 'input', 'textInput', 'keyup', 'dragdrop'] |
{dirtyCls} | String,当脏值时使用的样式类 |
{fieldCls} | String,字段输入元素的样式类 |
{fieldStyle} | String,可选的INPUT元素的样式 |
{fieldSubTpl} | String,私有配置,提供INPUT的模板 |
{focusCls} | String,字段获得焦点时的样式 |
{readOnlyCls} | String,只读样式类,应用到字段的主要元素 |
{invalidCls} | String,字段验证失败时的样式类 |
{inputAttrTpl} | String/Array/Ext.XTemplate,用作字段INPUT元素的属性的模板,getSubTplData()是其占位符上下文 |
{inputId} | String,用作字段INPUT元素的ID,必须全局唯一 |
{inputType} | String,用作字段INPUT元素的type属性,例如radio、text、password、file |
{invalidText} | String,如果没有提供定制消息,验证失败时显示的文本 |
{msgTarget} | |
{name} | String,字段的名称,默认inputId |
{readOnly} | Boolean,用于在HTML显示为只读,会设置INPUT的readOnly=true |
{tabIndex} | Number,用于TAB键切换顺序的索引 |
{validateOnBlur} | Boolean,是否在失去焦点时执行验证,默认true |
inputEl | Element,字段的INPUT元素,只有在渲染(render)后可用 |
clearInvalid() | 覆盖了Field混入的方法。清除无效样式和消息。 |
getInputId() | 获取INPUT元素的ID |
getRawValue() | String,获取原始值,不进行任何正常化、转换、验证 |
getSubTplData() | Object,创建并返回用于fieldSubTpl的占位符上下文对象 |
getSubTplMarkup() | 覆盖Labelable混入的方法 |
getSubmitValue() | String,得到用于标准表单提交的字段值,提交时的形式为name=value,如果值为空串则name=,如果值为null则不提交 |
isValid() | 覆盖了Field混入的方法。对原始值进行预处理并验证预处理的结果值 |
markInvalid() | 覆盖了Field混入的方法。根据msgTarget设置来显示错误信息,设置invalidCls样式 |
processRawValue() | Object (Object value),在准备对原始值进行转换、验证之前,进行必要的预处理。这些处理包括:去除多余的无效字符。提供了缺省适配 |
setFieldStyle() | 用于设置INPUT的样式 |
rawToValue() | 把原始的INPUT值转换为与特定字段类型匹配的数据值(例如对于日期字段,需要转换为Date)。该方法会被getValue()调用。提供了缺省适配 |
valueToRaw() | 用于把与特定字段类型匹配的数据值转化为方便在INPUT中显示的原始值形式,每次setValue()时,均会调用该方法设置原始值 |
setRawValue() | Object (Object value),跳过valueToRaw()、change事件检测、验证,仅进行transformRawValue(),直接设置原始值 |
transformRawValue() | Object (Object value),在setRawValue()之前,对参数进行转换 |
setReadOnly() | 设置只读状态 |
setValue() |
覆盖了Field混入的方法:
|
getValue() |
覆盖了Field混入的方法:
|
validateValue() |
Boolean (Object value),调用getErrors()生成验证错误,如果存在错误,则调用 markInvalid(),根据有无错误返回false/true |
⚡specialkey | specialkey( Ext.form.field.Base this, Ext.EventObject e, Object eOpts ) 与导航相关的按键事件被触发(箭头、TAB、回车、ESC)时触发 |
⚡writeablechange | writeablechange( Ext.form.field.Base this, Boolean Read, Object eOpts ) 当字段的readOnly状态改变时触发 |
Ext.form.field.Base的源代码分析:
Ext.define('Ext.form.field.Base', { extend: 'Ext.Component', mixins: { labelable: 'Ext.form.Labelable', field: 'Ext.form.field.Field' }, alias: 'widget.field', alternateClassName: ['Ext.form.Field', 'Ext.form.BaseField'], requires: ['Ext.util.DelayedTask', 'Ext.XTemplate', 'Ext.layout.component.field.Field'], //子类可以修改此模板以改变字段的UI,这里就是生成一个HTML的input元素。 //注意这里叫“子模板”是因为字段HTML还有外部包装元素,外部元素的渲染是Labelable混入负责的 fieldSubTpl: [ ' name="{name}"', //values是XTemplate内置变量,代表当前占位符替换时的上下文对象 ' value="{[Ext.util.Format.htmlEncode(values.value)]}"', ' placeholder="{placeholder}"', '{%if (values.maxLength !== undefined){%} maxlength="{maxLength}"{%}%}', ' readonly="readonly"', ' disabled="disabled"', ' tabIndex="{tabIdx}"', ' style="{fieldStyle}"', ' class="{fieldCls} {typeCls} {editableCls}" autocomplete="off"/>', { disableFormats: true } ], //需要插入字段模板的内容 subTplInsertions: [ 'inputAttrTpl' //增加Input元素的属性 ], inputType: 'text', invalidText : 'The value in this field is invalid', fieldCls : Ext.baseCSSPrefix + 'form-field', focusCls : 'form-focus', dirtyCls : Ext.baseCSSPrefix + 'form-dirty', //用于检测值变更的原始DOM事件 checkChangeEvents: Ext.isIE && (!document.documentMode || document.documentMode < 9) ? ['change', 'propertychange'] : ['change', 'input', 'textInput', 'keyup', 'dragdrop'], checkChangeBuffer: 50, componentLayout: 'field', //组件布局的类型 readOnly : false, readOnlyCls: Ext.baseCSSPrefix + 'form-readonly', validateOnBlur: true, // private hasFocus : false, baseCls: Ext.baseCSSPrefix + 'field', maskOnDisable: false, // private initComponent : function() { var me = this; me.callParent(); me.subTplData = me.subTplData || {}; //初始化模板数据 me.addEvents( 'specialkey', 'writeablechange' ); //初始化两个混入类 me.initLabelable(); me.initField(); if (!me.name) { me.name = me.getInputId(); } }, beforeRender: function(){ var me = this; me.callParent(arguments); me.beforeLabelableRender(arguments); //如果被设置为只读,则在渲染前添加只读样式类 if (me.readOnly) { me.addCls(me.readOnlyCls); } }, getInputId: function() { return this.inputId || (this.inputId = this.id + '-inputEl'); }, //获取字段UI模板的上下文数据 getSubTplData: function() { var me = this, type = me.inputType, inputId = me.getInputId(), data; data = Ext.apply({ id : inputId, cmpId : me.id, name : me.name || inputId, disabled : me.disabled, readOnly : me.readOnly, value : me.getRawValue(), type : type, fieldCls : me.fieldCls, fieldStyle : me.getFieldStyle(), tabIdx : me.tabIndex, typeCls : Ext.baseCSSPrefix + 'form-' + (type === 'password' ? 'text' : type) }, me.subTplData); me.getInsertionRenderData(data, me.subTplInsertions); return data; }, afterFirstLayout: function() { this.callParent(); var el = this.inputEl; if (el) { el.selectable(); } }, applyRenderSelectors: function() { var me = this; me.callParent(); //inputEl属性不一定总是存在子类可以不渲染inputEl me.inputEl = me.el.getById(me.getInputId()); }, //覆盖Labelable的方法,得到完成占位符替换的字段UI模板,对应的HTML会被插入外部模板(默认来自Labelable)的bodyEl占位符中 getSubTplMarkup: function() { return this.getTpl('fieldSubTpl').apply(this.getSubTplData()); }, //覆盖AbstractComponent混入的Renderable的方法,决定组件如何混入 initRenderTpl: function() { var me = this; //如果没有定义自己的渲染模板,则使用从Labelable混入的渲染模板 if (!me.hasOwnProperty('renderTpl')) { me.renderTpl = me.getTpl('labelableRenderTpl'); } return me.callParent(); }, //覆盖AbstractComponent混入的Renderable的方法,初始化渲染时的上下文数据 initRenderData: function() { //把来自Labelable的上下文数据合并进去 return Ext.applyIf(this.callParent(), this.getLabelableRenderData()); }, setFieldStyle: function(style) { var me = this, inputEl = me.inputEl; if (inputEl) { inputEl.applyStyles(style); } me.fieldStyle = style; }, getFieldStyle: function() { return 'width:100%;' + (Ext.isObject(this.fieldStyle) ? Ext.DomHelper.generateStyles(this.fieldStyle) : this.fieldStyle ||''); }, // private onRender : function() { var me = this; me.callParent(arguments); //渲染Labelable、错误信息 me.onLabelableRender(); me.renderActiveError(); }, getFocusEl: function() { return this.inputEl; }, isFileUpload: function() { return this.inputType === 'file'; }, extractFileInput: function() { var me = this, fileInput = me.isFileUpload() ? me.inputEl.dom : null, clone; if (fileInput) { clone = fileInput.cloneNode(true); fileInput.parentNode.replaceChild(clone, fileInput); me.inputEl = Ext.get(clone); } return fileInput; }, // private 覆盖Field的方法,与getSubmitValue()逻辑一致 getSubmitData: function() { var me = this, data = null, val; if (!me.disabled && me.submitValue && !me.isFileUpload()) { val = me.getSubmitValue(); if (val !== null) { data = {}; data[me.getName()] = val; } } return data; }, getSubmitValue: function() { return this.processRawValue(this.getRawValue()); }, //获得原始值,如果字段有inputEl,则使用其HTML属性value,否则使用成员变量rawValue getRawValue: function() { var me = this, v = (me.inputEl ? me.inputEl.getValue() : Ext.value(me.rawValue, '')); me.rawValue = v; return v; }, //设置原始值,transformRawValue()方法的唯一意义就是在设置原始值时对入参进行转换 setRawValue: function(value) { var me = this; value = Ext.value(me.transformRawValue(value), ''); me.rawValue = value; if (me.inputEl) { me.inputEl.dom.value = value; } return value; }, transformRawValue: function(value) { return value; }, //缺省实现就是将数据值转换为字符串 valueToRaw: function(value) { return '' + Ext.value(value, ''); }, //缺省实现认为原始值与数据值一致 rawToValue: function(rawValue) { return rawValue; }, //原始值预处理 processRawValue: function(value) { return value; }, //在Ext.form.field.Base类层次下,原始值(rawValue)是中心,而不是接口Field定义的数据值(value) //数据值是根据原始值导出的 //该方法具有边际效应,即获取数据值的时候也更新了它 getValue: function() { var me = this, val = me.rawToValue(me.processRawValue(me.getRawValue())); me.value = val; return val; }, //设置值之前先设置原始值 setValue: function(value) { var me = this; me.setRawValue(me.valueToRaw(value)); return me.mixins.field.setValue.call(me, value); }, onBoxReady: function() { var me = this; me.callParent(); if (me.setReadOnlyOnBoxReady) { me.setReadOnly(me.readOnly); } }, //private onDisable: function() { var me = this, inputEl = me.inputEl; me.callParent(); if (inputEl) { inputEl.dom.disabled = true; if (me.hasActiveError()) { me.clearInvalid(); me.needsValidateOnEnable = true; } } }, //private onEnable: function() { var me = this, inputEl = me.inputEl; me.callParent(); if (inputEl) { inputEl.dom.disabled = false; if (me.needsValidateOnEnable) { delete me.needsValidateOnEnable; //启用后立即验证以触发错误 me.forceValidation = true; me.isValid(); delete me.forceValidation; } } }, setReadOnly: function(readOnly) { var me = this, inputEl = me.inputEl; readOnly = !!readOnly; me[readOnly ? 'addCls' : 'removeCls'](me.readOnlyCls); me.readOnly = readOnly; if (inputEl) { inputEl.dom.readOnly = readOnly; } else if (me.rendering) { me.setReadOnlyOnBoxReady = true; } me.fireEvent('writeablechange', me, readOnly); }, // private fireKey: function(e){ if(e.isSpecialKey()){ this.fireEvent('specialkey', this, new Ext.EventObjectImpl(e)); } }, //由Renderable在afterRender之后调用,可以完成必要的事件监听器注册 initEvents : function(){ var me = this, inputEl = me.inputEl, onChangeTask, onChangeEvent, events = me.checkChangeEvents, e, eLen = events.length, event; if (me.inEditor) { me.onBlur = Ext.Function.createBuffered(me.onBlur, 10); } if (inputEl) { me.mon(inputEl, Ext.EventManager.getKeyEvent(), me.fireKey, me); //用于实时监听值改变 onChangeTask = new Ext.util.DelayedTask(me.checkChange, me); me.onChangeEvent = onChangeEvent = function() { onChangeTask.delay(me.checkChangeBuffer); }; for (e = 0; e < eLen; e++) { event = events[e]; if (event === 'propertychange') { me.usesPropertychange = true; } me.mon(inputEl, event, onChangeEvent); } } me.callParent(); }, //执行组件布局 doComponentLayout: function() { var me = this, inputEl = me.inputEl, usesPropertychange = me.usesPropertychange, ename = 'propertychange', onChangeEvent = me.onChangeEvent; if (usesPropertychange) { me.mun(inputEl, ename, onChangeEvent); } me.callParent(arguments); if (usesPropertychange) { me.mon(inputEl, ename, onChangeEvent); } }, onDirtyChange: function(isDirty) { //注意这种写法,比if-else结构清爽 this[isDirty ? 'addCls' : 'removeCls'](this.dirtyCls); }, isValid : function() { var me = this, disabled = me.disabled, validate = me.forceValidation || !disabled; return validate ? me.validateValue(me.processRawValue(me.getRawValue())) : disabled; }, validateValue: function(value) { var me = this, errors = me.getErrors(value), //来自Field混入的getErrors()是数据验证的核心 isValid = Ext.isEmpty(errors); if (!me.preventMark) { if (isValid) { me.clearInvalid(); } else { me.markInvalid(errors); } } return isValid; }, markInvalid : function(errors) { var me = this, oldMsg = me.getActiveError(); me.setActiveErrors(Ext.Array.from(errors)); if (oldMsg !== me.getActiveError()) { me.updateLayout(); } }, clearInvalid : function() { var me = this, hadError = me.hasActiveError(); me.unsetActiveError(); if (hadError) { me.updateLayout(); } }, renderActiveError: function() { var me = this, hasError = me.hasActiveError(); if (me.inputEl) { me.inputEl[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field'); } me.mixins.labelable.renderActiveError.call(me); }, getActionEl: function() { return this.inputEl || this.el; } });
但凡混入了该类的ExtJS类型,都可以视作“表单字段”。该混入为表单字段提供了行为、状态的公共接口::
直接混入了该类的类型包括:
需要自定义表单字段时,通常不需要直接使用该混入,最好是继承Ext.form.field.Base。
Ext.form.field.Field提供的配置项/属性/方法包括:
配置项/属性/方法 | 说明 |
{disabled} | Boolean,该字段是否禁用,禁用的字段不会被提交(Ext.form.Basic.sumbit) |
{name} | String,该字段的名称,在提交(Ext.form.Basic.sumbit)时,将作为请求参数名 |
{submitValue} | Boolean,设置为false则不会提交该字段,即使disabled=false |
{validateOnChange} | Boolean,该字段的值发生变化时,是否立即执行验证。如果验证结果发生改变,发布validitychange事件。即使设置为false,也不代表完全禁用验证,表单提交时可以执行验证 |
{value} | Object,该字段的初始值 |
isFormField | Boolean,总是真,是否表单字段 |
originalValue | Object,字段的原始值,要么是配置项提供的,要么是trackResetOnLoad=true的表单的load方法提供的 |
batchChanges(fn) | 在函数里执行若干可能导致字段值改变的逻辑,防止过多的change事件被触发 |
beforeReset() | 模板方法,在重置前触发 |
checkChange() |
检查值是否发生变化(从上一次检查至今),如果变化,则:
|
checkDirty() |
检查字段的脏值状态是否发生变化(从上一次检查至今),如果变化,则发布dirtychange |
clearInvalid() |
子类必须事先该方法。清除字段的无效值样式、消息 |
str[] getErrors(value) |
运行该字段的验证器(Validators)并得到任何错误的消息数组。一般不需要手工调用,在验证过程中会内部调用。可以传递一个值,如果不传递,则使用当前字段值 |
obj getModelData() |
得到用于设置到Ext.data.Model的字段值。该方法由Ext.form.Basic.updateRecord调用。 该方法通常返回具有单个属性的对象:{字段名:字段数据值},某些高级字段可能返回具有多个属性的对象,返回值会被设置到Model对应的字段中。该方法返回的值可能为经验证。 |
obj getSubmitData() |
得到用于标准表单提交的字段值。 该方法通常返回具有单个属性的对象:{字段名:字段字面值},某些高级字段可能返回具有多个属性的对象。该方法返回的值可能为经验证。 |
obj getValue() |
获取字段当前的“数据”值,值的类型取决于字段的类型,例如对于Ext.form.field.Date会返回一个日期对象 |
initField() |
在当前字段实例上初始化当前混入。字段必须在初始化过程中调用该方法 |
initValue() |
根据初始化配置项来设定字段值 |
isDirty() |
字段值是否想对于originalValue发生变化。如果disabled则一直为false |
isEqual(val1,val2) |
子类可以覆盖该方法,用于判断两个值是否在逻辑上相等 |
isFileUpload() |
判断当前字段是不是文件上传,对于文件上传,表单将使用特殊的Ajax技术来处理之 |
HtmlEl extractFileInput() |
如果isFileUpload()=true,该方法有意义。返回持有用户输入的input type=file HTML元素 |
isValid() |
通过执行验证当前值,返回字段值的有效性。该方法不会导致 validitychange事件的触发。注意disabled字段总返回true 子类覆盖该方法时,应当注意不要有边际效应,例如导致错误信息、样式的显示 |
validate() |
通过执行验证当前值,返回字段值的有效性。该方法可能触发validitychange事件。注意disabled字段总返回true 子类覆盖该方法时,可以有边际效应,例如导致错误信息、样式的显示 |
markInvalid(str[] errs) |
子类需要实现该方法。关联一个/多个错误消息到该字段,并且渲染这些消息。该方法与值的有效性无关 |
reset() |
清除任何验证消息,把值设置为初始加载的值 |
resetOriginalValue() |
重置初始值为当前值,由trackResetOnLoad=true的lExt.form.Basic.setValues()方法调用 |
setValue(obj) |
设置字段的值,并且执行change检测、有效性验证 |
transformOriginalValue(val) |
子类可以覆盖该方法,字段初始化时,会自动使用该方法转换配置传来的值 |
⚡change |
change(this, Object newValue, Object oldValue, Object eOpts ) 由setValue()方法触发,如果值发生了变化 |
⚡dirtychange |
dirtychange( this, Boolean isDirty, Object eOpts ) 当dirtry状态发生变化时,触发该事件 |
⚡validitychange |
validitychange( this, Boolean isValid, Object eOpts ) 当有效性状态发生变化时,触发该事件 |
Ext.form.field.Field的源代码分析,比较简单,主要关心字段的读、写、脏、验这四个方面,不包含任何UI相关的逻辑:
Ext.define('Ext.form.field.Field', { isFormField : true, disabled : false, submitValue: true, validateOnChange: true, suspendCheckChange: 0, /** * 字段在初始化时必须调用该方法 */ initField: function() { //添加基础事件 this.addEvents( 'change', 'validitychange', 'dirtychange' ); this.initValue(); }, /** * 初始化字段值 */ initValue: function() { var me = this; me.value = me.transformOriginalValue(me.value);//对配置项提供的值进行转换 me.originalValue = me.lastValue = me.value; //设置原始值 me.suspendCheckChange++; //有很多这样的计数器,来判断是否允许触发change事件 me.setValue(me.value); me.suspendCheckChange--; }, setValue: function(value) { var me = this; me.value = value; me.checkChange(); //检查是否需要触发change事件 return me; }, //覆写此方法,会影响change事件的触发 isEqual: function(value1, value2) { return String(value1) === String(value2); }, isEqualAsString: function(value1, value2){ return String(Ext.value(value1, '')) === String(Ext.value(value2, '')); }, getSubmitData: function() { var me = this, data = null; if (!me.disabled && me.submitValue && !me.isFileUpload()) { data = {}; data[me.getName()] = '' + me.getValue();//默认的字符串转换 } return data;//如果禁用,将返回null }, getModelData: function() { var me = this, data = null; if (!me.disabled && !me.isFileUpload()) { data = {}; data[me.getName()] = me.getValue(); } return data;//如果禁用,将返回null }, reset : function(){ var me = this; me.beforeReset(); me.setValue(me.originalValue); me.clearInvalid(); //恢复原始的验证状态 delete me.wasValid; }, resetOriginalValue: function() { this.originalValue = this.getValue();//直接设置当前值为原始值 this.checkDirty(); }, checkChange: function() { if (!this.suspendCheckChange) {//如果没有暂停值变化检测 var me = this, newVal = me.getValue(), oldVal = me.lastValue; if (!me.isEqual(newVal, oldVal) && !me.isDestroyed) { me.lastValue = newVal; me.fireEvent('change', me, newVal, oldVal); me.onChange(newVal, oldVal); } } }, //私有方法 onChange: function(newVal, oldVal) { if (this.validateOnChange) { this.validate(); } this.checkDirty(); }, isDirty : function() { var me = this; return !me.disabled && !me.isEqual(me.getValue(), me.originalValue); }, checkDirty: function() { var me = this, isDirty = me.isDirty(); if (isDirty !== me.wasDirty) { me.fireEvent('dirtychange', me, isDirty); me.onDirtyChange(isDirty); me.wasDirty = isDirty; } }, /*验证的底层都是调用getErrors()*/ isValid : function() { var me = this; return me.disabled || Ext.isEmpty(me.getErrors()); }, validate : function() { var me = this, isValid = me.isValid(); if (isValid !== me.wasValid) { me.wasValid = isValid; me.fireEvent('validitychange', me, isValid); } return isValid; }, batchChanges: function(fn) { try { this.suspendCheckChange++; fn(); } catch(e){ throw e; } finally { this.suspendCheckChange--; } this.checkChange(); }, isFileUpload: function() { return false; }, /*下面都是缺省适配 */ extractFileInput: function() { return null; }, markInvalid: Ext.emptyFn, //添加字段的无效值样式、消息 clearInvalid: Ext.emptyFn, //清除字段的无效值样式、消息 beforeReset: Ext.emptyFn, //模板方法 transformOriginalValue: function(value){ return value; }, getName: function() { return this.name; }, getValue: function() { return this.value; }, onDirtyChange: Ext.emptyFn, getErrors: function(value) { return []; } });
该混入类可以为组件(通常用于表单字段)添加标签、错误消息。Ext.form.FieldContainer、Ext.form.field.Base、Ext.form.field.HtmlEditor混入了该类,通常不需要直接使用该混入类。
使用该混入的组件应当使用Field组件布局(或其衍生形式)以便确定标签、消息的位置大小,在组件初始化阶段(initComponent)应当调用initLabelable进行该混入的初始化。
Ext.form.Labelable提供的配置项/属性/方法包括:
配置项/属性/方法 | 说明 |
{activeError} | String,如果指定该配置项,在组件渲染后立即显示错误信息。渲染后可以使用 setActiveError/unsetActiveError方法修改 |
{activeErrorsTpl} | String/String[]/Ext.XTemplate,传递给setActiveErrors()的模板,用来格式化一组错误消息为HTML片段 |
{afterBodyEl} | String/Array/Ext.XTemplate,可选的用于插入表单字段包含input的元素之后的模板,上下文为AbstractComponent.renderData |
{afterLabelTextTpl} | String/Array/Ext.XTemplate,可选的用于插入表单字段标签文本(Label Text)之后的模板,上下文为AbstractComponent.renderData |
{afterLabelTpl} | String/Array/Ext.XTemplate,可选的用于插入表单字段标签元素(Label Element)之后的模板,上下文为AbstractComponent.renderData |
{afterSubTpl} | String/Array/Ext.XTemplate,可选的用于插入表单字段getSubTplMarkup()标记之后的模板,上下文为AbstractComponent.renderData |
{autoFitErrors} | Boolean,调整组件大小(与标签无关),为side、under错误消息提供空间 |
{baseBodyCls} | String,应用到字段体内容元素的样式,默认“x-form-item-body” |
{beforeBodyEl} | 参考afterXxx部分的配置 |
{beforeLabelTextTpl} | |
{beforeLabelTpl} | |
{beforeSubTpl} | |
{errorMsgCls} | String,应用到错误消息元素的样式 |
{fieldBodyCls} | String,额外(baseBodyCls)字段体内容元素样式 |
{fieldLabel} | String,标签内容 |
{formItemCls} | String,用于最外层元素的样式类,指示参与表单的字段布局 |
{hideEmptyLabel } | Boolean,如果fieldLabel为空,是否自动隐藏,默认true |
{hideLabel} | Boolean,是否隐藏标签 |
{invalidCls} | String,当验证失败时,应用到组件的样式,默认“x-form-invalid” |
{labelAlign} | String,控制标签的位置与对齐:left、top、right |
{labelAttrTpl} | String/Array/Ext.XTemplate,可选的、作为属性插入到标签对应元素的HTML属性的模板 |
{labelCls} | String,标签的样式,默认“x-form-item-label” |
{labelClsExtra} | String,标签的额外样式 |
{labelPad} | Number,标签与input之间的补白,默认5像素 |
{labelSeparator} | String,添加到标签尾部的文字,默认为冒号(:) |
{labelStyle} | String,应用到标签的样式文本 |
{labelWidth} | Number,对于left、right位置的标签,用于指定标签宽度,默认100像素 |
{labelableRenderTpl} |
String/Array/Ext.XTemplate,私有。表单字段装饰的渲染模板,使用该混入的组件应当事先逻辑,把该渲染模板包含在其renderTpl中。并且实现getSubTplMarkup()方法以生成字段体的内容(field body content)。 默认模板使用HTML表格(table)来组织label 、InputField、sideErrorEl |
{msgTarget} | String,错误信息应当显示的位置。支持以下值: qtip 默认值,当用户鼠标悬停字段时显示错误信息 title 显示为底层HTML输入的title属性 under 在字段下面显示一个块状区域,其中包含错误信息 side 在字段右侧显示一个图标,鼠标悬停时显示错误信息 none 不显示错误信息 [element id] 错误信息渲染为id对应HTML元素的innerHTML |
{preventMark} | Boolean,设置为真,禁止显示任何错误信息 |
bodyEl | Element,包裹字段内容的div元素,仅在渲染后可用 |
errorEl | Element,包含错误信息的div元素 |
isFieldLabelable | Boolean,指示当前组件是否表单字段,总是true |
labelCell | Element,包含标签的td元素,仅在渲染后可用 |
labelEl | Element,标签元素,仅在渲染后可用 |
noWrap | Boolean,提示布局系统高度可以立即获得 |
getActiveError() | String,获取当前错误信息,不会触发验证动作 |
getActiveErrors( ) | String[],获取当前错误信息的数组,不会触发验证动作 |
getFieldLabel( ) | String,模板方法。返回当前字段的标签,默认实现仅仅是返回fieldLabel配置 |
getInputId( ) | String,获取input元素的id |
getLabelableRenderData() | Object,生成用于填充被Labelable装饰了的表单字段HTML标记模板的上下文数据 |
getSubTplMarkup( ) | String,用于插入到外部模板bodyEl的模板,默认空,混入Labelable的类应当实现之 |
hasActiveError( ) | Boolean,当前是否存在错误 |
hasVisibleLabel( ) | Boolean,是否具有可见的标签 |
initLabelable( ) | 初始化该混入 |
renderActiveError( ) | 私有方法,渲染当前错误 |
setActiveError() | 设置当前错误 |
setActiveErrors() | 设置当前错误 |
setFieldDefaults() | 应用到当前表单字段的默认配置 |
setFieldLabel() | 设置标签 |
trimLabelSeparator() | 返回剔除冒号的标签 |
unsetActiveError() | 清除当前错误消息,仅仅是清除错误消息元素的文本和属性,需要手工调用doComponentLayou()来重新布局 |
⚡errorchange |
errorchange( Ext.form.Labelable this, String error, Object eOpts ) 当通过setActiveError()改变错误消息时触发。 |
Ext.form.Labelable的源代码分析,关注的内容主要是标签、错误信息的显示和布局:
Ext.define("Ext.form.Labelable", { requires: ['Ext.XTemplate'], autoEl: { tag: 'table', cellpadding: 0 }, //描述组件子元素的数组,渲染到renderTpl中 childEls: [ 'labelCell', 'labelEl', 'bodyEl', 'sideErrorCell', 'errorEl', 'inputRow', 'bottomPlaceHolder' ], //修饰了标签、错误信息后的字段的HTML标记模板 //Ext.form.field.Base使用该模板作为其默认的组件renderTpl labelableRenderTpl: [ //标签部分 '<tr id="{id}-inputRow" <tpl if="inFormLayout">id="{id}"</tpl>>', '<tpl if="labelOnLeft">', //如果:左侧标签 '<td id="{id}-labelCell" style="{labelCellStyle}" {labelCellAttrs}>', '{beforeLabelTpl}',//子模板:标签元素之前 //子模板:标签元素属性 '<label id="{id}-labelEl" {labelAttrTpl}<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"', '<tpl if="labelStyle"> style="{labelStyle}"</tpl>>', '{beforeLabelTextTpl}', //子模板:标签文本之前 //文字标签 冒号 '<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>', '{afterLabelTextTpl}', //子模板:标签文本之后 '</label>', '{afterLabelTpl}', //子模板:标签元素之后 '</td>', '</tpl>', //如果结束:左侧标签 '<td class="{baseBodyCls} {fieldBodyCls}" id="{id}-bodyEl" colspan="{bodyColspan}" role="presentation">', '{beforeBodyEl}', //在字段bodyEl之前的元素 '<tpl if="labelAlign==\'top\'">', //如果:顶部标签 '{beforeLabelTpl}', //子模板:标签元素之前 '<div id="{id}-labelCell" style="{labelCellStyle}">', //子模板:标签元素属性 '<label id="{id}-labelEl" {labelAttrTpl}<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"', '<tpl if="labelStyle"> style="{labelStyle}"</tpl>>', '{beforeLabelTextTpl}',//子模板:标签文本之前 //文字标签 冒号 '<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>', '{afterLabelTextTpl}', //子模板:标签文本之后 '</label>', '</div>', '{afterLabelTpl}', //子模板:标签元素之后 '</tpl>', //结束如果:顶部标签 '{beforeSubTpl}', //字段主体标记之前 '{[values.$comp.getSubTplMarkup()]}', //字段主体标记,调用字段的getSubTplMarkup()得到 '{afterSubTpl}', //字段主体标记之后 //错误消息部分 '<tpl if="msgTarget===\'side\'">', //右侧 '{afterBodyEl}', //在字段bodyEl之后的元素 '</td>', '<td id="{id}-sideErrorCell" vAlign="{[values.labelAlign===\'top\' && !values.hideLabel ? \'bottom\' : \'middle\']}" style="{[values.autoFitErrors ? \'display:none\' : \'\']}" width="{errorIconWidth}">', '<div id="{id}-errorEl" class="{errorMsgCls}" style="display:none;width:{errorIconWidth}px"></div>', '</td>', '<tpl elseif="msgTarget==\'under\'">', //下面 '<div id="{id}-errorEl" class="{errorMsgClass}" colspan="2" style="display:none"></div>', '{afterBodyEl}', //在字段bodyEl之后的元素 '</td>', '</tpl>', '</tr>', { disableFormats: true } ], //当前错误信息的HTML标记模板 activeErrorsTpl: [ '<tpl if="errors && errors.length">', '<ul><tpl for="errors"><li>{.}</li></tpl></ul>', '</tpl>' ], isFieldLabelable: true, formItemCls: Ext.baseCSSPrefix + 'form-item', labelCls: Ext.baseCSSPrefix + 'form-item-label', errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg', baseBodyCls: Ext.baseCSSPrefix + 'form-item-body', fieldBodyCls: '', clearCls: Ext.baseCSSPrefix + 'clear', invalidCls : Ext.baseCSSPrefix + 'form-invalid', fieldLabel: undefined, labelAlign : 'left', labelWidth: 100, labelPad : 5, labelSeparator : ':', hideLabel: false, hideEmptyLabel: true, preventMark: false, autoFitErrors: true, msgTarget: 'qtip', noWrap: true, //模板插入点成员属性声明 labelableInsertions: [], isFieldLabelable: true, formItemCls: Ext.baseCSSPrefix + 'form-item', labelCls: Ext.baseCSSPrefix + 'form-item-label', errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg', baseBodyCls: Ext.baseCSSPrefix + 'form-item-body', fieldBodyCls: '', clearCls: Ext.baseCSSPrefix + 'clear', invalidCls : Ext.baseCSSPrefix + 'form-invalid', fieldLabel: undefined, labelAlign : 'left', labelWidth: 100, labelPad : 5, labelSeparator : ':', hideLabel: false, hideEmptyLabel: true, preventMark: false, autoFitErrors: true, msgTarget: 'qtip', noWrap: true, //模板插入点成员属性声明 labelableInsertions: [ 'beforeBodyEl', 'afterBodyEl', 'beforeLabelTpl', 'afterLabelTpl', 'beforeSubTpl', 'afterSubTpl', 'beforeLabelTextTpl', 'afterLabelTextTpl', 'labelAttrTpl' ], labelableRenderProps: [ 'allowBlank', 'id', 'labelAlign', 'fieldBodyCls', 'baseBodyCls', 'clearCls', 'labelSeparator', 'msgTarget' ], //在字段的initComponent阶段调用 initLabelable: function() { var me = this, padding = me.padding; if (padding) { me.padding = undefined; me.extraMargins = Ext.Element.parseBox(padding); } me.addCls(me.formItemCls); me.lastActiveError = ''; me.addEvents( 'errorchange' ); }, trimLabelSeparator: function() { var me = this, separator = me.labelSeparator, label = me.fieldLabel || '', lastChar = label.substr(label.length - 1); return lastChar === separator ? label.slice(0, -1) : label; }, getFieldLabel: function() { return this.trimLabelSeparator(); }, setFieldLabel: function(label){ label = label || ''; var me = this, separator = me.labelSeparator, labelEl = me.labelEl; me.fieldLabel = label; if (me.rendered) { if (Ext.isEmpty(label) && me.hideEmptyLabel) { labelEl.parent().setDisplayed('none'); } else { if (separator) { label = me.trimLabelSeparator() + separator; } labelEl.update(label); labelEl.parent().setDisplayed(''); } me.updateLayout(); } }, getInsertionRenderData: function (data, names) { var i = names.length, name, value; while (i--) { name = names[i]; value = this[name]; if (value) { if (typeof value != 'string') { if (!value.isTemplate) { value = Ext.XTemplate.getTpl(this, name); } value = value.apply(data); } } data[name] = value || ''; } return data; }, //收集渲染数据 getLabelableRenderData: function() { var me = this, data, tempEl, topLabel = me.labelAlign === 'top'; if (!Ext.form.Labelable.errorIconWidth) { Ext.form.Labelable.errorIconWidth = (tempEl = Ext.resetElement.createChild({style: 'position:absolute', cls: Ext.baseCSSPrefix + 'form-invalid-icon'})).getWidth(); tempEl.remove(); } //复制并覆盖默认值 data = Ext.copyTo({ inFormLayout : me.ownerLayout && me.ownerLayout.type === 'form', inputId : me.getInputId(), labelOnLeft : !topLabel, hideLabel : !me.hasVisibleLabel(), fieldLabel : me.getFieldLabel(), labelCellStyle : me.getLabelCellStyle(), labelCellAttrs : me.getLabelCellAttrs(), labelCls : me.getLabelCls(), labelStyle : me.getLabelStyle(), bodyColspan : me.getBodyColspan(), externalError : !me.autoFitErrors, errorMsgCls : me.getErrorMsgCls(), errorIconWidth : Ext.form.Labelable.errorIconWidth }, me, me.labelableRenderProps, true); //用于插入主模板的若干变量或者模板,模板在getInsertionRenderData完成替换 me.getInsertionRenderData(data, me.labelableInsertions); return data; }, beforeLabelableRender: function() { var me = this; if (me.ownerLayout) { me.addCls(Ext.baseCSSPrefix + me.ownerLayout.type + '-form-item'); } }, onLabelableRender: function() { var me = this, margins, side, style = {}; if (me.extraMargins) { margins = me.el.getMargin(); for (side in margins) { if (margins.hasOwnProperty(side)) { style['margin-' + side] = (margins[side] + me.extraMargins[side]) + 'px'; } } me.el.setStyle(style); } }, hasVisibleLabel: function(){ if (this.hideLabel) { return false; } return !(this.hideEmptyLabel && !this.getFieldLabel()); }, getBodyColspan: function() { var me = this, result; if (me.msgTarget === 'side' && (!me.autoFitErrors || me.hasActiveError())) { result = 1; } else { result = 2; } if (me.labelAlign !== 'top' && !me.hasVisibleLabel()) { result++; } return result; }, getLabelCls: function() { var labelCls = this.labelCls, labelClsExtra = this.labelClsExtra; if (this.labelAlign === 'top') { labelCls += '-top'; } return labelClsExtra ? labelCls + ' ' + labelClsExtra : labelCls; }, getLabelCellStyle: function() { var me = this, hideLabelCell = me.hideLabel || (!me.fieldLabel && me.hideEmptyLabel); return hideLabelCell ? 'display:none;' : ''; }, getErrorMsgCls: function() { var me = this, hideLabelCell = (me.hideLabel || (!me.fieldLabel && me.hideEmptyLabel)); return me.errorMsgCls + (!hideLabelCell && me.labelAlign === 'top' ? ' ' + Ext.baseCSSPrefix + 'lbl-top-err-icon' : ''); }, getLabelCellAttrs: function() { var me = this, labelAlign = me.labelAlign, result = ''; if (labelAlign !== 'top') { result = 'valign="top" halign="' + labelAlign + '" width="' + (me.labelWidth + me.labelPad) + '"'; } return result + ' class="' + Ext.baseCSSPrefix + 'field-label-cell"'; }, getLabelStyle: function(){ var me = this, labelPad = me.labelPad, labelStyle = ''; if (me.labelAlign !== 'top') { if (me.labelWidth) { labelStyle = 'width:' + me.labelWidth + 'px;'; } labelStyle += 'margin-right:' + labelPad + 'px;'; } return labelStyle + (me.labelStyle || ''); }, //混入该类的组件必须实现此方法,提供表单字段的input元素 getSubTplMarkup: function() { return ''; }, getInputId: function() { return ''; }, getActiveError : function() { return this.activeError || ''; }, hasActiveError: function() { return !!this.getActiveError(); }, setActiveError: function(msg) { this.setActiveErrors(msg); }, getActiveErrors: function() { return this.activeErrors || []; }, setActiveErrors: function(errors) { errors = Ext.Array.from(errors); this.activeError = errors[0]; this.activeErrors = errors; this.activeError = this.getTpl('activeErrorsTpl').apply({errors: errors}); this.renderActiveError(); }, unsetActiveError: function() { delete this.activeError; delete this.activeErrors; this.renderActiveError(); }, renderActiveError: function() { var me = this, activeError = me.getActiveError(), hasError = !!activeError; if (activeError !== me.lastActiveError) { me.fireEvent('errorchange', me, activeError); me.lastActiveError = activeError; } if (me.rendered && !me.isDestroyed && !me.preventMark) { me.el[hasError ? 'addCls' : 'removeCls'](me.invalidCls); me.getActionEl().dom.setAttribute('aria-invalid', hasError); if (me.errorEl) { me.errorEl.dom.innerHTML = activeError; } } }, setFieldDefaults: function(defaults) { var me = this, val, key; for (key in defaults) { if (defaults.hasOwnProperty(key)) { val = defaults[key]; if (!me.hasOwnProperty(key)) { me[key] = val; } } } } });
Leave a Reply to Alex Cancel reply