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,
...
});
上面的例子中我們在節點上設定了兩三個不同的屬性,但是節點到底是什嗎?前面提到,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:
var child = parent.insertChild(0, { text: 'Child 2.5', leaf: true });
parent.insertBefore({ text: 'Child 2.75', leaf: true }, child.nextSibling);
如果只做這些,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返回資料如下
需要注意的是:
所有非葉子節點,但是又沒有子節點的,例如上面圖中的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會嘗試向讀取api readPersons請求一個這樣的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();
轉自:http://www.showframework.com/2012/08/extjs-4-trees/