ExtJS 4 樹

 本文原地址http://www.showframework.com/2012/08/extjs-4-trees/

Tree Panel是ExtJS中最多能的組件之一,它非常適合用於展示分層的數據。Tree PanelGrid 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                     }                 ]             }         ]     } }); 

運行效果如圖

extjs-4-tree-smiple-tree.png

這個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獲得了在樹中使用需要的方法、屬性、字段。下面是個樹節點對象在開發工具中打印的截圖

extjs-4-tree-node-interface.png

關於節點的方法、屬性等,請查看API文檔(ps. 每一個學習ExtJS的開發者都應該仔細研讀API文檔,這是最好的教材)

Visually changing your tree 外觀定製

先嚐試一些簡單的改動。把useArrows設置爲true,Tree Panel就會隱藏前導線使用箭頭表示節點的展開

extjs-4-trees-arrows.png

設置rootVisible屬性爲false,根節點就會被隱藏起來:

extjs-4-trees-root-lines.png

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         }]     } }); 

extjs-4-trees-multiple-columns.png

這裏面的columns配置項期望得到一個Ext.grid.column.Column配置,就跟GridPanel一樣的。唯一的不同就是Tree Panel需要至少一個treecolumn列,這種列是擁有tree視覺效果的,典型的Tree Panel應該只有一列treecolumn。

fields配置項會傳遞給tree內置生成的store用。dataIndex是如何跟列匹配的請仔細看上面例子中的 namedescription,其實就是和每個節點附帶的屬性值匹配

如果不配置column,tree會自動生成一列treecolumn,並且它的dataIndextext,並且也自動隱藏了表頭,如果想顯示錶頭,可以用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方法展開這個新的父節點。

extjs-4-trees-append-children.png

上面的例子利用內聯的方式,亦可:

var parent = root.appendChild({     text: 'Parent 1',     expanded: true,     children: [{         text: 'Child 3',         leaf: true     }] }); 

有時我們期望將節點插入到一個特定的位置,而不是在最末端添加。除了appendChild方法,Ext.data.NodeInterface還提供了insertBeforeinsertChild方法。

var child = parent.insertChild(0, {     text: 'Child 2.5',     leaf: true });  parent.insertBefore({     text: 'Child 2.75',     leaf: true }, child.nextSibling); 

insertChild方法需要一個節點位置,新增的節點將會插入到這個位置。insertBefore方法需要一個節點的引用,新節點將會插入到這個節點之前。

extjs-4-trees-insert-children.png

NodeInterface也提供了幾個可以引用到其他節點的屬性

  • nextSibling
  • previousSibling
  • parentNode
  • lastChild
  • firstChild
  • childNodes

Loading and Saving Tree Data using a Proxy 加載和保存樹上的數據

加載和保存樹上的數據比處理扁平化的數據要複雜一點,因爲每個字段都需要展示層級關係,這一章將會解釋處理這一複雜的工作。

NodeInterface Fields

使用tree數據的時候,最重要的就是理解NodeInterface是如何工作的。每個tree節點都是一個用NodeInterface裝飾的Model實例。假設有個Person Model,它有兩個字段idname

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 }     ] } 

最終形成的樹就是這樣

extjs-4-trees-bulk-load.png

需要注意的是:

  • 所有非葉子節點,但是又沒有子節點的,例如上面圖中的Sue,服務器端返回的數據必須loaded屬性設置爲true,否則這個節點會變成可展開的,並且會嘗試向服務器請求它的子節點數據
  • 另外一個問題,既然loaded是個默認不持久化的屬性,上面一條說了服務器端要返回loaded爲true,那麼服務器端的其他返回內容也會影響tree的其他屬性,比如expanded,這就需要注意了,服務器返回的有些數據可能會導致錯誤,比如如果服務器返回的數據帶有root,和可能會導致錯誤。通常建議除了loadedexpanded,服務器端不要返回其他會被樹利用的屬性。

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 }     ] } 

現在樹會變成這樣:

extjs-4-trees-dynamic-load.png

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();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章