翻译 - 【Dojo Tutorials】Connecting a Store to a Tree
原文:Connecting a Store to a Tree
Dojo Tree组件是一个强大的展示层级数据的工具。该教程将展示如何连接tree与store来快速有效的展示层级数据。
介绍
Dojo Tree组件为展示层级数据提供了一种综合的,熟悉的,直观的方式。它将展示与数据分离。本文将提到为驱动一个tree提供数据的多种方法。
第一个例子使用一个静态tree,其数据从一个JSON文件中获取。这可以用于通过数据提供导航。第二个例子在这个设计的基础上扩展新的强大功能,如拖拽,维护一个动态的tree。最后的例子如何懒加载数据。
带有静态Store的Tree
静态的store比较适合用于有限的tree。这个例子中,点击tree的节点将会展示一个相关的图片。
第一步是构造数据。我们将使用内存store,也就是说store的数据是用JSON编码表示的,可以包含一些提供的信息。在这方面,每个节点中name做标签使用。这个tree有四个子项,每个子项都有name与id。
1 { 2 "name": "US Goverment", 3 "id": "root", 4 "children": [ 5 { 6 "name": "Congress", 7 "id": "congress" 8 }, 9 { 10 "name": "Executive", 11 "id": "exec" 12 }, 13 { 14 "name": "Judicial", 15 "id": "judicial" 16 } 17 ] 18 }
一个tree由实现了dijit/tree/model的接口的对象提供数据。通常,这个对象是dijit/store/ObjectStoreModel的一个实例,且有dojo/store存储。这就是该实例工作的方式。
下面的代码构建一个内存store,供给一个ObjectStoreModel,然后用于一个tree。最后,onLoad与onClick事件用于展示相关的图片。
1 require([ 2 "dojo/dom", 3 "dojo/json", 4 "dojo/store/Memory", 5 "dijit/tree/ObjectStoreModel", 6 "dijit/Tree", 7 "dojo/text!./data/static", 8 "dojo/domReady!" 9 ], function(dom, JSON, Memory, ObjectStoreModel, tree, data) { 10 var govermentStore = new Memory({ 11 data: [ JSON.parse(data) ], 12 getChildren: function(object) { 13 return object.children || []; 14 } 15 }); 16 17 var govermentModel = new ObjectStoreModel({ 18 store: govermentStore, 19 query: {id: 'root'}, 20 mayHaveChildren: function(item) { 21 return "children" in item; 22 } 23 }); 24 25 var govermentTree = new Tree({ 26 model: govermentModel, 27 onOpenClick: true, 28 onLoad: function() { 29 dom.byId("image").src = "../resources/images/root.jpg"; 30 }, 31 onClick: function(item) { 32 dom.byId("image").src = "../resources/images/" + item.id + ".jpg"; 33 } 34 }, "divTree"); 35 36 govermentTree.startup(); 37 });
注意我们自己实现两个方法:
- getChildren() - 用于返回给定项的子项列表
- mayHaveChildren() - 在这个简单的表单中它总是返回真,如果返回假,则说明只是查看这个项,它并没有子项,当前或以后(由于拖拽或其他数据更新了store)。
动态更新与拖拽
我们将学习如何使用Tree的拖拽功能,以及实时响应数据的变化。
对于一个带有数据更新的Tree,包括DnD,store需要能够理解与追踪父子关系,当一个甚至一系列子项变化时通知tree。基于此,我们将创建关联格式的数据,每项都指向它的父元素:
1 [ 2 { 3 "name": "US Government", 4 "id": "root" 5 }, 6 { 7 "name": "Congress", 8 "id": "congress", 9 "parent": "root" 10 }, 11 { 12 "name": "House of Representatives", 13 "id": "house", 14 "parent": "congress" 15 }, 16 { 17 "name": "Senate", 18 "id": "senate", 19 "parent": "congress" 20 }, 21 { 22 "name": "Executive", 23 "id": "exec", 24 "parent": "root" 25 }, 26 { 27 "name": "President", 28 "id": "pres", 29 "parent": "exec" 30 }, 31 { 32 "name": "Vice President", 33 "id": "vice-pres", 34 "parent": "exec" 35 }, 36 { 37 "name": "Secretary of State", 38 "id": "state", 39 "parent": "exec" 40 }, 41 { 42 "name": "Cabinet", 43 "id": "cabinet", 44 "parent": "exec" 45 }, 46 { 47 "name": "National Security Council", 48 "id": "security", 49 "parent": "cabinet" 50 }, 51 { 52 "name": "Council of Economic Advisers", 53 "id": "economic", 54 "parent": "cabinet" 55 }, 56 { 57 "name": "Office of Management and Budget", 58 "id": "budget", 59 "parent": "cabinet" 60 }, 61 { 62 "name": "Judicial", 63 "id": "judicial", 64 "parent": "root" 65 } 66 ]
不管缩进,这实际是一个扁平化的列表,每个元素指向它的父元素(除了根元素)。
接下来,我们构建store。这个store需要具备一下能力用于ObjectStoreModel与Tree,且能反射动态数据的更新:
- 支持通过query方法获取根节点(store.query()),而不是store.get(),且返回一个只有一个元素的列表与observe()方法。ObjectStoreModel将利用observe()的方法来检测跟节点的改变,例如修改了标签。
- getChildren()方法返回一个带有observe()函数的子元素列表。observe()方法用于检测子节点的变化,对于新的节点加入或有子节点被删除的情况。
- put()方法可以用于向一个节点添加子节点。
前两点用一个Observable包裹的store很容易实现。另外,getRoot()与getChildren()也可以在store作为查询类实现。
对于第三点,自从dojo/store/Memory不再支持这一选项,我们需要自己添加。代码像下面这样:
1 require([ 2 "dojo/aspect", 3 "dojo/json", 4 "dojo/query", 5 "dojo/store/Memory", 6 "dojo/store/Observable", 7 "dijit/Tree", 8 "dijit/tree/ObjectStoreModel", 9 "dijit/tree/dndSource", 10 "dojo/text!./data/all.json", 11 "dojo/domReady!" 12 ], function(aspect, json, query, Memory, Observable, Tree, ObjectStoreModel, dndSource, data) { 13 14 // set up the store to get the tree data, plus define the method 15 // to query the children of a node 16 var governmentStore = new Memory({ 17 data: json.parse(data), 18 getChildren: function(object) { 19 return this.query({parent: object.id}); 20 } 21 }); 22 23 // To support dynamic data changes, including DnD, 24 // the store must support put(child, {parent: parent}) 25 // But dojo/store/Memory doesn't, so we have to implement it. 26 // Sine our store is relational, that just amounts to setting child.parent 27 // to the parent's id. 28 aspect.around(governmentStore, "put", function(originalPut) { 29 return function(obj, options) { 30 if(options && options.parent) { 31 obj.parent = options.parent.id; 32 } 33 return originalPut.call(governmentStore, obj, options); 34 } 35 }); 36 37 // give store Observable interface so Tree can track updates 38 governmentStore = new Observable(governmentStore); 39 });
最后,添加拖拽支持,我们需要定义一个拖拽控制器:我们将使用标准的dijit/tree/dndStore作为控制器:
1 require([ 2 "dijit/Tree", 3 "dijit/tree/dndSource", 4 "dojo/domReady!" 5 ], function(Tree, dndSource) { 6 var tree = new Tree({ 7 model: usGov, 8 // define the drap-n-drop controller 9 dndController: dndSource 10 }, "tree"); 11 tree.startup(); 12 });
现在拖拽操作应该触发调用通过put()更新数据的model.pasteItem()函数。
代码操作数据变更
值得注意的是Tree遵循响应数据变更而不是控制器动作的标准MVC原则。这是非常强大的功能因为数据视图可以响应变化而不关心是什么引起了变化(也许是代码操作,拖拽等等)。
因此,添加一个子节点,我们可以使用put()保存它,指定父节点,Tree可以自动响应。下面的样例中,一个按钮出发子对象的添加动作:
1 query("#add-new-child").on("click", function() { 2 // get the selected object from the tree 3 var selectedObject = tree.get("selectedItems")[0]; 4 if(!selectedObject) { 5 return alert("No object selected"); 6 } 7 8 // add a new child item 9 var childItem = { 10 name: "New child", 11 id: Math.random() 12 }; 13 14 governmentStore.put(childItem, { 15 overwrite: true, 16 parent: selectedObject 17 }); 18 });
我们可以使用同样的方法移除子节点。我们也可以改变对象的属性,如name。下面我们将监听双击事件弹出输入框获取一个新name:
1 tree.on("dbclick", function(object) { 2 object.name = propmt("Enter a new name for the object"); 3 governmentStore.put(object); 4 }, true);
最后,我们来看完整代码:
1 require([ 2 "dojo/aspect", 3 "dojo/json", 4 "dojo/query", 5 "dojo/store/Memory", 6 "dojo/store/Observable", 7 "dijit/Tree", 8 "dijit/form/Button", 9 "dijit/tree/ObjectStoreModel", 10 "dijit/tree/dndSource", 11 "dojo/text!./data/all.json", 12 "dojo/domReady!" 13 ], function(aspect, json, query, Memory, Observable, Tree, Button, ObjectStoreModel, dndSource, data) { 14 15 var governmentStore = new Memory({ 16 data: json.parse(data), 17 getChildren: function(object) { 18 return this.query({parent: object.id}); 19 } 20 }); 21 22 aspect.around(governmentStore, "put", function(originalPut) { 23 return function(obj, options) { 24 if(options && options.parent) { 25 obj.parent = options.parent.id; 26 } 27 return originalPut.call(governmentStore, obj, options); 28 } 29 }); 30 31 governmentStore = new Observable(governmentStore); 32 33 var model = new ObjectStoreModel({ 34 store: governmentStore, 35 query: {id: "root"} 36 }); 37 38 var tree = new Tree({ 39 model: model, 40 dndController: dndSource 41 }, "tree"); 42 tree.startup(); 43 44 var addBtn = new Button({ 45 label: "Add Child" 46 }, "add-new-child"); 47 addBtn.startup(); 48 addBtn.on("click", function() { 49 var selectedObject = tree.get("selectedItems")[0]; 50 if(!selectedObject) { 51 return alert("No object selected"); 52 } 53 54 var childItem = { 55 name: "New child", 56 id: Math.random() 57 }; 58 59 governmentStore.put(childItem, { 60 overwrite: true, 61 parent: selectedObject 62 }); 63 }); 64 65 var removeBtn = new Button({ 66 label: "Remove" 67 }, "remove"); 68 removeBtn.startup(); 69 removeBtn.on("click", function() { 70 var selectedObject = tree.get("selectedItems")[0]; 71 if(!selectedObject) { 72 return alert("No object selected"); 73 } 74 governmentStore.remove(selectedObject.id); 75 }); 76 77 tree.on("dblclick", function(object) { 78 var newName = prompt("Enter a new name for the object"); 79 if(newName) { 80 object.name = newName; 81 governmentStore.put(object); 82 } 83 }, true); 84 });
由于拖拽的原因,任一节点都可能拥有子节点,故mayHaveChildren()方法应该返回false。因此我们去掉了自定义的mayHaveChildren()方法,使用默认的。
懒加载的Tree
当数据集合很大的时候,最好是能够按需从服务端获取节点数据(也叫懒加载),这要比直接一次性载入所有数据要优良。
第一步,同样的还是构建数据。现实中,数据常常存在数据库中且由像Perservere或CouchDB这样的REST-full服务提供。为了在实例中演示,我们把数据节点分开存在服务器上不同的文件中:
data/
cabinet
congress
exec
root
然后每个节点应该有个子节点列表(如列出子节点的name,而不是子节点的子节点)。所以,为Congress准备的数据命名为“congress”大体如下:
1 { 2 "name": "Congress", 3 "id": "congress", 4 "children": [ 5 { 6 "name": "House of Representatives", 7 "id": "house" 8 9 }, 10 { 11 "name": "Senate", 12 "id": "senate" 13 } 14 ] 15 }
接下来创建我们的store。它将驱动这个Tree。在这里我们将使用JsonRest store,可以帮助实现懒加载。下面是一个连接到我们服务器的JsonRest的实例代码:
1 require(["dojo/store/JsonRest"], function(JsonRest) { 2 var usGov = new JsonRest({ 3 target: "data/congress", 4 5 getChildren: function(object) { 6 return this.get(object.id).then(function(fullObject) { 7 return fullObject.children; 8 }); 9 } 10 }); 11 });
getChildren()获取到如下的内容:
1 { 2 "name": "Congress", 3 "id": "congress", 4 "children": true 5 }
所以为了得到子节点数据,它首先需要得到名为“congress”的文件:
1 { 2 "name": "Congress", 3 "id": "congress", 4 "children": [ 5 { 6 "name": "House of Representatives", 7 "id": "house" 8 9 }, 10 { 11 "name": "Senate", 12 "id": "senate" 13 } 14 ] 15 }
下面的代码来创建模型,tree和上面的那个类似:
1 require([ 2 "dojo/store/JsonRest", 3 "dijit/Tree", 4 "dijit/tree/ObjectStoreModel", 5 "dojo/domReady!" 6 ], function(JsonRest, Tree, ObjectStoreModel) { 7 var usGov = new JsonRest({ 8 target: "http://192.168.0.87:3000/", 9 10 getChildren: function(object) { 11 return this.get(object.id).then(function(fullObject) { 12 return fullObject.children; 13 }); 14 } 15 }); 16 17 var model = new ObjectStoreModel({ 18 store: usGov, 19 getRoot: function(onItem) { 20 this.store.get("root").then(onItem); 21 }, 22 mayHaveChildren: function(object) { 23 return "children" in object; 24 } 25 }); 26 27 var tree = new Tree({ 28 model: model 29 }, "tree"); 30 tree.startup(); 31 });
请注意我们添加另一个自定义个方法getRoot()。这是因为默认的ObjectStoreModel视图从服务器执行查询类获取根节点。因为我们模拟的是静态的文件,不能相应查询,所以我们重写了getRoot()方法,也就是取了全部数据。
使用client/server store实现动态更新与拖拽
支持使用client-server store实现像JsonRest那样的拖拽与动态更新是很复杂的,且超越了本教程所覆盖的范围。捆绑有dojo的JsonRest store还不太完善,因为那只是一个客户端的组件。还需要一个服务端提供数据。
你可以使用像Persevere这样的包来提供必须要的功能。如果你想实现自己的服务端的RESTful服务,那它应该具有以下功能:
1 检索一个单独的元素
使用JsonRest,检索一个元素常使用类路径的URL方式,如下:
1 http://server/data/root
然而,当ObjectStoreModel要获取Tree的根元素的时候,它用一个查询,JsonRest发起一个GET请求:
1 http://server/data?id=cabinet
将会返回以下内容:
1 [ 2 { 3 "name": "Cabinet", 4 "id": "cabinet" 5 } 6 ]
2 检索一个元素的子元素
如:
1 http://server/data?parent=cabinet
返回父节点为“cabinet”的子元素:
1 [ 2 { 3 "name": "National Security Council", 4 "id": "security" 5 }, 6 { 7 "name": "Council of Economic Advisers", 8 "id": "economic" 9 }, 10 { 11 "name": "Office of Management and Budget", 12 "id": "budget" 13 } 14 ]
3 变更通知
4 PUT与DELETE请求
最后服务器要有处理PUT与DELETE请求的能力,用于更新,插入,删除。
总结
Tree被设计的将表现与数据模型分离,一个新的store很容易可以被层级逻辑扩展来驱动这个Tree。Tree提供了很重的功能,如键盘导航与访问。另外,Tree与store相结合增加了强大的功能,包括懒加载,拖拽,对数据变更的实时响应。我们估计你去深入研究Tree的文档,以了解它更多的功能,如样式,自定义图标,API等。