Connecting a Store toa Tree
Dojo树组件是一个强大的视觉呈现分层数据的工具。本教程中,我们将看到如何去迅速、高效的获取tree组件数据以及向下钻取到嵌套的数据。
原文链接:http://dojotoolkit.org/documentation/tutorials/1.7/store_driven_tree/
困难:中间的 Dojo版本:1.7
1. 简介
DojoTree组件提供了一个复杂的、常见的、直观的分级数据钻取展现。Tree支持分支的懒加载,使得它对于大数据集有很高的可扩展性。当数据有父子关系时,Tree是一个很有用的widget。
在这里,我们将会学习如何使用新的Dojo对象树存储接口,去迅速构造数据驱动的树结构。在本指南里,为了易于打开章节和折叠我们不使用的章节,我们将会使用一个提供US政府结构的数据源信息并在一个树里显示信息。我们将会从头开始,创建一个简单的对象存储。用带有懒加载、可拖拽的的数据驱动树来结束,并且实时回应数据的变化。
2. 树与静态存储
一个静态存储很适合于固定大小不能动态改变的树。在这个例子中,单击树节点显示一个相关的图像。
第一步是创建数据。数据源是json编码的数据,可以包含支持信息。在这个例子中,名称用于标签树的每个节点。这棵树有三个节点,每一个都有名称和id。
2.1. 数据源
2. "label":"name",
3. "name": "USGovernment",
4. "id": "root",
5. "items": [
6. {
7. "name":"Congress",
8. "id":"congress",
9. },
10. {
11. "name":"Executive",
12. "id":"exec",
13. },
14. {
15. "name":"Judicial",
16. "id":"judicial"
17. }
18. ]
19.}
2.2. Tree
代码读取存储数据,应用数据模型,并将其分配给树小部件。onLoad和onClick事件被用于显示相关的图像。
20.dojo.require("dojo.parser");
21.dojo.require("dojo.data.ItemFileReadStore");
22.dojo.require("dijit.Tree");
23.dojo.require("dijit.tree.ForestStoreModel");
24.
25.// when dojo is loaded and ready
26.dojo.ready(function(){
27.
28. // set up the store to get the tree data
29. var governmentStore = newdojo.data.ItemFileReadStore({
30. url: "data/static"
31. });
32.
33. // set up the model, assigninggovernmentStore
34. var governmentModel = newdijit.tree.ForestStoreModel({
35. store: governmentStore,
36. query: {"id":"*"},
37. rootId: "root",
38. rootLabel: "USGovernment",
39. childrenAttrs: "items"
40. });
41.
42. // set up the tree, assigning governmentModel
43. var governmentTree = new dijit.Tree({
44. model: governmentModel,
45. onOpenClick: true,
46. onLoad: function(){
47. dojo.byId('image').src= '../resources/images/root.jpg';
48. },
49. onClick: function(item){
50. dojo.byId('image').src= '../resources/images/'+item.id+'.jpg';
51. }
52. },"divTree");
注意,我们使用一个ForestStoreModel,它允许多个根节点,所以树或数据可以更方便地从不同的层面获取。
3. 树和对象存储
这棵树支持延迟加载分支机构,使其高度可伸缩的大型数据集,这里我们将学习如何使用新的Dojo对象存储接,快速构建树的数据驱动。在这个例子中,我们将使用一个美国政府结构的数据信息。我们要从头开始创建一个简单的对象存储,和一个数据驱动的树懒加载,拖放,实时响应数据的变化。
3.1. Store
我们将首先创建数据源.这将是tree的数据驱动。在这里我们将要使用JsonRest数据源,这有助于延迟加载数据。在这个示例中,我们将展示美国政府的层次结构。这是JsonRest store基本的实例化,用于连接到我们的服务器,以便数据检索:
54.require(["dojo/store/JsonRest"],function(JsonRest) {
55. usGov = new JsonRest({
56. target:"data/"
57. });
3.2. 添加基本的数据模型
我们将使用我们的数据作为树的存储模型。为了做到这一点,我们还需要定义模型逻辑层次结构,描述在我们的数据。这棵树需要五个模型方法来呈现数据:
u getIdentity(object)
– 数据源已经提供,通常不需要重新实现。
u mayHaveChildren(object)
– 标记一个对象是否可能有子节点(在加载子节点以前)。在这个例子中我们根据children的属性来标记是否有子节点。
u getChildren(parent, onComplete, onError)-检索子节点。在这个例子中,我们将执行一个get()来检索父对象来获取的孩子。一旦父节点完全被加载,我们返回子节点,以数组形式。
u getRoot(onItem, onError)-调用来检索的根节点。 onItem回调应该被称为根对象。
u getLabel(object)——返回标签对象。在这个例子中,标签只是name属性的对象
现在,让我们看看如何实现定义的数据结构的层次结构, 我们可以非常容易做到这一点,通过JsonRest实例化中定义方法:
59.usGov = JsonRest({
60. target:"data/",
61. mayHaveChildren:function(object){
62. // see ifit has a children property
63. return"children" in object;
64. },
65. getChildren: function(object,onComplete, onError){
66. //retrieve the full copy of the object
67. this.get(object.id).then(function(fullObject){
68. //copy to the original object so it has the children array as well.
69. object.children= fullObject.children;
70. //now that we have the full object, we should have an array of children
71. onComplete(fullObject.children);
72. },function(error){
73. //an error occurred, log it, and indicate no children
74. console.error(error);
75. onComplete([]);
76. });
77. },
78. getRoot: function(onItem,onError){
79. // getthe root object, we will do a get() and callback the result
80. this.get("root").then(onItem,onError);
81. },
82. getLabel: function(object){
83. // justget the name
84. returnobject.name;
85. }
3.3. 创建树与数据源作为数据模型
现在我们可以很容易地填补这个存储进我们的树。
87.require(["dijit/Tree"], function(Tree) {
88. tree = new Tree({ // create atree
89. model:usGov // give it the model
90. },"tree"); // target HTML element's id
91. tree.startup();
92.});
当这个树 startup时,它将查询我们的模型/存储。它会询问数据源的标签(通过getLabel()),和子节点们(通过调用getChildren())。对于每个孩子,它将呈现标签和添加一个扩展器图标,如果对象可能有孩子(通过mayHaveChildren())。我们的getChildren()和getRoot()函数委托给get()调用,这触发请求到服务器(使用数据源的target,连接id传递给get(),如get请求的URL)。服务器响应这些请求与JSON来满足模型和树。这是它的样子。
4. 延迟加载
利用延迟加载,当装载一个对象以及它的节点时,我们的服务器提供了每个节点的对象。然而,对于每个孩子只有”name”属性(标签),“id“属性(确定对象),和一个boolean属性”children”(如果为true代表有子节点,反之无)。这种方法延迟加载确保只有一个请求是需要每次一个节点被扩展(而不是一个请求的每个子节点扩展节点)。这就是我们的服务器返回的“根”对象(得到数据/ root):
93. {
94. "name": "USGovernment",
95. "id": "root",
96. "children": [
97. {
98. "name":"Congress",
99. "id":"congress",
100. "children":true
101. },
102. {
103. "name":"Executive",
104. "id":"exec",
105. "children":true
106. },
107. {
108. "name":"Judicial",
109. "id":"judicial"
110. }
112. }
然后,当我们点击扩展一个节点时,树将请求目标对象的子节点。如果我们点击Executive 节点,store将使用目标对象的id(“exec)请求完整的对象,触发请求Get data/ exec。然后服务器响应:
113. {
114. "name":"Executive",
115. "id": "exec",
116. "children": [
117. {
118. "name":"President",
119. "id":"pres"
120. },
121. {
122. "name":"Vice President",
123. "id":"vice-pres"
124. },
125. {
126. "name":"Secretary of State",
127. "id":"state"
128. },
129. {
130. "name":"Cabinet",
131. "id":"cabinet",
132. "children":true
133. }
134. ]
135. }
在这个相应里面,你可以看到只有Cabinet有子节点。
4.1. 用户修改树
树小部件对拖放修改具有良好的支持。如果我们想通过拖放修改我们的数据,我们可以实现pasteItem()方法并设置树拖放控制器即可。首先,让我们实现pasteItem()。当拖放操作发生时,这个方法被调用。这个pasteItem()方法被调用时包含以下几个参数:
u child – 这个子节点是被粘贴,拖放的。
u oldParent – 子节点被脱离的父节点。
u newParent – 子节点被拖放到的位置,该位置就是该子节点的新父节点。
u bCopy - 表示如果孩子应该被复制而不是移动。
u insertIndex – 被拖放的节点(1)拖放到别的节点(2)中,则2节点变成了新的父节点(如果存储支持排序)
实现 pasteItem()的基本方法是非常简单的。在我们的示例中,我们只是想删除子对象从oldParent数组并添加子对象数组作为newParent的子对象。我们可以通过寻找孩子的索引在oldParentChildren数组,使用splice()来删除它,然后使用splice()将其放置在newParent中。然后我们调用put()为每个父对象来保存修改。
然而, ,我们也需要考虑一些并发症。首先,父对象可能会或可能不会完全下载对象。在我们的延迟加载方案中,只有完整对象有孩子数组。因此,我们将对每个父节点执行一个get(),以确保我们有完整的对象。其次,因为有可能是替代对象的副本,我们不能做直接的indexOf()调用孩子数组,找到子对象,所以我们需要扫描阵列相匹配的ID找到一个对象.
考虑到这些因素,我们可以创建我们的pasteItem()实现;
136. usGov = new JsonRest({
137. pasteItem: function(child,oldParent, newParent, bCopy, insertIndex){
138. // makethis store available in all the inner functions
139. var store= this;
140. // getthe full oldParent object
141. store.get(oldParent.id).then(function(oldParent){
142. //get the full newParent object
143. returnstore.get(newParent.id)
144. }).then(function(newParent){
145. //get the oldParent's children and scan through it find the child object
146. varoldChildren = oldParent.children;
147. dojo.some(oldChildren,function(oldChild, i){
148. //it matches if the ids match
149. if(oldChild.id== child.id){
150. //found the child, now remove it from the children array
151. oldChildren.splice(i,1);
152. returntrue; // done, break out of the some() loop
153. }
154. });
155. //do a put to save the oldParent with the modified childrens array
156. store.put(oldParent);
157. //now insert the child object into the new parent,
158. //using the insertIndex if available
159. newParent.children.splice(insertIndex|| 0, 0, child);
160. //save changes to the newParent
161. store.put(newParent);
162. },function(error){
163. //catch and report any errors
164. alert("Erroroccurred (this demo is not hooked up to a real database, so this is expected):" + error);
165. });
166. });
167. },
168. ...
4.2. 为树配置拖放
当我们定义拖放树的时候,需要时使用标准的dijit/tree/dndSource作为控制器:
169. require(["dijit/Tree","dijit/tree/dndSource", "dojo/domReady!"], function(Tree,dndSource) {
170. tree = new Tree({
171. model:usGov,
172. // definethe drag-n-drop controller
173. dndController:dndSource
174. }, "tree");
175. tree.startup();
176. });
现在当我们进行拖放操作的时候会触发并且实现pasteItem()方法,然后引起子节点数组被修改和保存。在JsonRest store中,这些修改保存调用put()将会触发Http Put请求去保存数据到服务。
4.3. Notifications
我们需要通知树子节点的变化。树遵循标准的MVC数据模型变化,而不是控制器的操作原则。这是非常强大的,因为无论是什么引起的变化,视图中的数据都会做出反应(直接编程,拖放等)。这棵树侦听“onChildrenChange”、“onChange”,和“onDelete”事件。Store API规定数据更新发生通过它的put()方法。我们可以扩展put()调用这些模型改变方法(触发树事件),然后调用原始的put()方法来完成行为上的存储。同样我们可以调用onDelete事件的remove()方法:
177. usGov = new JsonRest({
178. put: function(object, options){
179. // firethe onChildrenChange event
180. this.onChildrenChange(object,object.children);
181. // firethe onChange event
182. this.onChange(object);
183. //execute the default action
184. return JsonRest.prototype.put.apply(this,arguments);
185. },
186. remove: function(id){
187. // Wecall onDelete to signal to the tree to remove the child. The
188. //remove(id) gets and id, but onDelete expects an object, so we create
189. // a fakeobject that has an identity matching the id of the object we
190. // areremoving.
191. this.onDelete({id:id});
192. // notethat you could alternately wait for this inherited add function to
193. // finish(using .then()) if you don't want the event to fire until it is
194. // confirmedby the server
195. },
196. // we also add event stubs sothese methods can be
197. // called before the listenersare applied
198. onChildrenChange:function(parent, children){
199. // firedwhen the set of children for an object changes
200. },
201. onChange: function(object){
202. // firedwhen the properties of an object change
203. },
204. onDelete: function(object){
205. // firedwhen an object is deleted
206. },
207. ...
我们现在已经定义了我们的数据模型的方法,我们可以使用存储与树拖放。我们可以查看教程演示,但是请注意,这个演示没有实现任何响应HTTP PUT请求。演示是静态文件,所以没有什么是真的改变了。如果你做多个拖放操作你会看到对象出现在老地方,因为服务器是不断地回应相同的静态数据。
4.4. 编程数据的变化
前面讲过,the Tree/model interface is designed so thatthe Tree responds to changes regardless of thetrigger. 因此,添加一个新的子节点,我们可以简单地插入一个子对象到父节点的孩子数组,保存它调用put(),这棵树会自动响应。在这个演示中,一个按钮触发器添加一个子对象使用以下代码
208. // get the selected object from the tree
209. var selectedObject =tree.get("selectedItems")[0];
210. // check ensure an object is selected
211. if(!selectedObject){
212. // alert and return if noselected object
213. return alert("No objectselected");
214. }
215. // get the full copy of the object
216. usGov.get(selectedObject.id).then(function(selectedObject){
217. // add a new child
218. selectedObject.children.push({
219. name:"New child",
220. id:Math.random()
221. });
222. // save it with a put(). The tree will automatically update the UI
223. usGov.put(selectedObject);
224. });
而且,我们可以用同样的方法把节点删除。我们也可以改变对象的属性,比如名称(标签的节点)。在这个演示中,我们倾听双击提示输入一个新名字,对象:
225. tree.on("dblclick", function(object){
226. // node was double clicked,prompt for a new name
227. object.name = prompt("Entera new name for the object");
228. // save the change, again thetree auto-updates
229. usGov.put(object);
230. }, true);
最后,我们最终得到以下:
231. require(["dojo/store/JsonRest","dojo/store/Observable", "dijit/Tree","dijit/tree/dndSource", "dojo/query","dojo/domReady!"],
232. function(JsonRest, Observable,Tree, dndSource, query) {
233.
234. usGov =JsonRest({
235. target:"data/",
236. mayHaveChildren:function(object){
237. //see if it has a children property
238. return"children" in object;
239. },
240. getChildren:function(object, onComplete, onError){
241. //retrieve the full copy of the object
242. this.get(object.id).then(function(fullObject){
243. //copy to the original object so it has the children array as well.
244. object.children= fullObject.children;
245. //now that we have the full object, we should have an array of children
246. onComplete(fullObject.children);
247. },function(error){
248. //an error occurred, log it, and indicate no children
249. console.error(error);
250. onComplete([]);
251. });
252. },
253. getRoot:function(onItem, onError){
254. //get the root object, we will do a get() and callback the result
255. this.get("root").then(onItem,onError);
256. },
257. getLabel:function(object){
258. //just get the name
259. returnobject.name;
260. },
261.
262. pasteItem:function(child, oldParent, newParent, bCopy, insertIndex){
263. varstore = this;
264. store.get(oldParent.id).then(function(oldParent){
265. store.get(newParent.id).then(function(newParent){
266. varoldChildren = oldParent.children;
267. dojo.some(oldChildren,function(oldChild, i){
268. if(oldChild.id== child.id){
269. oldChildren.splice(i,1);
270. returntrue; // done
271. }
272. });
273. store.put(oldParent);
274. newParent.children.splice(insertIndex|| 0, 0, child);
275. store.put(newParent);
276. },function(error){
277. alert("Erroroccurred (this demo is not hooked up to a real database, so this is expected):" + error);
278. });
279. });
280. },
281. put:function(object, options){
282. this.onChildrenChange(object,object.children);
283. this.onChange(object);
284. returnJsonRest.prototype.put.apply(this, arguments);
285. },
286. remove:function(id){
287. this.onDelete({id:id});
288. returnJsonRest.prototype.remove.apply(this, arguments);
289. }
290. });
291. tree =new Tree({
292. model:usGov,
293. dndController:dndSource
294. },"tree"); // make sure you have a target HTML element with this id
295. tree.startup();
296. query("#add-new-child").on("click",function(){
297. varselectedObject = tree.get("selectedItems")[0];
298. if(!selectedObject){
299. returnalert("No object selected");
300. }
301. usGov.get(selectedObject.id).then(function(selectedObject){
302. selectedObject.children.push({
303. name:"New child",
304. id:Math.random()
305. });
306. usGov.put(selectedObject);
307. });
308.
309. });
310. query("#remove").on("click",function(){
311. varselectedObject = tree.get("selectedItems")[0];
312. if(!selectedObject){
313. returnalert("No object selected");
314. }
315. usGov.remove(selectedObject.id);
316. });
317. tree.on("dblclick",function(object){
318. object.name= prompt("Enter a new name for the object");
319. usGov.put(object);
320. }, true);
321. });