Ext JS是一种强大的JavaScript库,可以用来开发RIA(Rich Internet Applications),也即富客户端的Ajax应用,是一个与后台技术无关的前端Ajax框架。
Ext JS最开始基于YUI(Yahoo!UserInterfaceLibrary)技术,由开发人员JackSlocum开发,通过参考JavaSwing等机制来组织可视化组件,无论从UI界面上CSS样式的应用,到数据解析上的异常处理,都可算是一个非常优秀的Web开发框架。
对于大多数程序员来说,我们没有任何美术功底,公司的很多项目也没有配备美工,要想开发吸引人眼球的用户界面,一直以来不是一件容易的事情。但是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.实例化新的组件对象
- varmyObject=newMyNewClass(arg1,arg2,etc);
掌握了扩展Ext组件的基本方法之后,我们就可以随意构造满足特定需求的组件。然而Ext里已有的组件和示例永远是我们取之不尽,用之不竭的创造源泉。本文以三个Ext组件为基础,“嫁接”了其他组件的功能,形成三个新的组件,实现了现有Ext组件没有的功能。本文的目的,旨在抛砖引玉,希望能给初学Ext的同仁们一点启发和参考,开发出更多、功能更强大的组件。
#p#
移Property Grid之花接EditorGrid之木
首先,介绍一下我们的场景和实际需求。某大学要建设一个教职工科研基金的管理系统,该系统可供基金设置人员设置基金申请条件、发放步骤等,申请人员填报申请人信息、申领基金等。这里以构建一个基金申请条件的组件为例。条件制定人员在制定申请条件时,可以随意添加、删除申请条件;对于某些申请条件,比如院系、性别等要求系统能提供预先定义好的选项供条件制定人员选择,而对于比如特长、年龄等内容不明确的或者选项过多无法列举的情况,则直接提供输入框供条件制定人员输入。
为了用Ext构建这样的组件,我们首先想到的是选用EditorGrid组件或者Property Grid组件。EditorGrid(可编辑表格控件)扩展自GridPanel,提供对于选中列的单元格编辑。可编辑的列是通过在表示表格的列信息的类Ext.grid.ColumnModel中添加editor来实现的。但是这个editor是对整个列有效的,就是说每一行在该列的位置的数据的编辑器是一样的。
Property Grid(属性表格)扩展自EditorGridPanel,所以可以直接编辑右边属性值部分的内容。但是,只是右边的,即使你单击左边的单元格,编辑器也只会出现在右边。实际上,我们可以用散列表来形容Property Grid,左边可以看作key,右边的是value。key是由我们指定好的,用户只需要修改对应的value即可。
Property Grid默认的编辑器包括TextField、DateField、NumberField和ComboBox,也就只能处理数字、字符串的输入和日期的选择,布尔值的选择等一般的情况。当我们想对编辑器进行更详细的配置时,就需要用到Property Grid的customEditors,为指定id的那行数据设置对应的编辑器。customEditors和source的设置基本一样,只需要将两者的属性名称对应起来,并且为customEditors里的所有属性指定一个editor。
Property Grid虽然能够给不同的单元格定制不同的编辑器,但是一方面这种表格只有两列,***列还不可编辑,而且表格的内容(source)需要事先确定;另一方面定义customEditors的时候必须知道表格的内容(source),而且必须将两者的属性名称对应起来。EditorGrid其实已经大部分满足了我们的需求,只是不能对每个单元格定制编辑器,只能指定列编辑器。
经过上面的分析,单纯的使用任何一个控件,都难以达到我们的目的。同时我们发现问题主要出在EditorGrid的列模式(ColumnModel)上,Property Grid就是扩展自EditorGrid,通过对其ColumnModel的扩展来支持单元格的编辑器。所以我们尝试把EditorGrid的ColumnModel扩展一下,使得新的ColumnModel支持customEditors,这样我们就获得了对编辑器的完全控制权,可以根据表格的内容动态的改变单元格的编辑器了。清单3是我们为满足上述需求而扩展的新类MyColumnModel的部分代码,清单4是使用MyColumnModel构造了一个EditorGrid作为基金申请条件组件。
- 清单3.定义新类MyColumnModel
- Ext.ns('Ext.ux.grid');
- //新类MyColumnModel的构造函数
- Ext.ux.grid.MyColumnModel=function(grid,store,column){
- this.grid=grid;
- this.store=store;
- vargender=[
- ['0100','男'],
- ['0101','女']
- ];
- vardepartment=[
- ['0200','文学院'],
- //省略部分代码
- ['0207','医学部']
- ];
- vartitle=[
- ['0300','助教'],
- //省略部分代码
- ['0303','教授']
- ];
- vargenderCombo=newExt.form.ComboBox({
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:gender
- }),
- emptyText:'请输入',
- mode:'local',
- triggerAction:'all',
- valueField:'value',
- displayField:'text',
- readOnly:true
- });
- vardepartmentCombo=newExt.form.ComboBox({
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:department
- }),
- //与上面定义genderCombo类似,故省略部分代码
- …
- });
- vartitleCombo=newExt.form.ComboBox({
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:title
- }),
- //与上面定义genderCombo类似,故省略部分代码
- …
- });
- //当选择“性别”、“院系”、“职称”时,提供相应的下拉列表作为单元格编辑器,
- 供用户选择;当选择“年龄”、“论文数量”时,提供数字文本框供用户输入
- this.customEditors={
- 'GENDER':newExt.grid.GridEditor(genderCombo),
- 'DEPARTMENT':newExt.grid.GridEditor(departmentCombo),
- 'TITLE':newExt.grid.GridEditor(titleCombo),
- 'AGE':newExt.grid.GridEditor(newExt.form.NumberField({selectOnFocus:true,
- style:'text-align:left;'})),
- 'PAPER':newExt.grid.GridEditor(newExt.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){
- varp=this.store.getAt(rowIndex);
- n=p.data.attrName;//对应表达式的条件列的取值
- if(colIndex==4)//表达式的值propertyValue所在的列
- {
- if(this.customEditors[n]){
- returnthis.customEditors[n];
- }else{
- //如果没有定义特定的单元格编辑器,则返回普通的文本框编辑器
- vared=newExt.grid.GridEditor(newExt.form.TextField({
- selectOnFocus:true
- })
- );
- returned;
- }
- }
- else
- returnthis.config[colIndex].editor;
- }
- });
#p#
- 清单4.基金申请条件组件
- Ext.onReady(function(){
- varcomboData1=[
- ['AGE','年龄'],
- //省略部分代码
- ['DEPARTMENT','院系']
- ];
- varcomboData2=[
- ['>','大于'],
- //省略部分代码
- ['!=','不等于']
- ];
- varcombo1=newExt.form.ComboBox({
- id:'attrCombo',
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:comboData1
- }),
- emptyText:'请选择',
- mode:'local',
- triggerAction:'all',
- valueField:'value',
- displayField:'text',
- readOnly:true
- });
- varcombo2=newExt.form.ComboBox({
- id:'operatorCombo',
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:comboData2
- }),
- //与上面定义combo1类似,故省略部分代码
- …
- });
- varconditiondata=[];
- vargStore=newExt.data.SimpleStore({
- fields:[
- {name:'fundConditionId'},
- {name:'attrName'},
- {name:'operator'},
- {name:'propertyValue'}
- ],
- data:conditiondata
- });
- varsm=newExt.grid.CheckboxSelectionModel({handleMouseDown:Ext.emptyFn});
- varcolumn=[
- sm,
- {
- header:'条件id',
- dataIndex:'fundConditionId',
- hidden:true
- },{
- header:'属性名称',
- dataIndex:'attrName',
- editor:newExt.grid.GridEditor(combo1),
- //attributeRenderer方法是用来格式化输出的函数,这里从略。
- renderer:attributeRenderer.createDelegate(this,["properties"],0)
- },{
- header:'操作符',
- dataIndex:'operator',
- editor:newExt.grid.GridEditor(combo2),
- renderer:attributeRenderer.createDelegate(this,["operators"],0)
- },{
- header:'属性值',
- dataIndex:'propertyValue',
- editor:newExt.grid.GridEditor(newExt.form.TextField({selectOnFocus:true})),
- renderer:attributeRenderer.createDelegate(this,["values"],0)
- }
- ];
- varfundConditionGrid=newExt.grid.EditorGridPanel({
- name:'fundCondition',
- id:'fundCondition',
- store:gStore,
- cm:newExt.ux.grid.MyColumnModel(this,gStore,column),
- sm:sm,
- tbar:newExt.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所示。
这里表示计算机学院年龄不大于35岁讲师以上(含)职称的女教师,如果发表的论文数量多于10篇的,有资格申请该助研基金。可以看出该基金体现了对优秀青年女教师的科研支持。此部分的代码请参考示例代码中的Example1.js。
#p#
用ComboBox实现在光标处插入文本
在上述的教职工科研基金管理系统中,如果满足基金申请条件的教职工人数很多,我们就需要根据某种评分机制对申请人进行评分,然后按分数从高到低择优选择。这里以构建一个制定申请人得分计算公式的组件为例。制定计算公式时,可以引用上面系统中已经定义好的申请条件作为计分要素,然后用加减乘除等运算符将计分要素和比例系数连接起来构成得分计算公式。
为实现上面的功能,我们考虑在文本框的右边放一个按钮,点击该按钮,列出所有的计分要素,选择一个计分要素后,在文本框中光标所在位置插入该计分要素。列出所有的计分要素,并选择其一,***的实现方式是ComboBox,但是使用ComboBox,选择的值会覆盖文本框内所有的文字,无法实现在光标处插入文本的功能。
所以我们决定扩展ComboBox,使得新的ComboBox支持在光标处插入文本。清单5是我们为满足上述需求而扩展的新类valueCombo的部分代码,清单6是使用valueCombo作为申请人得分计算公式组件。效果如图6和图7所示。
- 清单5.定义新类valueCombo
- Ext.ns('Ext.ux.form');
- ExtExt.ux.form.valueCombo=Ext.extend(Ext.form.ComboBox,{
- initComponent:function(){
- Ext.ux.form.valueCombo.superclass.initComponent.call(this);
- },
- setValue:function(v){
- vvartext=v;
- //直接显示选项的值,不做格式化转换,覆盖原来ComboBox的格式化显示的功能
- /*if(this.valueField){
- varr=this.findRecord(this.valueField,v);
- if(r){
- text=r.data[this.valueField];
- }elseif(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;
- returnthis;
- },
- //private
- onSelect:function(record,index){
- if(this.fireEvent('beforeselect',this,record,index)!==false){
- varstr=record.data[this.valueField||this.displayField];
- //实现在光标处插入文本的功能
- vartc=this.getRawValue();
- vartclen=this.getRawValue().length;
- this.focus();
- //以下代码只对Firefox生效
- if(typeofdocument.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.申请人得分计算公式组件
- varcomboData=[
- ['AGE','年龄'],
- //省略部分代码
- ['DEPARTMENT','院系']
- ];
- varscoreExpression=newExt.ux.form.valueCombo({
- width:250,
- listWidth:120,
- fieldLabel:‘得分计算公式’,
- mode:'local',
- valueField:'value',
- displayField:'text',
- triggerAction:'all',
- store:newExt.data.SimpleStore({
- fields:['value','text'],
- data:comboData
- }),
- triggerConfig:{tag:"img",src:"../images/score-element.jpg",
- cls:"x-textfield-button-trigger"}
- });
该计算公式体现了对青年教职工和女性教职工的鼓励和扶助。此部分的代码请参考示例代码中的Example2.js。
#p#
实现带ComboBox的TwinTriggerField
在上述的教职工科研基金管理系统中,需要一个查询员工详细信息的控件。只要输入员工号或者从下拉列表中选择一个员工号,就能自动载入该员工所有的信息。同时希望能根据搜索条件查询符合条件的员工,从中选择某个员工,查看他的详细信息。
依据这个需求,我们要构建一个查询员工的组件,当在文本框中输入员工号前几位能自动列出所有相关员工号,或者直接从下拉框中选择一个员工号,随后自动载入员工信息;当点击文本框右边的搜索按钮,打开新的窗口,在新窗口中能够根据员工职位、院系、出生日期所在范围等进行搜索,选中员工之后,也会自动载入该员工的信息。如果能将ComboBox和TwinTriggerField的功能结合起来,将是实现此需求的最直接、最便利的方法。
清单7是我们为满足上述需求而扩展的新类ComboSearchField的部分代码,清单8是使用ComboSearchField构造了一个Form作为教职工信息查询控件。此部分的完整代码请参考Example3.js。效果如图8和图9所示。
- 清单7.定义新类ComboSearchField
- Ext.ns('Ext.ux.form');
- ExtExt.ux.form.ComboSearchField=Ext.extend(Ext.form.ComboBox,{
- initComponent:function(){
- Ext.ux.form.ComboSearchField.superclass.initComponent.call(this);
- this.triggerConfig={
- //使用TwinTrigger的样式
- 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){
- returnthis.triggers[index];
- },
- initTrigger:function(){
- varts=this.trigger.select('.x-form-trigger',true);
- this.wrap.setStyle('overflow','hidden');
- vartriggerField=this;
- ts.each(function(t,all,index){
- t.hide=function(){
- varw=triggerField.wrap.getWidth();
- this.dom.style.display='none';
- triggerField.el.setWidth(w-triggerField.trigger.getWidth());
- };
- t.show=function(){
- varw=triggerField.wrap.getWidth();
- this.dom.style.display='';
- triggerField.el.setWidth(w-triggerField.trigger.getWidth());
- };
- vartriggerIndex='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(){
- varemployeeData=[
- ['20001234'],
- //省略部分代码
- ['20091546']
- ];
- varsearchField=newExt.ux.form.ComboSearchField({
- id:"employeeId",
- fieldLabel:'员工编码'+'<fontcolorfontcolor="red">*</font>',
- store:newExt.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中运行有效
- //初始化生成表格
- varemployeeForm=newExt.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'
- });
- });
总结
本文在介绍了Ext的基本概念以及扩展Ext的一般方法后,以三个应用场景为例,详细描述了如何从已有的Ext控件出发,借鉴其他控件的功能,开发出满足实际需要的新控件。对于初学者来说,这种“移花接木”式的开发方式,不仅能使开发者深入了解每个控件背后的实现方式,而且能迅速助其实现新的功能、新的需求。可以说,它是一种值得推荐的创新方式。
【编辑推荐】