ExtJS 4 樹
Tree Panel
是ExtJS中最多能的組件之一,它非常適合用于展示分層的數(shù)據(jù)。Tree Panel
和Grid Panel
繼承自相同的基類,所以所有從Grid Panel
能獲得到的特性、擴(kuò)展、插件等帶來的好處,在Tree Panel
中也同樣可以獲得。列、列寬調(diào)整、拖拽、渲染器、排序、過濾等特性,在兩種組件中都是差不多的工作方式。
讓我們開始創(chuàng)建一個(gè)簡單的樹組件
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
}
]
}
]
}
});
運(yùn)行效果如圖
這個(gè)Tree Panel
直接渲染在document.body上,我們定義了一個(gè)默認(rèn)展開的根節(jié)點(diǎn),根節(jié)點(diǎn)有三個(gè)子節(jié)點(diǎn),前兩個(gè)子節(jié)點(diǎn)是葉子節(jié)點(diǎn),這意味著他們不能擁有自己的子節(jié)點(diǎn)了,第三個(gè)節(jié)點(diǎn)不是葉子節(jié)點(diǎn),它有一個(gè)子節(jié)點(diǎn)。每個(gè)節(jié)點(diǎn)的text
屬性用來設(shè)置節(jié)點(diǎn)上展示的文字。
Tree Panel
內(nèi)部使用Tree Store
存儲數(shù)據(jù)。上面的例子中使用了root
配置項(xiàng)作為使用store的捷徑。如果我們單獨(dú)指定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 節(jié)點(diǎn)接口
上面的例子中我們在節(jié)點(diǎn)上設(shè)定了兩三個(gè)不同的屬性,但是節(jié)點(diǎn)到底是什么?前面提到,TreePanel綁定了一個(gè)TreeStore,Store在ExtJS中的作用是管理Model實(shí)例的集合。樹節(jié)點(diǎn)是用NodeInterface
裝飾的簡單的模型實(shí)例。用NodeInterface
裝飾Model
使Model獲得了在樹中使用需要的方法、屬性、字段。下面是個(gè)樹節(jié)點(diǎn)對象在開發(fā)工具中打印的截圖
關(guān)于節(jié)點(diǎn)的方法、屬性等,請查看API文檔(ps. 每一個(gè)學(xué)習(xí)ExtJS的開發(fā)者都應(yīng)該仔細(xì)研讀API文檔,這是最好的教材)
Visually changing your tree 外觀定制
先嘗試一些簡單的改動。把useArrows
設(shè)置為true,Tree Panel
就會隱藏前導(dǎo)線使用箭頭表示節(jié)點(diǎn)的展開
設(shè)置rootVisible
屬性為false,根節(jié)點(diǎn)就會被隱藏起來:
Multiple columns 多列
由于Tree Panel
也是從Grid Panel
相同的父類繼承的,因此實(shí)現(xiàn)多列很容易。
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
配置項(xiàng)期望得到一個(gè)Ext.grid.column.Column
配置,就跟GridPanel
一樣的。唯一的不同就是Tree Panel需要至少一個(gè)treecolumn
列,這種列是擁有tree視覺效果的,典型的Tree Panel應(yīng)該只有一列treecolumn。
fields
配置項(xiàng)會傳遞給tree內(nèi)置生成的store用。dataIndex
是如何跟列匹配的請仔細(xì)看上面例子中的 name
和description
,其實(shí)就是和每個(gè)節(jié)點(diǎn)附帶的屬性值匹配
如果不配置column,tree會自動生成一列treecolumn,并且它的dataIndex
是text
,并且也自動隱藏了表頭,如果想顯示表頭,可以用hideHeaders
配置為false。(LZ注:看到這里extjs3和4的tree已經(jīng)有了本質(zhì)的不同,extjs4的tree本質(zhì)上就是TreeGrid,只是在只有一列的時(shí)候,展現(xiàn)形式為原來的TreePanel)
Adding nodes to the tree 添加節(jié)點(diǎn)
tree的根節(jié)點(diǎn)不是必須在初始化時(shí)設(shè)定。后續(xù)再添加也可以:
var tree = Ext.create('Ext.tree.Panel');
tree.setRootNode({
text: 'Root',
expanded: true,
children: [{
text: 'Child 1',
leaf: true
}, {
text: 'Child 2',
leaf: true
}]
});
盡管對于很小的樹只有默認(rèn)幾個(gè)靜態(tài)節(jié)點(diǎn)的,這種直接在代碼里面配置的方式很方便,但是大多數(shù)情況tree還是有很多節(jié)點(diǎn)的。讓我們看一下如何通過程序添加節(jié)點(diǎn)。
var root = tree.getRootNode();
var parent = root.appendChild({
text: 'Parent 1'
});
parent.appendChild({
text: 'Child 3',
leaf: true
});
parent.expand();
每一個(gè)不是葉節(jié)點(diǎn)的節(jié)點(diǎn)都有一個(gè)appendChild
方法,這個(gè)方法接收一個(gè)Node類型,或者是Node的配置參數(shù)的參數(shù),返回值是新添加的節(jié)點(diǎn)對象。上面的例子中也調(diào)用了expand
方法展開這個(gè)新的父節(jié)點(diǎn)。
上面的例子利用內(nèi)聯(lián)的方式,亦可:
var parent = root.appendChild({
text: 'Parent 1',
expanded: true,
children: [{
text: 'Child 3',
leaf: true
}]
});
有時(shí)我們期望將節(jié)點(diǎn)插入到一個(gè)特定的位置,而不是在最末端添加。除了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
方法需要一個(gè)節(jié)點(diǎn)位置,新增的節(jié)點(diǎn)將會插入到這個(gè)位置。insertBefore
方法需要一個(gè)節(jié)點(diǎn)的引用,新節(jié)點(diǎn)將會插入到這個(gè)節(jié)點(diǎn)之前。
NodeInterface也提供了幾個(gè)可以引用到其他節(jié)點(diǎn)的屬性
nextSibling
previousSibling
parentNode
lastChild
firstChild
childNodes
Loading and Saving Tree Data using a Proxy 加載和保存樹上的數(shù)據(jù)
加載和保存樹上的數(shù)據(jù)比處理扁平化的數(shù)據(jù)要復(fù)雜一點(diǎn),因?yàn)槊總€(gè)字段都需要展示層級關(guān)系,這一章將會解釋處理這一復(fù)雜的工作。
NodeInterface Fields
使用tree數(shù)據(jù)的時(shí)候,最重要的就是理解NodeInterface
是如何工作的。每個(gè)tree節(jié)點(diǎn)都是一個(gè)用NodeInterface
裝飾的Model
實(shí)例。假設(shè)有個(gè)Person Model,它有兩個(gè)字段id
和name
:
Ext.define('Person', {
extend: 'Ext.data.Model',
fields: [
{ name: 'id', type: 'int' },
{ name: 'name', type: 'string' }
]
});
如果只做這些,Person Model還只是普通的Model,如果取它的字段個(gè)數(shù):
console.log(Person.prototype.fields.getCount()); //輸出 '2'
但是如果將Person Model應(yīng)用到TreeStore
之中后,就會有些變化:
var store = Ext.create('Ext.data.TreeStore', {
model: 'Person',
root: {
name: 'Phil'
}
});
console.log(Person.prototype.fields.getCount()); //輸出 '24'
被TreeStore
使用之后,Person多了22個(gè)字段。所有這些字段都是在NodeInterface
中定義的,TreeStore初次實(shí)例化Person的時(shí)候,這些字段會被加入到Person的原型鏈中。
那這22個(gè)字段都是什么,有什么用處?讓我們簡要的看一下NodeInterface
,它用如下字段裝飾Model,這些字段都是存儲tree相關(guān)結(jié)構(gòu)和狀態(tài)的:
{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 節(jié)點(diǎn)接口的字段都是保留字
有一點(diǎn)非常重要,就是上面列舉的這些字段都應(yīng)該當(dāng)作保留字段。例如,Model中就不允許有一個(gè)字段叫做parentId
了,因?yàn)楫?dāng)Model用在Tree上時(shí),Model的字段會覆蓋NodeInterface的字段。除非這里有個(gè)合法的需求要覆蓋NodeInterface的字段的持久化屬性。
Persistent Fields vs Non-persistent Fields and Overriding the Persistence of Fields 持久化字段和非持久化字段,如何覆蓋持久化屬性
大多數(shù)NodeInterface的字段都默認(rèn)是persist: false
不持久化的。非持久化字段在TreeStore做保存操作的時(shí)候不會被保存。大多數(shù)情況默認(rèn)的配置是符合需求的,但是如果真的需要覆蓋持久化設(shè)置,下面展示了如何覆蓋持久化配置。當(dāng)覆蓋持久化配置的時(shí)候,只改變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
屬性的情景。下面的每個(gè)例子都假設(shè)使用了Server Proxy
除非提示不使用。(注:這需要有一些server端編程的知識)
默認(rèn)持久化的:
parentId
– 用來指定父節(jié)點(diǎn)的id,這個(gè)字段應(yīng)該總是持久化,不要覆蓋它leaf
– 用來指出這個(gè)節(jié)點(diǎn)是不是葉子節(jié)點(diǎn),因此決定了節(jié)點(diǎn)是不是可以有子節(jié)點(diǎn),最好不要改變它的持久化設(shè)置
默認(rèn)不持久化的:
index
– 用來指出當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)的所有子節(jié)點(diǎn)中的位置,當(dāng)有節(jié)點(diǎn)插入或者移除,它的所有鄰居節(jié)點(diǎn)的位置都會更新,如果需要,可以用這個(gè)屬性去持久化樹節(jié)點(diǎn)的排列順序。然而如果服務(wù)器端使用另外的排序方法,最好把這個(gè)字段保留為非持久化的,當(dāng)使用WebStorage Proxy
作為存儲,且需要保留節(jié)點(diǎn)順序,那一定要設(shè)置為持久化的。如果使用了本地排序,建議設(shè)置非持久化,因?yàn)楸镜嘏判驎淖児?jié)點(diǎn)的index
屬性depth
用來存儲節(jié)點(diǎn)在樹中的層級,如果server需要保存節(jié)點(diǎn)層級請開啟持久化。使用WebStorage Proxy
的時(shí)候建議不要持久化,會多占用存儲空間。checked
如果在tree使用checkbox
特性,看業(yè)務(wù)需求來開啟持久化expanded
存儲節(jié)點(diǎn)的展開收起狀態(tài),要不要持久化看業(yè)務(wù)需求expandable
內(nèi)部使用,不要變更持久化配置cls
用來給節(jié)點(diǎn)增加css類,看業(yè)務(wù)需求iconCls
用來給節(jié)點(diǎn)icon增加css類,看業(yè)務(wù)需求icon
用來自定義節(jié)點(diǎn),看業(yè)務(wù)需求root
對根節(jié)點(diǎn)的引用,不要變動配置isLast
標(biāo)識最后一個(gè)節(jié)點(diǎn),此配置一般不需要變動isFirst
標(biāo)識第一個(gè)節(jié)點(diǎn),此配置一般不需要變動allowDrop
用來標(biāo)識可放的節(jié)點(diǎn),此配置不要?jiǎng)?/li>allowDrag
用來標(biāo)識可拖的節(jié)點(diǎn),此配置不要?jiǎng)?/li>loaded
用來標(biāo)識子節(jié)點(diǎn)是否加載完成,此配置不要?jiǎng)?/li>loading
用來標(biāo)識子節(jié)點(diǎn)是否正在加載中,此配置不要?jiǎng)?/li>href
用來指定節(jié)點(diǎn)鏈接,此配置看業(yè)務(wù)需求變動hrefTarget
節(jié)點(diǎn)鏈接的target,此配置看業(yè)務(wù)需求變動qtip
指定tooltip
文字,此配置看業(yè)務(wù)需求變動qtitle
指定tooltip
的title,此配置看業(yè)務(wù)需求變動children
內(nèi)部使用,不要?jiǎng)?/li>
Loading Data 加載數(shù)據(jù)
有兩種加載數(shù)據(jù)的方式。一次性加載全部節(jié)點(diǎn)和分步加載,當(dāng)節(jié)點(diǎn)過多時(shí),一次加載會有性能問題,而且不一定每個(gè)節(jié)點(diǎn)都用到。動態(tài)分步加載是指在父節(jié)點(diǎn)展開的時(shí)候加載子節(jié)點(diǎn)。
Loading the Entire Tree 一次加載
Tree的內(nèi)部實(shí)現(xiàn)是只有節(jié)點(diǎn)展開的時(shí)候加載數(shù)據(jù)。然而全部的層級關(guān)系可以通過一個(gè)嵌套的數(shù)據(jù)結(jié)構(gòu)一次全部加載,只要配置root節(jié)點(diǎn)是展開的即可
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 }
]
});
假設(shè)readPersons
返回?cái)?shù)據(jù)如下
{
"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 }
]
}
最終形成的樹就是這樣
需要注意的是:
- 所有非葉子節(jié)點(diǎn),但是又沒有子節(jié)點(diǎn)的,例如上面圖中的
Sue
,服務(wù)器端返回的數(shù)據(jù)必須是loaded
屬性設(shè)置為true
,否則這個(gè)節(jié)點(diǎn)會變成可展開的,并且會嘗試向服務(wù)器請求它的子節(jié)點(diǎn)數(shù)據(jù) - 另外一個(gè)問題,既然
loaded
是個(gè)默認(rèn)不持久化的屬性,上面一條說了服務(wù)器端要返回loaded
為true,那么服務(wù)器端的其他返回內(nèi)容也會影響tree的其他屬性,比如expanded
,這就需要注意了,服務(wù)器返回的有些數(shù)據(jù)可能會導(dǎo)致錯(cuò)誤,比如如果服務(wù)器返回的數(shù)據(jù)帶有root
,和可能會導(dǎo)致錯(cuò)誤。通常建議除了loaded
和expanded
,服務(wù)器端不要返回其他會被樹利用的屬性。
Dynamically Loading Children When a Node is Expanded 節(jié)點(diǎn)展開時(shí)動態(tài)加載
對于節(jié)點(diǎn)非常多的樹,通常期望動態(tài)加載,當(dāng)點(diǎn)擊父節(jié)點(diǎn)的展開icon時(shí)再向服務(wù)器請求子節(jié)點(diǎn)數(shù)據(jù)。例如上面的例子中假設(shè)Sue
沒有被服務(wù)器端返回的數(shù)據(jù)設(shè)置為loaded true
,那么當(dāng)它的展開icon點(diǎn)擊時(shí),樹的proxy會嘗試向讀取api readPersons
請求一個(gè)這樣的url
/readPersons?node=4
這意思是告訴服務(wù)器取得id為4的節(jié)點(diǎn)的子節(jié)點(diǎn),返回的數(shù)據(jù)格式跟一次加載相同:
{
"success": true,
"children": [
{ "id": 5, "name": "Evan", "leaf": true }
]
}
現(xiàn)在樹會變成這樣:
Saving Data 保存數(shù)據(jù)
創(chuàng)建、更新、刪除節(jié)點(diǎn)都由Proxy自動無縫的處理了。
Creating a New Node 創(chuàng)建新節(jié)點(diǎn)
// 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
方法可以用來持久化節(jié)點(diǎn)數(shù)據(jù):
newPerson.save();
Updating an Existing Node 更新節(jié)點(diǎn)
store.getNodeById(1).set('name', 'Philip');
Removing a Node 刪除節(jié)點(diǎn)
store.getRootNode().lastChild.remove();
Bulk Operations 批處理
也可以等創(chuàng)建、更新、刪除了若干個(gè)節(jié)點(diǎn)之后,由TreeStore的sync
方法一次保存全部
store.sync();