本文原地址http://www.showframework.com/2012/08/extjs-4-trees/
Tree Panel
是ExtJS中最多能的組件之一,它非常適合用於展示分層的數據。Tree Panel
和Grid Panel
繼承自相同的基類,所以所有從Grid Panel
能獲得到的特性、擴展、插件等帶來的好處,在Tree Panel
中也同樣可以獲得。列、列寬調整、拖拽、渲染器、排序、過濾等特性,在兩種組件中都是差不多的工作方式。
讓我們開始創建一個簡單的樹組件
Ext.create('Ext.tree.Panel', { renderTo: Ext.getBody(), title: 'Simple Tree', width: 150, height: 150, root: { text: 'Root', expanded: true, children: [ { text: 'Child 1', leaf: true }, { text: 'Child 2', leaf: true }, { text: 'Child 3', expanded: true, children: [ { text: 'Grandchild', leaf: true } ] } ] } });
運行效果如圖
這個Tree Panel
直接渲染在document.body上,我們定義了一個默認展開的根節點,根節點有三個子節點,前兩個子節點是葉子節點,這意味着他們不能擁有自己的子節點了,第三個節點不是葉子節點,它有一個子節點。每個節點的text
屬性用來設置節點上展示的文字。
Tree Panel
內部使用Tree Store
存儲數據。上面的例子中使用了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, ... });
The Node Interface 節點接口
上面的例子中我們在節點上設定了兩三個不同的屬性,但是節點到底是什麼?前面提到,TreePanel綁定了一個TreeStore,Store在ExtJS中的作用是管理Model實例的集合。樹節點是用NodeInterface
裝飾的簡單的模型實例。用NodeInterface
裝飾Model
使Model獲得了在樹中使用需要的方法、屬性、字段。下面是個樹節點對象在開發工具中打印的截圖
關於節點的方法、屬性等,請查看API文檔(ps. 每一個學習ExtJS的開發者都應該仔細研讀API文檔,這是最好的教材)
Visually changing your tree 外觀定製
先嚐試一些簡單的改動。把useArrows
設置爲true,Tree Panel
就會隱藏前導線使用箭頭表示節點的展開
設置rootVisible
屬性爲false,根節點就會被隱藏起來:
Multiple columns 多列
由於Tree Panel
也是從Grid Panel
相同的父類繼承的,因此實現多列很容易。
var tree = Ext.create('Ext.tree.Panel', { renderTo: Ext.getBody(), title: 'TreeGrid', width: 300, height: 150, fields: ['name', 'description'], //注意這裏 columns: [{ xtype: 'treecolumn', text: 'Name', dataIndex: 'name', width: 150, sortable: true }, { text: 'Description', dataIndex: 'description', flex: 1, sortable: true }], root: { name: 'Root', description: 'Root description', expanded: true, children: [{ name: 'Child 1', description: 'Description 1', leaf: true }, { name: 'Child 2', description: 'Description 2', leaf: true }] } });
這裏面的columns
配置項期望得到一個Ext.grid.column.Column
配置,就跟GridPanel
一樣的。唯一的不同就是Tree Panel需要至少一個treecolumn
列,這種列是擁有tree視覺效果的,典型的Tree Panel應該只有一列treecolumn。
fields
配置項會傳遞給tree內置生成的store用。dataIndex
是如何跟列匹配的請仔細看上面例子中的 name
和description
,其實就是和每個節點附帶的屬性值匹配
如果不配置column,tree會自動生成一列treecolumn,並且它的dataIndex
是text
,並且也自動隱藏了表頭,如果想顯示錶頭,可以用hideHeaders
配置爲false。(LZ注:看到這裏extjs3和4的tree已經有了本質的不同,extjs4的tree本質上就是TreeGrid,只是在只有一列的時候,展現形式爲原來的TreePanel)
Adding nodes to the tree 添加節點
tree的根節點不是必須在初始化時設定。後續再添加也可以:
var tree = Ext.create('Ext.tree.Panel'); tree.setRootNode({ text: 'Root', expanded: true, children: [{ text: 'Child 1', leaf: true }, { text: 'Child 2', leaf: true }] });
儘管對於很小的樹只有默認幾個靜態節點的,這種直接在代碼裏面配置的方式很方便,但是大多數情況tree還是有很多節點的。讓我們看一下如何通過程序添加節點。
var root = tree.getRootNode(); var parent = root.appendChild({ text: 'Parent 1' }); parent.appendChild({ text: 'Child 3', leaf: true }); parent.expand();
每一個不是葉節點的節點都有一個appendChild
方法,這個方法接收一個Node類型,或者是Node的配置參數的參數,返回值是新添加的節點對象。上面的例子中也調用了expand
方法展開這個新的父節點。
上面的例子利用內聯的方式,亦可:
var parent = root.appendChild({ text: 'Parent 1', expanded: true, children: [{ text: 'Child 3', leaf: true }] });
有時我們期望將節點插入到一個特定的位置,而不是在最末端添加。除了appendChild
方法,Ext.data.NodeInterface
還提供了insertBefore
和insertChild
方法。
var child = parent.insertChild(0, { text: 'Child 2.5', leaf: true }); parent.insertBefore({ text: 'Child 2.75', leaf: true }, child.nextSibling);
insertChild
方法需要一個節點位置,新增的節點將會插入到這個位置。insertBefore
方法需要一個節點的引用,新節點將會插入到這個節點之前。
NodeInterface也提供了幾個可以引用到其他節點的屬性
nextSibling
previousSibling
parentNode
lastChild
firstChild
childNodes
Loading and Saving Tree Data using a Proxy 加載和保存樹上的數據
加載和保存樹上的數據比處理扁平化的數據要複雜一點,因爲每個字段都需要展示層級關係,這一章將會解釋處理這一複雜的工作。
NodeInterface Fields
使用tree數據的時候,最重要的就是理解NodeInterface
是如何工作的。每個tree節點都是一個用NodeInterface
裝飾的Model
實例。假設有個Person Model,它有兩個字段id
和name
:
Ext.define('Person', { extend: 'Ext.data.Model', fields: [ { name: 'id', type: 'int' }, { name: 'name', type: 'string' } ] });
如果只做這些,Person Model還只是普通的Model,如果取它的字段個數:
console.log(Person.prototype.fields.getCount()); //輸出 '2'
但是如果將Person Model應用到TreeStore
之中後,就會有些變化:
var store = Ext.create('Ext.data.TreeStore', { model: 'Person', root: { name: 'Phil' } }); console.log(Person.prototype.fields.getCount()); //輸出 '24'
被TreeStore
使用之後,Person多了22個字段。所有這些字段都是在NodeInterface
中定義的,TreeStore初次實例化Person的時候,這些字段會被加入到Person的原型鏈中。
那這22個字段都是什麼,有什麼用處?讓我們簡要的看一下NodeInterface
,它用如下字段裝飾Model,這些字段都是存儲tree相關結構和狀態的:
{name: 'parentId', type: idType, defaultValue: null}, {name: 'index', type: 'int', defaultValue: null, persist: false}, {name: 'depth', type: 'int', defaultValue: 0, persist: false}, {name: 'expanded', type: 'bool', defaultValue: false, persist: false}, {name: 'expandable', type: 'bool', defaultValue: true, persist: false}, {name: 'checked', type: 'auto', defaultValue: null, persist: false}, {name: 'leaf', type: 'bool', defaultValue: false}, {name: 'cls', type: 'string', defaultValue: null, persist: false}, {name: 'iconCls', type: 'string', defaultValue: null, persist: false}, {name: 'icon', type: 'string', defaultValue: null, persist: false}, {name: 'root', type: 'boolean', defaultValue: false, persist: false}, {name: 'isLast', type: 'boolean', defaultValue: false, persist: false}, {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false}, {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false}, {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false}, {name: 'loaded', type: 'boolean', defaultValue: false, persist: false}, {name: 'loading', type: 'boolean', defaultValue: false, persist: false}, {name: 'href', type: 'string', defaultValue: null, persist: false}, {name: 'hrefTarget', type: 'string', defaultValue: null, persist: false}, {name: 'qtip', type: 'string', defaultValue: null, persist: false}, {name: 'qtitle', type: 'string', defaultValue: null, persist: false}, {name: 'children', type: 'auto', defaultValue: null, persist: false}
NodeInterface Fields are Reserved Names 節點接口的字段都是保留字
有一點非常重要,就是上面列舉的這些字段都應該當作保留字段。例如,Model中就不允許有一個字段叫做parentId
了,因爲當Model用在Tree上時,Model的字段會覆蓋NodeInterface的字段。除非這裏有個合法的需求要覆蓋NodeInterface的字段的持久化屬性。
Persistent Fields vs Non-persistent Fields and Overriding the Persistence of Fields 持久化字段和非持久化字段,如何覆蓋持久化屬性
大多數NodeInterface的字段都默認是persist: false
不持久化的。非持久化字段在TreeStore做保存操作的時候不會被保存。大多數情況默認的配置是符合需求的,但是如果真的需要覆蓋持久化設置,下面展示瞭如何覆蓋持久化配置。當覆蓋持久化配置的時候,只改變presist
屬性,其他任何屬性都不要修改
// overriding the persistence of NodeInterface fields in a Model definition Ext.define('Person', { extend: 'Ext.data.Model', fields: [ // Person fields { name: 'id', type: 'int' }, { name: 'name', type: 'string' } // override a non-persistent NodeInterface field to make it persistent { name: 'iconCls', type: 'string', defaultValue: null, persist: true }, ] });
讓我們深入的看一下NodeInterface的字段,列舉一下可能需要覆蓋persist
屬性的情景。下面的每個例子都假設使用了Server Proxy
除非提示不使用。(注:這需要有一些server端編程的知識)
默認持久化的:
parentId
– 用來指定父節點的id,這個字段應該總是持久化,不要覆蓋它leaf
– 用來指出這個節點是不是葉子節點,因此決定了節點是不是可以有子節點,最好不要改變它的持久化設置
默認不持久化的:
index
– 用來指出當前節點在父節點的所有子節點中的位置,當有節點插入或者移除,它的所有鄰居節點的位置都會更新,如果需要,可以用這個屬性去持久化樹節點的排列順序。然而如果服務器端使用另外的排序方法,最好把這個字段保留爲非持久化的,當使用WebStorage Proxy
作爲存儲,且需要保留節點順序,那一定要設置爲持久化的。如果使用了本地排序,建議設置非持久化,因爲本地排序會改變節點的index
屬性depth
用來存儲節點在樹中的層級,如果server需要保存節點層級請開啓持久化。使用WebStorage Proxy
的時候建議不要持久化,會多佔用存儲空間。checked
如果在tree使用checkbox
特性,看業務需求來開啓持久化expanded
存儲節點的展開收起狀態,要不要持久化看業務需求expandable
內部使用,不要變更持久化配置cls
用來給節點增加css類,看業務需求iconCls
用來給節點icon增加css類,看業務需求icon
用來自定義節點,看業務需求root
對根節點的引用,不要變動配置isLast
標識最後一個節點,此配置一般不需要變動isFirst
標識第一個節點,此配置一般不需要變動allowDrop
用來標識可放的節點,此配置不要動allowDrag
用來標識可拖的節點,此配置不要動loaded
用來標識子節點是否加載完成,此配置不要動loading
用來標識子節點是否正在加載中,此配置不要動href
用來指定節點鏈接,此配置看業務需求變動hrefTarget
節點鏈接的target,此配置看業務需求變動qtip
指定tooltip
文字,此配置看業務需求變動qtitle
指定tooltip
的title,此配置看業務需求變動children
內部使用,不要動
Loading Data 加載數據
有兩種加載數據的方式。一次性加載全部節點和分步加載,當節點過多時,一次加載會有性能問題,而且不一定每個節點都用到。動態分步加載是指在父節點展開的時候加載子節點。
Loading the Entire Tree 一次加載
Tree的內部實現是隻有節點展開的時候加載數據。然而全部的層級關係可以通過一個嵌套的數據結構一次全部加載,只要配置root節點是展開的即可
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
返回數據如下
{ "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 } ] }
最終形成的樹就是這樣
需要注意的是:
- 所有非葉子節點,但是又沒有子節點的,例如上面圖中的
Sue
,服務器端返回的數據必須是loaded
屬性設置爲true
,否則這個節點會變成可展開的,並且會嘗試向服務器請求它的子節點數據 - 另外一個問題,既然
loaded
是個默認不持久化的屬性,上面一條說了服務器端要返回loaded
爲true,那麼服務器端的其他返回內容也會影響tree的其他屬性,比如expanded
,這就需要注意了,服務器返回的有些數據可能會導致錯誤,比如如果服務器返回的數據帶有root
,和可能會導致錯誤。通常建議除了loaded
和expanded
,服務器端不要返回其他會被樹利用的屬性。
Dynamically Loading Children When a Node is Expanded 節點展開時動態加載
對於節點非常多的樹,通常期望動態加載,當點擊父節點的展開icon時再向服務器請求子節點數據。例如上面的例子中假設Sue
沒有被服務器端返回的數據設置爲loaded true
,那麼當它的展開icon點擊時,樹的proxy會嘗試向讀取apireadPersons
請求一個這樣的url
/readPersons?node=4
這意思是告訴服務器取得id爲4的節點的子節點,返回的數據格式跟一次加載相同:
{ "success": true, "children": [ { "id": 5, "name": "Evan", "leaf": true } ] }
現在樹會變成這樣:
Saving Data 保存數據
創建、更新、刪除節點都由Proxy自動無縫的處理了。
Creating a New Node 創建新節點
// Create a new node and append it to the tree: var newPerson = Ext.create('Person', { name: 'Nige', leaf: true }); store.getNodeById(2).appendChild(newPerson);
由於Model中定義過proxy,Model的save
方法可以用來持久化節點數據:
newPerson.save();
Updating an Existing Node 更新節點
store.getNodeById(1).set('name', 'Philip');
Removing a Node 刪除節點
store.getRootNode().lastChild.remove();
Bulk Operations 批處理
也可以等創建、更新、刪除了若干個節點之後,由TreeStore的sync
方法一次保存全部
store.sync();