移花接木 實例講解Ext JS控件的擴展
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篇的,有資格申請該助研基金??梢钥闯鲈摶痼w現了對優秀青年女教師的科研支持。此部分的代碼請參考示例代碼中的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控件出發,借鑒其他控件的功能,開發出滿足實際需要的新控件。對于初學者來說,這種“移花接木”式的開發方式,不僅能使開發者深入了解每個控件背后的實現方式,而且能迅速助其實現新的功能、新的需求。可以說,它是一種值得推薦的創新方式。
【編輯推薦】