`

Connecting a Store to a Tree

 
阅读更多

对于可视化分级数据的展现,Dojo  Tree组件是一个强大的工具。在本指南里,对于迅速和有效的数据钻取到嵌套数据,我们将会考虑如何如何连接tree到数据存储。

 

Tree 和存储

Dojo Tree组件提供了一个复杂的、常见的、直观的分级数据钻取展现。Tree支持分支的懒加载,使得它对于大数据集有很高的可扩展性。当数据有父子关系时,Tree是一个很有用的widget。

在这里,我们将会学习如何使用新的Dojo对象树存储接口,去迅速构造数据驱动的树结构。在本指南里,为了易于打开章节和折叠我们不使用的章节,我们将会使用一个提供US政府结构的数据源信息并在一个树里显示信息。我们将会从头开始,创建一个简单的对象存储。用带有懒加载、可拖拽的的数据驱动树来结束,并且实时回应数据的变化。

Start with a Store

我们通过创建数据源开始。这个将会是驱动存储树。这里我们将使用JsonRest存储。这个存储使得易于使用数据的懒加载。在这个例子里,我们将展现US政府的层级。这儿是JsonRest存储的一个基本实例,它连接我们的服务器,以至于数据可以通过RESTful重新取回:

 

require(["dojo/store/JsonRest"], function(JsonRest) {
    usGov = new JsonRest({
        target:"data/"
    });

});

 

增加基本数据模型方法

我们将使用我们的存储作为Tree的数据模型。为了做这个,我们也需要去定义模型逻辑,用于描述我们数据的层级。这个Tree需要5个模型方法来呈现数据作为树。

 

 

  • getIdentity(object) - Already provided by the store, and doesn't usually need to be reimplemented.
  • mayHaveChildren(object) - Indicates whether or not an object may have children (prior to actually loading the children). In this example, we will treat the presence of a "children" property as the indication of having children.
  • getChildren(parent, onComplete, onError) - Called to retrieve the children. This may execute asynchronously and should call the onComplete callback when finished. In this example, we will do a get() to retrieve the full representation of the parent object to get the children. Once the parent is fully loaded, we return the "children" array from the parent.
  • getRoot(onItem) - Called to retrieve the root node. The onItem callback should be called with the root object. In this example, we get() the object with the id/URL of "root" for the root object.
  • getLabel(object) - Returns the label for the object (this is the text that is displayed next to the node in the tree). In this example, the label is just the "name" property of the object.

 

现在,让我们来看看如何实现我们数据结构的层级定义。通过在JsonRest实例里定义方法,我们可以比较容易的做到:

 

usGov = JsonRest({
    target:"data/",
    mayHaveChildren: function(object){
        // see if it has a children property
        return "children" in object;
    },
    getChildren: function(object, onComplete, onError){
        // retrieve the full copy of the object
        this.get(object.id).then(function(fullObject){
            // copy to the original object so it has the children array as well.
            object.children = fullObject.children;
            // now that full object, we should have an array of children
            onComplete(fullObject.children);
        }, function(error){
            // an error occurred, log it, and indicate no children
            console.error(error);
            onComplete([]);
        });
    },
    getRoot: function(onItem, onError){
        // get the root object, we will do a get() and callback the result
        this.get("root").then(onItem, onError);
    },
    getLabel: function(object){
        // just get the name
        return object.name;
    }
});

 

用我们的存贮作为数据模型创建Tree

现在,我们可以很容易的把这个存储插入到我们的树中:

 

require(["dijit/Tree"], function(Tree) {
    tree = new Tree({ // create a tree
            model: usGov // give it the model
        }, "tree"); // target HTML element's id
    tree.startup();
});

 

 

当Tree启动之后,它将会查询我们的模型/存储的根对象。它将会向存储请求label(通过getLabel())并且得到children。对于每一个child,它将会提供label,如果这个对象可能有children,则增加一个扩展图标。我们的getChildren() 和 getRoot() 函数 委派给 get() 调用,它触发请求到server (using the store's target + the get(id) argument as the URL for a GET). 服务器用json来回应这些请求去满足模型和Tree。看起来像这样:

 延迟加载

 

为了利用延迟加载的优势,当加载带有孩子的对象时,我们的服务器提供了对象的每一个孩子,但是仅仅在孩子中包含了足够的数据以提供它。这个请求对象是对象的一个“全”展现。然而,对于每一个孩子,仅有name属性、id属性和一个children属性的逻辑值(用于指出是否有孩子),这些孩子对象时有效的部分呈现。延迟加载的这种方法确保了在一个节点展开时,每一次只需要一次请求(而不是每一个展开节点的每一个孩子节点一个请求)。这是我们服务器返回的“root”对象(GET data/root):

 

{
    "name": "US Government",
    "id": "root",
    "children": [
        {
            "name": "Congress",
            "id": "congress",
            "children": true
        },
        {
            "name": "Executive",
            "id": "exec",
            "children": true
        },
        {
            "name": "Judicial",
            "id": "judicial"
        }
    ]

}

 

 

 

然后,当单击展开一个节点时,Tree将会请求目标对象孩子。这被翻译成父对象全呈现的一个请求。如果我们单击Executive节点,这个存储将会使用目标对象id,并请求完全的"exec"对象,触发请求 GET data/exec。服务器用如下的进行回应:

 

{
    "name": "Executive",
    "id": "exec",
    "children": [
        {
            "name": "President",
            "id": "pres"
        },
        {
            "name": "Vice President",
            "id": "vice-pres"
        },
        {
            "name": "Secretary of State",
            "id": "state"
        },
        {
            "name": "Cabinet",
            "id": "cabinet",
            "children": true
        }
    ]
}

 

在这个回应里,你可以看到仅有Cabinet对象也许有孩子。

Tree的用户修改

基于Tree的结构层级的修改,对于drag-n-drop,Tree widget有优秀的支持。如果我们想通过拖拽,允许修改我们的数据,我们可以实现pasteItem()方法和对树设置拖拽控制器。首先,让我们实现pasteItem().当一个拖拽操作发生时,这个方法被调用。pasteItem方法调用的参数如下:

 

  • child - The child object that is being pasted.
  • oldParent - The parent object where the child was dragged from.
  • newParent - The new parent of the child object, where the child was dragged to.
  • bCopy - Indicates if the child should be copied (instead of moved).
  • insertIndex - The index of where the child should be placed in the lists of children for the new parent (if the store supports ordering of children).

实现pasteItem的基本方式是简单的。在我们的例子里,我们只是简单的从oldParent孩子数组中移除对象,并且把这个孩子对象添加到newParent孩子数组中。我们通过在oldParent的孩子数组中查找索引来做这个,用splice()去移除它,然后用splice()去把它放置在newParent孩子数组的正确的索引里。我们然后d对于这些父对象中的每一个调用put()去保存修改。然而我们也需要考虑几个并发症。首先,First, the parent objects may or may not be fully downloaded objects. With our lazy loading scheme, only full object's have the children array. Therefore, we will do a get() on each of the parent's to ensure we have the full object. Next, because there may be alternate copies of objects, we can't do a directindexOf() call to find the child object in the children, so we need to scan the children to find an object with a matching id. With these considerations in mind, we can craft our pasteItem()implementation:

 

usGov = new JsonRest({
    pasteItem: function(child, oldParent, newParent, bCopy, insertIndex){
        // make the this store available in all the inner functions
        var store = this;
        // get the full oldParent object
        store.get(oldParent.id).then(function(oldParent){
            // get the full newParent object
            store.get(newParent.id).then(function(newParent){
                // get the oldParent's children and scan through it find the child object
                var oldChildren = oldParent.children;
                dojo.some(oldChildren, function(oldChild, i){
                    // it matches if the id's match
                    if(oldChild.id == child.id){
                        // found the child, now remove it from the children array
                        oldChildren.splice(i, 1);
                        return true; // done, break out of the some() loop
                    }
                });
                // do a put to save the oldParent with the modified children's array
                store.put(oldParent);
                // now insert the child object into the new parent,
                //using the insertIndex if available
                newParent.children.splice(insertIndex || 0, 0, child);
                // save changes to the newParent
                store.put(newParent);
            });
        });

    },

 

配置树的拖拽

 

 

We then need to define the drag-n-drop controller for the Tree as well. We will use the standarddijit/tree/dndSource as the controller:

 

require(["dijit/Tree", "dijit/tree/dndSource", "dojo/domReady!"], function(Tree, dndSource) {
    tree = new Tree({
        model: usGov,
        // define the drag-n-drop controller
        dndController: dndSource
    }, "tree");
});

 

 

Now drag-n-drop operations should trigger our pasteItem() implementation and cause children arrays to be modified and saved. With the JsonRest store, the modifications that are saved via put() will trigger HTTP PUT requests to save the data back to the server.

通知

 

We aren't quite done yet. We need to also notify the Tree of the changes in the children. The Treefollows standard MVC principles of responding to data model changes rather than controller actions. This is extremely powerful because the view of the data can respond to changes regardless of what triggered the change (direct programmatic changes, drag-n-drop, etc.). The Tree listens for the "onChildrenChange", "onChange", and "onDelete" events. The Store API dictates that data updates happen via its put() method. So we can extend put() to call these model change methods (triggering the Tree events), and then call the original put() method to complete the action on the store. Likewise we can call the onDelete event in the remove() method:

 

usGov = new JsonRest({
    put: function(object, options){
        // fire the onChildrenChange event
        this.onChildrenChange(object, object.children);
        // fire the onChange event
        this.onChange(object);
        // execute the default action
        return JsonRest.prototype.put.apply(this, arguments);
    },
    remove: function(id){
        // We call onDelete to signal to the tree to remove the child. The
        // remove(id) gets and id, but onDelete expects an object, so we create
        // a fake object that has an identity matching the id of the object we
        // are removing.
        this.onDelete({id: id});
        // note that you could alternately wait for this inherited add function to
        // finish (using .then()) if you don't want the event to fire until it is
        // confirmed by the server
    },
    // we can also put event stubs so these methods can be
    // called before the listeners are applied
    onChildrenChange: function(parent, children){
        // fired when the set of children for an object change
    },
    onChange: function(object){
        // fired when the properties of an object change
    },
    onDelete: function(object){
        // fired when an object is deleted
    },
    ...

 

 

编程数据更改

 

As we mentioned before, the Tree/model interface is designed so that the Tree responds to changes regardless of the trigger. Consequently to add a new child, we can simply insert a child object into a parent's children array, save it with a put(), and the Tree will automatically respond. In the demo, a button triggers the addition of a child object using the following code:

 

// get the selected object from the tree
var selectedObject = tree.get("selectedItems")[0];
// get the full copy of the object
usGov.get(selectedObject.id).then(function(selectedObject){
    // add a new child
    selectedObject.children.push({
        name: "New child",
        id: "new-child-id"
    });
    // save it with a put(). The tree will automatically update the UI
    usGov.put(selectedObject);

});

 

And, we can remove children with the same approach. We could also change properties of objects, such as the name (the label of the nodes). In the demo, we listen for double-clicks to prompt for a new name for objects:

 

tree.on("dblclick", function(object){
    // node was double clicked, prompt for a new name
    object.name = prompt("Enter a new name for the object");
    // save the change, again the tree auto-updates
    usGov.put(object);
}, true);
require(["dojo/store/JsonRest", "dojo/store/Observable", "dijit/Tree", "dijit/tree/dndSource", "dojo/query", "dojo/domReady!"],
    function(JsonRest, Observable, Tree, dndSource, query) {
     
        usGov = JsonRest({
            target:"data/",
            mayHaveChildren: function(object){
                // see if it has a children property
                return "children" in object;
            },
            getChildren: function(object, onComplete, onError){
                // retrieve the full copy of the object
                this.get(object.id).then(function(fullObject){
                    // copy to the original object so it has the children array as well.
                    object.children = fullObject.children;
                    // now that full object, we should have an array of children
                    onComplete(fullObject.children);
                }, function(error){
                    // an error occurred, log it, and indicate no children
                    console.error(error);
                    onComplete([]);
                });
            },
            getRoot: function(onItem, onError){
                // get the root object, we will do a get() and callback the result
                this.get("root").then(onItem, onError);
            },
            getLabel: function(object){
                // just get the name
                return object.name;
            },
         
            pasteItem: function(child, oldParent, newParent, bCopy, insertIndex){
                var store = this;
                store.get(oldParent.id).then(function(oldParent){
                    store.get(newParent.id).then(function(newParent){
                        var oldChildren = oldParent.children;
                        dojo.some(oldChildren, function(oldChild, i){
                            if(oldChild.id == child.id){
                                oldChildren.splice(i, 1);
                                return true; // done
                            }
                        });
                        store.put(oldParent);
                        newParent.children.splice(insertIndex || 0, 0, child);
                        store.put(newParent);
                    }, function(error){
                        alert("Error occurred (this demo is not hooked up to a real database, so this is expected): " + error);
                    });
                });
            },
            put: function(object, options){
                this.onChildrenChange(object, object.children);
                this.onChange(object);
                return JsonRest.prototype.put.apply(this, arguments);
            },
            remove: function(id){
                this.onDelete({id: id});
                return JsonRest.prototype.remove.apply(this, arguments);
            }
        });
        tree = new Tree({
            model: usGov,
            dndController: dndSource
        }, "tree"); // make sure you have a target HTML element with this id
        tree.startup();
        query("#add-new-child").on("click", function(){
            var selectedObject = tree.get("selectedItems")[0];
            if(!selectedObject){
                return alert("No object selected");
            }
            usGov.get(selectedObject.id).then(function(selectedObject){
                selectedObject.children.push({
                    name: "New child",
                    id: Math.random()
                });
                usGov.put(selectedObject);
            });
         
        });
        query("#remove").on("click", function(){
            var selectedObject = tree.get("selectedItems")[0];
            if(!selectedObject){
                return alert("No object selected");
            }
            usGov.remove(selectedObject.id);
        });
        tree.on("dblclick", function(object){
            object.name = prompt("Enter a new name for the object");
            usGov.put(object);
        }, true);
});

 

 

结论

The Tree is designed to properly separate the data model concerns from presentation, and the new object store can easily be extended with application hierarchical logic to drive the Tree. The Treeprovides important features such as keyboard navigation and accessibility. Also, the Tree and object store combination leverages the additional powerful functionality of the Tree including scalable lazy loading, drag-n-drop, and real-time response to data model changes. We encourage you to explore the Tree documentation in more depth to learn more about the Tree capabilities such styling, icon customization, and its API.

 

  • 大小: 12.4 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics