征服ExtJs那棵树(ExtJs官方开发手册汉语详解--TreePanel)
结构图
概述
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
如需转载,
注明出处,
鄙人不才,
若有寒暄,
但说无妨.