简介: 文章首先介绍了 Ext JS 的基本概念以及扩展 Ext JS 的一般方法,然后通过三个实例详细描述了如何从已有的 Ext JS 控件出发,综合多个控件的功能,开发出满足实际需要的新控件。 本文主要面向 JavaScript 开发人员,假定读者对 Ext JS 和 Web 开发有基本的了解,您也可以参见参考资料以了解更多 Ext JS 和 JavaScript 相关的知识。 Ext 概述 Ext JS 是一种强大的 JavaScript 库,可以用来开发 RIA (Rich Internet Applications),也即富客户端的 Ajax 应用,是一个与后台技术无关的前端 Ajax 框架。Ext JS 最开始基于 YUI (Yahoo! User Interface Library) 技术,由开发人员 Jack Slocum 开发,通过参考 Java Swing 等机制来组织可视化组件,无论从 UI 界面上 CSS 样式的应用,到数据解析上的异常处理,都可算是一个非常优秀的 Web 开发框架。而且在 www.extjs.com的网站上提供了免费的 Ext 发布包,其中源代码、API 文档和示例资源非常丰富,特别适合初学者上手实践。 对于大多数程序员来说,我们没有任何美术功底,公司的很多项目也没有配备美工,要想开发吸引人眼球的用户界面,一直以来不是一件容易的事情。 但是 Ext 的出现使得开发美观的界面变得容易,Ext 提供了表格、树、布局、按钮等很多外观绚丽、功能强大的控件,为我们的日常开发工作节约了大量的时间和精力。更重要的是这个框架是完全面向对象且可扩展 的,通过对现有的库的功能进行修改或加入新的功能,来实现 Ext 框架中没有的功能。 扩展 Ext 组件 扩展 (extension) 在 Ext 中就是指衍生的子类。假设我们已经有一个附有一些方法的基类,现在欲加入新方法。我们可以利用框架的继承特性和 JavaScript 创建新类的语言特性组合新的一个类。 Ext 提供了这样的一个实用函数 Ext.extend 在 Ext 框架中实现类继承的机制。这赋予了扩展任何 JavaScript 基类的能力,而无须对类自身进行代码的修改,扩展 Ext 组件这是个较理想的方法。 要从一个现有的类创建出一个新类,首先要通过一个函数声明新类的构造器,然后调用新类属性所共享的扩展方法。这些共享的属性通常是方法,但是如果要在实例之间共享数据,应该也一同声明。 JavaScript 并没有提供一个自动的调用父类构造器的机制,所以必须通过属性 superclass 在构造器中显式调用父类。第一个参数总是 this,以保证构造器工作在调用函数的作用域。 清单 1. 扩展 Ext 组件的基本方法 MyNewClass = function(arg1, arg2, etc) {
// 显式调用父类的构造函数
MyNewClass.superclass.constructor.call(this, arg1, arg2, etc);
};
Ext.extend(MyNewClass, SomeBaseClass, {
myNewFn1: function() {
// etc.
},
myNewFn2: function() {
// etc.
}
}); | 使用时,我们需要实例化对象: 清单 2. 实例化新的组件对象 var myObject = new MyNewClass (arg1, arg2, etc); | 掌握了扩展 Ext 组件的基本方法之后,我们就可以随意构造满足特定需求的组件。然而 Ext 里已有的组件和示例永远是我们取之不尽,用之不竭的创造源泉。本文以三个 Ext 组件为基础,“嫁接”了其他组件的功能,形成三个新的组件,实现了现有 Ext 组件没有的功能。本文的目的,旨在抛砖引玉,希望能给初学 Ext 的同仁们一点启发和参考,开发出更多、功能更强大的组件。 移 Property Grid 之花接 Editor Grid 之木 首先,介绍一下我们的场景和实际需求。某大学要建设一个教职工科研基金的管理系统,该系统可供基金设置人员设置基金申请条件、发放步骤等,申 请人员填报申请人信息、申领基金等。这里以构建一个基金申请条件的组件为例。条件制定人员在制定申请条件时,可以随意添加、删除申请条件;对于某些申请条 件,比如院系、性别等要求系统能提供预先定义好的选项供条件制定人员选择,而对于比如特长、年龄等内容不明确的或者选项过多无法列举的情况,则直接提供输 入框供条件制定人员输入。 为了用 Ext 构建这样的组件,我们首先想到的是选用 Editor Grid 组件或者 Property Grid 组件。Editor Grid(可编辑表格控件)扩展自 GridPanel,提供对于选中列的单元格编辑。可编辑的列是通过在表示表格的列信息的类 Ext.grid.ColumnModel 中添加 editor 来实现的。但是这个 editor 是对整个列有效的,就是说每一行在该列的位置的数据的编辑器是一样的。 Property Grid( 属性表格 ) 扩展自 EditorGridPanel,所以可以直接编辑右边属性值部分的内容。但是,只是右边的,即使你单击左边的单元格,编辑器也只会出现在右边。实际上, 我们可以用散列表来形容 PropertyGrid,左边可以看作 key,右边的是 value。key 是由我们指定好的,用户只需要修改对应的 value 即可。 Property Grid 默认的编辑器包括 TextField、DateField、NumberField 和 ComboBox,也就只能处理数字、字符串的输入和日期的选择,布尔值的选择等一般的情况。当我们想对编辑器进行更详细的配置时,就需要用到 PropertyGrid 的 customEditors,为指定 id 的那行数据设置对应的编辑器。customEditors 和 source 的设置基本一样,只需要将两者的属性名称对应起来,并且为 customEditors 里的所有属性指定一个 editor。 Property Grid 虽然能够给不同的单元格定制不同的编辑器,但是一方面这种表格只有两列,第一列还不可编辑,而且表格的内容(source)需要事先确定;另一方面定义 customEditors 的时候必须知道表格的内容(source),而且必须将两者的属性名称对应起来。 Editor Grid 其实已经大部分满足了我们的需求,只是不能对每个单元格定制编辑器,只能指定列编辑器。 经过上面的分析,单纯的使用任何一个控件,都难以达到我们的目的。同时我们发现问题主要出在 Editor Grid 的列模式(ColumnModel)上,Property Grid 就是扩展自 Editor Grid,通过对其 ColumnModel 的扩展来支持单元格的编辑器。所以我们尝试把 Editor Grid 的 ColumnModel 扩展一下,使得新的 ColumnModel 支持 customEditors,这样我们就获得了对编辑器的完全控制权,可以根据表格的内容动态的改变单元格的编辑器了。清单 3 是我们为满足上述需求而扩展的新类 MyColumnModel 的部分代码,清单 4 是使用 MyColumnModel 构造了一个 Editor Grid 作为基金申请条件组件。 清单 3. 定义新类 MyColumnModel Ext.ns('Ext.ux.grid');
// 新类 MyColumnModel 的构造函数
Ext.ux.grid.MyColumnModel = function(grid, store, column){
this.grid = grid;
this.store = store;
var gender = [
['0100','男'],
['0101','女']
];
var department = [
['0200','文学院'],
// 省略部分代码
['0207','医学部']
];
var title = [
['0300','助教'],
// 省略部分代码
['0303','教授']
];
var genderCombo = new Ext.form.ComboBox({
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : gender
}),
emptyText : '请输入',
mode : 'local',
triggerAction : 'all',
valueField : 'value',
displayField : 'text',
readOnly : true
});
var departmentCombo = new Ext.form.ComboBox({
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : department
}),
// 与上面定义 genderCombo 类似,故省略部分代码
…
});
var titleCombo = new Ext.form.ComboBox({
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : title
}),
// 与上面定义 genderCombo 类似,故省略部分代码
…
});
// 当选择“性别”、“院系”、“职称”时,提供相应的下拉列表作为单元格编辑器,
供用户选择;当选择“年龄”、“论文数量”时,提供数字文本框供用户输入
this.customEditors = {
'GENDER' : new Ext.grid.GridEditor(genderCombo),
'DEPARTMENT' : new Ext.grid.GridEditor(departmentCombo),
'TITLE' : new Ext.grid.GridEditor(titleCombo),
'AGE': new Ext.grid.GridEditor(new Ext.form.NumberField({selectOnFocus:true,
style:'text-align:left;'})),
'PAPER' : new Ext.grid.GridEditor(new Ext.form.NumberField({selectOnFocus:true,
style:'text-align:left;'}))
};
Ext.ux.grid.MyColumnModel.superclass.constructor.call(this,column);
};
Ext.extend(Ext.ux.grid.MyColumnModel, Ext.grid.ColumnModel, {
// 通过覆盖父类中的方法 getCellEditor, 实现根据表达式中条件列的不同取值,
为表达式的值所在单元格返回不同的编辑器
getCellEditor : function(colIndex, rowIndex){
var p = this.store.getAt(rowIndex);
n = p.data.attrName;// 对应表达式的条件列的取值
if(colIndex==4)// 表达式的值 propertyValue 所在的列
{
if(this.customEditors[n]){
return this.customEditors[n];
}else{
// 如果没有定义特定的单元格编辑器,则返回普通的文本框编辑器
var ed = new Ext.grid.GridEditor(new Ext.form.TextField({
selectOnFocus:true
})
);
return ed;
}
}
else
return this.config[colIndex].editor;
}
}); | 清单 4. 基金申请条件组件 Ext.onReady(function(){
var comboData1 = [
['AGE' , '年龄'],
// 省略部分代码
['DEPARTMENT' , '院系']
];
var comboData2 = [
['>','大于'],
// 省略部分代码
['!=','不等于']
];
var combo1 = new Ext.form.ComboBox({
id : 'attrCombo',
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : comboData1
}),
emptyText : '请选择',
mode : 'local',
triggerAction : 'all',
valueField : 'value',
displayField : 'text',
readOnly : true
});
var combo2 = new Ext.form.ComboBox({
id : 'operatorCombo',
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : comboData2
}),
// 与上面定义 combo1 类似,故省略部分代码
…
});
var conditiondata = [];
var gStore = new Ext.data.SimpleStore({
fields: [
{name: 'fundConditionId'},
{name: 'attrName'},
{name: 'operator'},
{name: 'propertyValue'}
],
data : conditiondata
});
var sm = new Ext.grid.CheckboxSelectionModel({handleMouseDown: Ext.emptyFn});
var column = [
sm,
{
header : '条件 id',
dataIndex : 'fundConditionId',
hidden : true
},{
header : '属性名称',
dataIndex : 'attrName',
editor : new Ext.grid.GridEditor(combo1),
// attributeRenderer 方法是用来格式化输出的函数,这里从略。
renderer : attributeRenderer.createDelegate(this, ["properties"] , 0)
},{
header : '操作符',
dataIndex : 'operator',
editor : new Ext.grid.GridEditor(combo2),
renderer : attributeRenderer.createDelegate(this, ["operators"] , 0)
},{
header : '属性值',
dataIndex : 'propertyValue',
editor : new Ext.grid.GridEditor(new Ext.form.TextField({selectOnFocus:true})),
renderer : attributeRenderer.createDelegate(this, ["values"] , 0)
}
];
var fundConditionGrid = new Ext.grid.EditorGridPanel({
name : 'fundCondition',
id : 'fundCondition',
store : gStore,
cm : new Ext.ux.grid.MyColumnModel(this,gStore,column),
sm : sm,
tbar : new Ext.Toolbar(['-',{
text : '添加条件',
//_onAddCondition 方法是按钮“添加条件”的响应函数,实现在列表中增加一个条件的功能,这里从略。
handler : _onAddCondition.createDelegate(this)
}, '-', {
text : '删除条件',
//_onRemoveCondition 方法是按钮“删除条件”的响应函数,实现在列表中删除一个条件的功能,这里从略。
handler : _onRemoveCondition.createDelegate(this)
}, '-']),
frame:true,
collapsible: true,
animCollapse: false,
title : '助研基金申请条件',
width: 350,
height : 300,
iconCls: 'icon-grid',
clicksToEdit : 1,
renderTo: 'example1'
});
}); | 当属性名称选择性别、职称或者院系时,属性值分别对应不同的下拉列表供用户选择,当属性名称选择年龄或者论文数量时,属性值则对应数字文本框供用户输入。如图 1- 图 5 所示。 图 1. 编辑助研基金申请条件 1 图 2. 编辑助研基金申请条件 2 图 3. 编辑助研基金申请条件 3 图 4. 编辑助研基金申请条件 4 图 5. 编辑助研基金申请条件 5 这里表示计算机学院年龄不大于 35 岁讲师以上(含)职称的女教师,如果发表的论文数量多于 10 篇的,有资格申请该助研基金。可以看出该基金体现了对优秀青年女教师的科研支持。此部分的代码请参考示例代码中的 Example1.js。 用 ComboBox 实现在光标处插入文本 在上述的教职工科研基金管理系统中,如果满足基金申请条件的教职工人数很多,我们就需要根据某种评分机制对申请人进行评分,然后按分数从高到 低择优选择。这里以构建一个制定申请人得分计算公式的组件为例。制定计算公式时,可以引用上面系统中已经定义好的申请条件作为计分要素,然后用加减乘除等 运算符将计分要素和比例系数连接起来构成得分计算公式。 为实现上面的功能,我们考虑在文本框的右边放一个按钮,点击该按钮,列出所有的计分要素,选择一个计分要素后,在文本框中光标所在位置插入该 计分要素。列出所有的计分要素,并选择其一,最好的实现方式是 ComboBox,但是使用 ComboBox,选择的值会覆盖文本框内所有的文字,无法实现在光标处插入文本的功能。 所以我们决定扩展 ComboBox,使得新的 ComboBox 支持在光标处插入文本。清单 5 是我们为满足上述需求而扩展的新类 valueCombo 的部分代码,清单 6 是使用 valueCombo 作为申请人得分计算公式组件。效果如图 6 和图 7 所示。 清单 5. 定义新类 valueCombo Ext.ns('Ext.ux.form');
Ext.ux.form.valueCombo = Ext.extend(Ext.form.ComboBox, {
initComponent : function(){
Ext.ux.form.valueCombo.superclass.initComponent.call(this);
},
setValue : function(v){
var text = v;
// 直接显示选项的值,不做格式化转换,覆盖原来 ComboBox 的格式化显示的功能
/* if(this.valueField){
var r = this.findRecord(this.valueField, v);
if(r){
text = r.data[this.valueField];
}else if(Ext.isDefined(this.valueNotFoundText)){
text = this.valueNotFoundText;
}
} */
this.lastSelectionText = text;
if(this.hiddenField){
this.hiddenField.value = v;
}
Ext.ux.form.ComboBox.superclass.setValue.call(this, text);
this.value = v;
return this;
},
// private
onSelect : function(record, index){
if(this.fireEvent('beforeselect', this, record, index) !== false){
var str = record.data[this.valueField || this.displayField];
// 实现在光标处插入文本的功能
var tc = this.getRawValue();
var tclen = this.getRawValue().length;
this.focus();
// 以下代码只对 Firefox 生效
if(typeof document.selection != "undefined")
{
document.selection.createRange().text = str;
}
else
{
this.setValue(tc.substr(0,this.el.dom.selectionStart) + str
+tc.substring(this.el.dom.selectionStart,tclen));
}
this.collapse();
this.fireEvent('select', this, record, index);
}
},
onLoad : function(){
if(!this.hasFocus){
return;
}
if(this.store.getCount() > 0){
this.expand();
this.restrictHeight();
if(this.lastQuery == this.allQuery){
//if(this.editable){
// this.el.dom.select(); 为了保持光标位置,注释掉此段代码
//}
if(!this.selectByValue(this.value, true)){
this.select(0, true);
}
}else{
this.selectNext();
if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE
&& this.lastKey != Ext.EventObject.DELETE){
this.taTask.delay(this.typeAheadDelay);
}
}
}else{
this.onEmptyResults();
}
//this.el.focus();
},
initQuery : function(){
// 屏蔽掉下拉列表进行匹配查询的功能
// this.doQuery(this.getRawValue());
}
});
| 清单 6. 申请人得分计算公式组件 var comboData = [
['AGE' , '年龄'],
// 省略部分代码
['DEPARTMENT' , '院系']
];
var scoreExpression = new Ext.ux.form.valueCombo({
width: 250,
listWidth : 120,
fieldLabel: ‘得分计算公式’ ,
mode : 'local',
valueField : 'value',
displayField : 'text',
triggerAction : 'all',
store : new Ext.data.SimpleStore({
fields : ['value','text'],
data : comboData
}),
triggerConfig : {tag: "img",src: "../images/score-element.jpg",
cls: "x-textfield-button-trigger"}
}); | 图 6. 插入计分要素编辑评分计算公式 图 7. 评分计算公式 该计算公式体现了对青年教职工和女性教职工的鼓励和扶助。此部分的代码请参考示例代码中的 Example2.js。 实现带 ComboBox 的 TwinTriggerField 在上述的教职工科研基金管理系统中,需要一个查询员工详细信息的控件。只要输入员工号或者从下拉列表中选择一个员工号,就能自动载入该员工所 有的信息。同时希望能根据搜索条件查询符合条件的员工,从中选择某个员工,查看他的详细信息。依据这个需求,我们要构建一个查询员工的组件,当在文本框中 输入员工号前几位能自动列出所有相关员工号,或者直接从下拉框中选择一个员工号,随后自动载入员工信息;当点击文本框右边的搜索按钮,打开新的窗口,在新 窗口中能够根据员工职位、院系、出生日期所在范围等进行搜索,选中员工之后,也会自动载入该员工的信息。如果能将 ComboBox 和 TwinTriggerField 的功能结合起来,将是实现此需求的最直接、最便利的方法。 清单 7 是我们为满足上述需求而扩展的新类 ComboSearchField 的部分代码,清单 8 是使用 ComboSearchField 构造了一个 Form 作为教职工信息查询控件。此部分的完整代码请参考 Example3.js。效果如图 8 和图 9 所示。 清单 7. 定义新类 ComboSearchField Ext.ns('Ext.ux.form');
Ext.ux.form.ComboSearchField = Ext.extend(Ext.form.ComboBox, {
initComponent : function(){
Ext.ux.form.ComboSearchField.superclass.initComponent.call(this);
this.triggerConfig = {
// 使用 Twin Trigger 的样式
tag:'span', cls:'x-form-twin-triggers', cn:[
{tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " +
this.triggerClass},// 使用默认 ComboBox 的样式
{tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " +
this.trigger2Class}// 自定义 Trigger2 的样式
]};
},
getTrigger : function(index){
return this.triggers[index];
},
initTrigger : function(){
var ts = this.trigger.select('.x-form-trigger', true);
this.wrap.setStyle('overflow', 'hidden');
var triggerField = this;
ts.each(function(t, all, index){
t.hide = function(){
var w = triggerField.wrap.getWidth();
this.dom.style.display = 'none';
triggerField.el.setWidth(w-triggerField.trigger.getWidth());
};
t.show = function(){
var w = triggerField.wrap.getWidth();
this.dom.style.display = '';
triggerField.el.setWidth(w-triggerField.trigger.getWidth());
};
var triggerIndex = 'Trigger'+(index+1);
if(this['hide'+triggerIndex]){
t.dom.style.display = 'none';
}
//this.mon(t, 'click', this['on'+triggerIndex+'Click'], this,
{preventDefault:true});
// 定义第一个 trigger 的触发事件
if(index==0)
t.on("click", this['onTriggerClick'], this, {preventDefault:true});
// 定义第二个 trigger 的触发事件
if(index==1)
t.on("click", this['onTrigger2Click'], this, {preventDefault:true});
t.addClassOnOver('x-form-trigger-over');
t.addClassOnClick('x-form-trigger-click');
}, this);
this.triggers = ts.elements;
},
validationEvent:false,
validateOnBlur:false,
trigger2Class:'x-form-search-trigger',
width:180,
hasSearch : false,
paramName : 'query',
onTrigger2Click : Ext.emptyFn
}); | 清单 8. 教职工信息查询控件 Ext.onReady(function(){
var employeeData = [
['20001234'],
// 省略部分代码
['20091546']
];
var searchField = new Ext.ux.form.ComboSearchField({
id : "employeeId",
fieldLabel: '员工编码'+'<font color="red">*</font>',
store : new Ext.data.SimpleStore({
fields : ['value'],
data : employeeData
}),
valueField : 'value',
displayField : 'value',
typeAhead: false,
allowBlank: false,
mode : 'local',
triggerAction : 'all',
// 打开客户查询窗口,代码从略
onTrigger2Click : _searchCustomer.createDelegate(this),
listeners : {
// 根据选中客户编码加载客户信息,代码从略
select : _select_customer_id.createDelegate(this)
}
});
// 以下代码仅在 Firefox 中运行有效
// 初始化生成表格
var employeeForm = new Ext.FormPanel({
id : "employeeForm",
frame: true,
title: "教职工信息查询",
autoScroll : true,
items : [{
layout : 'column',
border : false,
autoHeight : true,
defaults :{
layout: 'form',
border: false,
width : 100,
bodyStyle: 'padding:4px'
},
items: [{
columnWidth: 0.33,
name : "form1",
id : "form1",
defaultType: 'textfield',
defaults : {
width: 200
},
items : [searchField,
{
fieldLabel: '职称',
name: 'title',
id: 'title',
disabled : true
},{
fieldLabel: '出生日期',
name: 'birthday',
id: 'birthday',
disabled : true
},{
fieldLabel: '联系电话',
name: 'tel',
id: 'tel',
disabled : true
}]},{
columnWidth: 0.33,
// 从略
},{
columnWidth: 0.33,
// 从略
}]
}],
renderTo: 'example3'
});
}); | 图 8. TwinTrigger 之下拉列表 图 9. 载入员工信息 总结 本文在介绍了 Ext 的基本概念以及扩展 Ext 的一般方法后,以三个应用场景为例,详细描述了如何从已有的 Ext 控件出发,借鉴其他控件的功能,开发出满足实际需要的新控件。对于初学者来说,这种“移花接木”式的开发方式,不仅能使开发者深入了解每个控件背后的实现 方式,而且能迅速助其实现新的功能、新的需求。可以说,它是一种值得推荐的创新方式。 参考资料 - Ext 官方演示示例:提供了众多示例和源代码供您参考。
- Ext API 文档:是您进行 Ext 开发的指导手册。
- 用 Ext JS 构建 Ajax 应用程序:这篇文章可以帮助您粗略地了解 Ext JS 框架的主要特性。
- Ext Manual: Component: Extension or Plugin zh-CN:帮助您了解 Ext 扩展相关的知识。
- 《深入浅出 Ext JS》:本书可以帮助您详细了解 Ext 基本组件的属性和功能。
- developerWorks 技术活动和网络广播:随时关注 developerWorks 技术活动和网络广播。
- developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
- developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
- developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。
|