結構圖
概述
TreePanel是在Ext JS中最功能豐富的組件之一,是一個非常棒的工具,用於顯示在應用程序中的結構化數據。TreePanel是從GridPane繼承的類,因此,所有GridPanel的特點,好處,擴展和插件都可以用在treePanel中。比如像列,列縮放,拖放,渲染,排序和過濾的東西,這兩個組件都可以一樣的用.
我們來創建一個非常簡單的demo.
Ext.create('Ext.tree.Panel', { renderTo: Ext.getBody(), title: '簡單的一棵樹', width: 150, height: 150, root: { text: 'Root', expanded: true, children: [ { text: '節點1', leaf: true }, { text: '節點2', leaf: true }, { text: '節點3', expanded: true, children: [ { text: '孫子節點', leaf: true } ] } ] } });
這棵樹將會在頁面的body中被渲染出來.我們定義了一個自動展開的根節點.根節點包含了樹的子節點.前兩個子節點帶有leaf:true屬性,標示他們不能包含任何子節點了.第三個子節點沒有包含leaf:true屬性,表示他可以包含子節點,text屬性是用來作為tree上的節點展示的.
下面給出效果圖.
樹的內部是用TreeStore來存放數據的.我們上面的例子用一個root節點來快速的配置了樹的store,如果我們想為樹另外配置一個store的話,一般代碼類似如下
var store = Ext.create('Ext.data.TreeStore', { root: { text: 'Root', expanded: true, children: [ { text: 'Child 1', leaf: true }, { text: 'Child 2', leaf: true }, ... ] } }); Ext.create('Ext.tree.Panel', { title: 'Simple Tree', store: store, ... });
想知道更多Store相關知識,你可以查看數據相關章節.(在這里我可能一時間還沒有時間去翻譯)
節點接口(Node Interface)
上面的例子,我們已經設置了一對屬性不同的節點了.但是,究竟節點(node)是個啥?就像前面說過的,TreePanel需要跟一個TreeStore協調工作,一個Store在ExtJs中,就是很多了Model(模型,詳細可以查閱Ext的Model相關知識)的實例集合.節點(node)是簡單的Model的實例,並且用NodeInterface來進行了包裝.其實就是用NodeInterface的字段(field,相關知識可以查閱Model文檔)來給一個Model進行屬性包裝,給予這個Model一些字段,方法,還有屬性來保證這個model的實例可以在treePanel中工作, 以下是一個屏幕截圖,顯示了開發者工具中的一個節點的結構。
這是官方給出的截圖,我不認為這個截圖很容易看懂,我多說幾句稍微說明一下吧.
查看下圖
你可以通過上圖結合了解一個問題,那就是上面這些個屬性,方法,都可以在treePanel中的任何一個節點上獲取到並且執行對應方法或者讀取屬性.
至於那個config就是model中的字段,也可以說,這里的字段都會在treeModel中默認存在,可以不用特意去配置.尤其是text屬性,看到這個屬性你應該能清楚明白了吧.
如果你想要查看全部的屬性,方法,字段等信息,請查閱API文檔中的 NodeInterface(注意,這是一個類,不過更類似java中的抽象類)
樹的視覺效果
讓我們嘗試一些簡單操作。
當您設置lines為false, TreePanel將會隱藏折線.
當您設置useArrows為true,TreePanel將會隱藏折線,並且顯示出一個箭頭圖標.
如下圖對比(出於國人不喜歡動手實驗習慣的原因,我沒有給出官方的demo圖,而是自己編碼了兩張效果圖)
設置rootVisible為false,將會不顯示出根節點,這樣的時候根節點會默認的進行展開(有的時候根節點幾乎是必須的,因為TreeStore的autoLoad屬性是無效的,至少Ext4.2.1為止是無效的),下面給出一個樹, rootVisible和lines都是false
首先注意如下代碼
可以看到,我的root設置expanded是false,也就是默認不展開.但是rootVisible也是false,也就是不顯示根節點.
多列樹
由於TreePanel繼承自GridPanel,所以想給一棵樹添加很多個列,是非常容易的.
看如下代碼
Ext.create('Ext.tree.Panel', { renderTo: Ext.getBody(), title: '一顆擁有多列的樹', fields: ['name', 'description'], columns: [{ xtype: 'treecolumn', text: '節點名', dataIndex: 'name', width: 150, sortable: true }, { text: '描述', dataIndex: 'description', flex: 1, sortable: true }], root: { name: 'Root', description: '根節點是這么描述的', expanded: true, children: [{ name: '子節點1', description: '這個就是子節點1的描述', leaf: true }, { name: '子節點2', description: '這個就是子節點2的描述', leaf: true }] } });
TreePanel的column配置就像給GridPanel配置Column一樣無非就是一組Ext.grid.column.Column集合,但是唯一不同的地方就是一組TreePanel至少需要一個xtype'treecolumn'的列,這類型的column帶有特定的視覺樣式,如展開和折疊的圖標,列前面的線等,一個典型的樹面板將只有一個'treecolumn。
字段(field)的配置是用過Store使用的那個Model來設置的(有關Model的更多信息,請參閱Data手冊),你要注意dataIndex需要映射到我們制定的字段(field)上,比如上面的name和description.
你需要注意,當你沒有配置任何列的時候,TreePanel會默認給你配置一個列,列的dataIndex是”text”,並且會隱藏這個列的標題,如果你想讓標題顯示,你需要配置TreePanel的hideHeaders為false.
動態給樹添加節點
TreePanel並沒有在內部直接定義好一個根節點(Root Node),我們隨時可以延遲性的添加.
var tree = Ext.create('Ext.tree.Panel',{ renderTo:Ext.getBody() }); tree.setRootNode({ text: 'Root', expanded: true, children: [ { text: 'Child 1', leaf: true }, { text: 'Child 2', leaf: true } ] });
雖然這樣對於那些只有幾個靜態節點的比較小的TreePanel來說是非常有用的,但是大多數的TreePanel都有更多的節點,既然這樣,我們來看看如何 [用編碼的方式] (翻譯的可能不太好),來動態添加節點.
在上面代碼的基礎之上,我們書寫如下代碼
var root = tree.getRootNode(); var parent = root.appendChild({ text: 'Parent 1' }); parent.appendChild({ text: 'Child 3', leaf: true }); parent.expand();
每一個非葉子節點(leaf node)的節點都擁有一個appendChild()的方法,第一個參數可以接收一個節點(node)或者接收一個object,並且會把這個節點作為返回值返回,上面的例子還調用了expand這個方法來把最后添加的那個節點進行展開.
在創建父節點的時候同事創建子節點也是非常可行的,看如下代碼,我們得到的效果是一樣的.
var parent = root.appendChild({ text: 'Parent 1', expanded: true, children: [{ text: 'Child 3', leaf: true }] });
有的時候,我們需要在樹的某個位置插入節點,而不是在一個節點附加到某個節點上.
Ext.data.NodeInterface除了appendChild這個方法之外,Ta還提供了一個insertBefore方法和insertChild方法.
我們來看看如下代碼
var child = parent.insertChild(0, { text: '我是插入的節點.我在第0個位置', leaf: true });
parent.insertBefore({
text: '我是插入的節點,我的位置是剛才插入的節點的下一個節點之前',
leaf: true
}, child.nextSibling);
insertChild這個方法需要一個排序號(index)來作為參數,以便指定節點要插入的地方. insertBefore則需要一個節點的引用作為參數,這個節點將會插入到指定的節點的前面.
繼續添加上述代碼執行,效果如圖展示
查看API類似如下(在節點接口章節已有說明)
對於這樣的一個Person類來說,他只是一個很普通的Model,如果他被實例化一個實例出來,可以很容易進行驗證,因為他的字段集合中只有兩個字段而已.
當Person這個類被用在TreeStore中的時候,一些很有趣的事情要發生了.你注意看一下此時他的字段數量.
當Person類被用在了TreeStore中的時候,他被添加了23個額外的屬性,這些額外的屬性都定義在NodeInterface上面.當Model實體類模型被用在某個TreeStore上,並且第一次實例化的時候 ,這些個屬性會添加到Model實體類的的原型(prototype )上 (至於上述代碼,則是通過把他設置為根節點的時候觸發實例化處理的)
參看如下代碼,如果你知識在TreeStore中使用了某個Model實體類模型,但是沒有實例化,這個model的原型是不會變得.
那么究竟這23個額外的字段,以及他們在做什么呢?快速瀏覽一下的NodeInterface源代碼,它包裝的模型具有以下字段。這些字段用於內部存儲樹的結構和狀態有關的信息:
NodeInterface字段保留名稱
重要的是要注意,上述的所有字段名應被視為” 保留關鍵字”,比如,他不允許你在Model實體類模型中設置一個叫”parentId”的字段,因為如果這個Model用在TreePanel(實際上是TreeStore)中的時候,系統會用Nodeinterface覆蓋這些個屬性,這個規則也有例外,就是你擁有很合乎情理的數據字段需要進行覆蓋.(譯者:這種情況極少出現,事實上我們完全可以用那些系統關鍵字作為系統結構穩定作用,我們完全不去理會那些關鍵字,只挑出必須要用的自行對應即可.)
持久性字段與非持久性字段和重寫字段的持久性
NodeInterface的大多數字段默認都是persist: false這樣的, 這意味着他們在默認情況下是非持久性字段的,非持久性字段是不會通過Proxy代理來進行保存的.不管你是調用TreeStore的sync方法還是調用Model的save方法,他都不會保存.在大多數的情況下,我們保留這些關鍵字的持久化屬性即可.但是在某些情況下也有必要來重寫這些字段的持久性屬性, 下面的例子演示了如何重寫持久的NodeInterface的字段,當你重寫NodeInterface的字段的時候,對你來說最重要的是去定義字段的(persist),name,和type, defaultValue這個屬性你絕對不應該去修改.
//重寫nodeInterface的字段 Ext.define('Person', { extend: 'Ext.data.Model', fields: [ // Person fields { name: 'id', type: 'int' },//讓Id變成int類型 { name: 'name', type: 'string' }, // 重寫非持久化屬性,讓他變成持久化的. { name: 'iconCls', type: 'string', defaultValue: null, persist: true } ] });
我們來深度剖析一下NodeInterface的字段屬性和應用場景,不管這個字段是不是必要的,我們都可以覆蓋他的持久化屬性,在下面的每一個例子中.如果沒有做特殊聲明,都會假設這個Store已經用了Server Proxy(服務器代理).
提示:在你重寫的時候其實不需要覆蓋persist屬性.
默認持久的化設置:
parentId:這個屬性用來標記這個節點的父節點的Id,這個屬性應該一直都被持久化,而且你不應該去重寫這個屬性.
laef:這個屬性標記了這個節點是子節點,不允許再有子節點添加進來了,通常這個屬性我們不應該去重寫.
並非默認持久化的屬性:
index
-用於存儲在它們的父節點的順序,當一個節點被inerted或者removed,他的所有兄弟節點會在插入或者刪除之后更新自己的索引(Index),如果有需要的話,服務器可以用這個字段來保證數據有序性,然而當服務器擁有一個完全不一樣的排序方式的時候,這有可能讓index屬性不進行持久化更好一些,當使用一個WebStorage Proxy的時候,這個字段必須重寫持久化屬性,如果你的客戶端允許直接在客戶端排,最好讓這個屬性為非持久性的.否則因為排序的Index屬性會去更新所有的節點的index,這樣會導致他們,這將導致下次調用sync或者save方法的時候他們被進行持久化保存,當然,如果設置他們為非持久化的,那就不會這樣了.depth
- 用來保存一個節點在樹狀數據結構的深度,如果服務器需要保存一個深度的字段,你可以覆蓋來打開這個字段的持久化屬性,當你使用WebStorage Proxy的時候,不可以覆蓋這個字段的持久性, 因為它並不需要妥善存儲的樹狀結構數據, 只是占用額外的空間罷了.checked
–如果你需要使用 checkbox 插件的話,你需要覆蓋這個屬性. (譯者:不在贅述”可以覆蓋這個屬性雲雲……”)expanded
– 如果你想要在服務器保存數據的默認展開狀態.expandable
– 我們沒有什么必要覆蓋這個屬性的持久性,這個屬性是指明你的節點是否可以進行展開的.cls
– 給TreePanel中給某個節點添加css類.iconCls
–給TreePanel中的某個節點重新定義一個icon圖標的類.(譯者:關於這個你可以查閱Ext相關css樣式表里面的icon定義方法.)icon
– 給TreePanel中的某個節點直接定義一個圖片引用作為圖標,這需要你指明圖片路徑(譯者:當然,不論是相對還是絕對)root
-用於指示該節點是否是根節點。該字段不應該被覆蓋.isLast
–這個字段通常不需要重寫,他是用來表示這個節點是否為同深度兄弟節點的最后一個節點的.isFirst
-這個字段通常不需要重寫,他是用來表示這個節點是否為同深度兄弟節點的第一個節點的.allowDrop
–不要覆蓋這個字段,這個字段是控制節點是否可以進行拖出的.allowDrag
–不要覆蓋這個字段,這個字段是控制節點是否可以進行拖拽的.(譯者:關於allowDrag
和allowDrop
,你可以慢慢體會,他們有很神奇的差別)loaded
–不要覆蓋這個字段,這個字段表示這個字段是否是已經加載完成的.loading
–不要覆蓋這個字段,這個字段他表示他正在用代理(proxy)讀取子節點.(關於loading和loaded,我們通常可以在程序中用來判斷節點的加載狀態,比如lazy樹是很有用的)href
– 用來配置節點擁有一個超鏈接地址.hrefTarget
–就好像html中的target一樣,如果節點有href屬性那么這個屬性用來表示在什么地方打開連接qtip
–一個短提示信息.qtitle
– 提示信息的title.children
– 通常我們不需要覆蓋這個字段,如果你想要一次性讀取整棵樹的時候,這里包含了其他子節點,這個字段是一個數組.(譯者:lazy樹和sync樹之間的差別往往體現在這里,當然,如果你的數據指明children:[]這樣這個節點就相當於不可以展開的子節點了.否則請不要讓返回的數據帶有這個屬性.)
加載數據
有兩種方式來加載TreePanel的數據,第一種方法就是讓數據代理在第一次請求的時候就把數據全都加載出來.對於一棵較大的樹來說,一次性加載所有的數據必然是不理想的,所以這種樹通常優先選擇第二種解決方案,那就是在展開一個節點的時候再動態的去加載每個子節點的孫子節點.
加載整個樹
樹的內部會在一個節點被展開的時候加載他下面的所有數據,然而,如果你的proxy代理在檢索的時候能加載到整個前台的樹狀數據結構,就可以直接構造完整的一棵樹. 要做到這一點,需要初始化TreeStore的根節點擴展:
(如果你能堅持看到這里,相信你對西面這段代碼不會陌生.因為筆者並非在原創ExtJs課程教材,只不過在翻譯一份官方的快速入門手冊,所以在這里,筆者就不進行前后台交互代碼演示的書寫了.如果有興趣你可以根據chrome查看xhr請求頭信息,通常這些信息對於一個開發者來說至關重要.如果你想要知道這些數據加載的交互詳情,你可以參考我的其他文章.當然,我個人認為如果你用過json,你只需要知道tree的請求頭信息,和響應格式即可.)
Ext.define('Person', { extend: 'Ext.data.Model', fields: [ { name: 'id', type: 'int' }, { name: 'name', type: 'string' } ], proxy: { type: 'ajax', api: { create: 'createPersons', read: 'readPersons', update: 'updatePersons', destroy: 'destroyPersons' } } }); var store = Ext.create('Ext.data.TreeStore', { model: 'Person', root: { name: 'People', expanded: true } }); Ext.create('Ext.tree.Panel', { renderTo: Ext.getBody(), width: 300, height: 200, title: 'People', store: store, columns: [ { xtype: 'treecolumn', header: 'Name', dataIndex: 'name', flex: 1 } ] }); //假設的readPersons URL返回下面的JSON對象 { "success": true, "children": [ { "id": 1, "name": "Phil", "leaf": true }, { "id": 2, "name": "Nico", "expanded": true, "children": [ { "id": 3, "name": "Mitchell", "leaf": true } ]}, { "id": 4, "name": "Sue", "loaded": true } ] }
那下面就我們要加載的整棵樹了
要注意的重要地方:
- 對於沒有子節點的節點,服務器有必要返回一個loaded屬性並且為true,否則treeStore會去加載這些子節點.
- 問題隨之而來,如果服務器允許返回一個loaded屬性在JSON中,他可以再設置其他非持久化字段嗎?答案當然可以,在上面的例子中.比如Nico這個節點,他的數據已經設置了expanded屬性為true,所以他的子節點會自動展開在TreePanel中.在某些情況下,這樣做是不恰當的,你需要小心一點.比如root這個字段,他表示是不是根節點,這樣的字段我們可以在服務器返回JSON數據的時候作為非持久化數據來進行返回,但是通常你不應該去重寫這個字段.
在節點展開的時候動態加載
當一棵樹比較大的時候,你可能只加載你需要加載的數據,然后剩下的數據在展開節點的時候繼續加載.比如上面的Sue這個節點,我們假設他並沒有在返回的數據設置他的loaded屬性為true,TreePanel將會在這個節點旁邊顯示一個展開節點的小圖標,當節點展開的時候, proxy 數據代理會進行”readPersons
”這個url的請求,並且這個請求大概長得是這個樣子:
/readPersons?node=4
這將會告訴服務器我現在要檢索的數據的節點的ID是4,服務器應該返回相應的數據,來填充到這個節點的子節點中.
也許就是類似這樣的數據
{ "success": true, "children": [ { "id": 5, "name": "Evan", "leaf": true } ] }
現在,這棵樹就會變成這個樣子:
保存數據
proxy數據代理會很完美的自動處理創建,更新和刪除節點.
創建一個節點
//創建一個新節點,然后添加到TreePanel中
var newPerson = Ext.create('Person', { name: 'Nige', leaf: true }); store.getNodeById(2).appendChild(newPerson);
由於在proxy數據代理上使用了Model模型,模型的save方法可以直接用來保存數據
(譯者:這表示他會自動發起一個請求,請求的路徑是根據proxy中的api屬性的create這個屬性,也就是創建的路徑,乍一看你可能覺得用處不大,但是事實上這是很有用的,因為這完全省去了我們書寫Ext.Ajax.requst的過程,也許有的人會用$.ajax(),總之這樣允許我們就好像在前台操作一樣,服務器的處理” 自動”被調用,你要做的就是跟后台開發人員這樣說:”我的數據帶有id,XX屬性和XX屬性,是XX表的,你給寫個保存的處理”)
newPerson.save();
更新一個節點
store.getNodeById(1).set('name', 'Philip');
刪除一個節點
store.getRootNode().lastChild.remove();
批量操作
創建,更新和刪除多個節點后,我們可以在一個操作中直接對這些數據進行處理.
store.sync();
譯者:葉知泉
聯系QQ:1330771552
初稿:2013年5月26日21:39:14
如需轉載,
注明出處,
鄙人不才,
若有寒暄,
但說無妨.