I'm trying to show a Kendo TreeView configured with remote data, but prefill the first two levels with data loaded directly from the html.
For that, I want to use the pushCreate method of the Kendo dataSource to add the initial elements to the tree:
homogeneous = new kendo.data.HierarchicalDataSource({
transport: { read: { url: serviceRoot + "/Employees", dataType: "jsonp" } },
schema: {
model: {
id: "EmployeeId",
hasChildren: "HasEmployees"
}
}
});
// Adding root
homogeneous.pushCreate({"EmployeeId":2,"FullName":"Andrew Fuller","HasEmployees":true,"ReportsTo":null});
// Adding children
homogeneous.pushCreate([
{"EmployeeId":1,"FullName":"Nancy Davolio","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":3,"FullName":"Janet Leverling","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":4,"FullName":"Margaret Peacock","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":5,"FullName":"Steven Buchanan","HasEmployees":true,"ReportsTo":2},
{"EmployeeId":8,"FullName":"Laura Callahan","HasEmployees":false,"ReportsTo":2}
]);
$("#treeview").kendoTreeView({
dataSource: homogeneous,
dataTextField: "FullName"
});
However, the children elements are added to the root level (you can see this running here).
There is a schema.model.children configuration to set the property that holds the children inside the root element and I can add all in one go, but if I use that then the dataSource stops working with remote data (you can see that running here).
So the question is, is there a way to use pushCreate to add elements as children of another one on a HierarchicalDataSource?
I found the answer while writting the question :)
It seems like if I simply use items as the key for the children, I don't need to specify the schema.model.children option so the tree still works with remote data:
homogeneous.pushCreate(
{"EmployeeId":2,"FullName":"Andrew Fuller","HasEmployees":true,"ReportsTo":null,
"items": [
{"EmployeeId":1,"FullName":"Nancy Davolio","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":3,"FullName":"Janet Leverling","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":4,"FullName":"Margaret Peacock","HasEmployees":false,"ReportsTo":2},
{"EmployeeId":5,"FullName":"Steven Buchanan","HasEmployees":true,"ReportsTo":2},
{"EmployeeId":8,"FullName":"Laura Callahan","HasEmployees":false,"ReportsTo":2}
]
}
);
Here's the working example.
I don't know if that's and unintended feature but works for adding a whole subtree to the root (which solves my problem). However the question still stands, can I use pushCreate to add a child element to another one?
You can do this by appending the node element with append. See this example by telerik: http://dojo.telerik.com/AjIti/32
Related
I'm using ag-grid (free) with Angular 1 and I've already gotten my tree data to display as desired, where the children of a node are in the column to the right of it. However, what I want to do is collapse or expand nodes on double click. Right now just focusing on getting them to collapse since my default view is set to expand. here's my code for the double click event, given within $scope.gridOptions:
onCellDoubleClicked: function(event){
event.node.expanded = false;
$scope.gridOptions.api.refreshView();
};
My assumption was that changing the expanded property to false would cause the refreshView call to re-render the grid with child nodes collapsed, but the view is unchanged after the double click.
Also, my getChildNodeDetails within gridOptions:
getNodeChildDetails: function(obj){
if (obj.children){
var nodeType = obj.breakdownCol;
return {
group: true,
expanded: obj.expanded || true,
children: obj.children,
field: 'name',
key: obj[nodeType]
}
} else {
return null;
}
}
Any ideas on how I might fix this without buying enterprise? I know that in enterprise you can group the rows and this comes with build in expand/collapse functionality.
In my own application I created a work around that simulates the row grouping feature. What it really does is adds and removes the data from the grid.
One drawback that this option has is that since the rows aren't actually in the table any filtering or sorting on columns can't actually take place on data that isn't shown, unlike the actual enterprise feature that the grid offers. However if you have disabled filtering and sorting then this option is perfectly viable.
Something like this:
function toggleExpansion(index, data) {
if (insert) {
gridOptions.api.insertItemsAtIndex(index, data);
} else {
gridOptions.api.removeItems(data)
}
}
My specific code goes into more checks and other things unrelated to this question but that is the simple explanation of what I am doing as a work around.
I am using React, but you could probably do something similar with Angular:
function expandAll(expand) {
agGridRef.current.api.forEachNode((node) => {
node.setExpanded(expand);
});
}
where the agGridRef is a reference to the component:
<AgGridReact
ref={agGridRef}
.
.
.
</AgGridReact>
I have a immutable nested tree (mori, immutable-js et al) consisting of arbitrary nodes, think file browser. The tree gets rendered by React. If a component representing a node receives focus, I'd like to:
Show an input field on the node component to change e.g. the node name.
Show a global panel that contains UI components to edit additional properties of the currently focused node, depending on the type of the selected node.
A simplistic state object could look like this:
{
root: {
type: 'folder'
name: 'Root',
focused: false,
children: [
{
type: 'text',
name: 'Essay',
focused: false
},
{
type: 'folder',
name: 'Images',
focused: false,
children: [
{
type: 'image',
name: 'Paris',
focused: true
}
]
}
]
},
currentlyFocusedNode: <Reference to Paris node>
}
Now, how would I keep a valid reference to the currently focused node? If I stored a Paris node reference at currentlyFocusedNode, it would be out of sync as soon as any other part of the app modifies the node (e.g. the inline name input from above). I thought about storing the path to the focused node, which I could use to retrieve the current reference:
currentlyFocusedNode: ['root', 'children', 0, 'children', 0]
But this seems very shaky to me as well, because even a simple node move operation on the tree could leave this path pointing to the wrong or even a non-existing node.
How are those kind of things handled with immutable data structures? Or am I not thinking "immutable" enough at all?
Your question does not have an answer as asked. And it seems you had a sense of that when you said
Or am I not thinking "immutable" enough at all?
First you say you are using an immutable data structure. Meaning it can't be changed.
Then you say:
If I stored a Paris node reference at currentlyFocusedNode, it would
be out of sync as soon as any other part of the app modifies the node
With immutable data structures things don't get modified. What you have is an old version of the data and a new version of the data. Neither will ever change.
So the question should really be something like:
How do I identify when 2 nodes represent the same data?
Answer is use an ID.
Another good question might be:
Given a reference to an old node, tree, and newData, how can I update the tree and keep track of the node?
This depends largely on how the rest of the code works and what parts you have access to. E.g. you could have something like this:
function updateNodeInTree(nodeId, newData, inTree){
oldNode = findNode(nodeId, inTree);
newNode = merge(oldNode, newData);
newTree = merge(inTree, {path: {to: newNode}});
return [newTree, newNode];
}
But if you don't have access to where the tree is updated, you may have to settle for:
newTree = updateTree(oldTree, someData);
newNode = findNode(nodeId, newTree);
Give each node a unique ID. Maintain a focused_id value.
Any component rendering parts of a node can check if
this.props.node.id === this.props.focused_id
and render appropriately.
This is where Flux architecture pattern comes in.
I would handle it like this:
Save the root and currentlyFocusedNode in a TreeStore where currentlyFocusedNode either saves null or references an object in the tree.
When a node is clicked, an event is dispatched (using AppDisptacher) with eventType: 'nodeClicked' and payload containing the object (reference) which was clicked.
TreeStore listens to AppDispatcher and makes changes to currentlyFocusedNode
React component listens to changes in TreeStore and re-renders whenever there is a change.
I am using jsTree to display my database hierarchy categories like interactive tree. Initial load is done with JSON, and by default some categories are checked. jsTree knows which categories are selected because it shows them as checked (I am using checkbox plugin), but I want to be able to open that checked nodes (and all their parents) on tree load.
I need that because, when user open a page with tree he is not aware that there might be some categories selected without expanding whole tree, and I would like to expand only those nodes that are checked.
This is my code so far:
var tree = $('.tree').bind('loaded.jstree', function (e, data) {
// I assume that logic that expand checked nodes must be placed here, after tree is loaded
})
.jstree({
// Configure JSON data plugin
'json_data': {
'data': [<this is initial json data>]
},
'checkbox': {
'override_ui': true,
'two_state': true,
'real_checkboxes': true
},
// Specify which plugins to load
'plugins': ['themes', 'json_data', 'ui', 'checkbox']
});
Thanks!
You should look at the state plugin,
You will have to set the state at the checked nodes to open.
In the plugin array add "state".
In the json of the tree, after the data of each node, add an object like so:
state: {
opened : true
}
I don't think the state plugin is required. It saves the state of the tree and reopens the tree to that state when it is next encountered.
Importantly though, #alostr is correct that if you set the data of your node to state: { opened : true } it will open that node after it is displayed.
I'm using ng-repeat on a table row with data from a JSON array retrieved from a server. My goal is to have the list update automatically whenever an item is added, removed, or modified on the server, without affecting the unmodified items. In the final implementation, these table rows will also contain bidirectionally bound <input> and <select> elements to send updates back to the server. Some of the available options in the <select> elements will also be generated using ng-repeat directives from another list that may also change.
So far, every time a new array comes from the server (currently polled every two seconds), the entire ng-repeat list is deleted and regenerated. This is problematic because it interferes with text selection, destroys input fields even if they are currently being edited by the user, and probably runs a lot more slowly than necessary.
I've written other web apps that do what I want using jQuery and DOM manipulation, but the code ends up being really hairy and development is time consuming. I'm hoping to use AngularJS and data binding to accomplish this in a fraction of the code and time.
So here's the question: is it possible to update the backing array this way, but only modify the DOM elements corresponding to the items/properties that actually changed?
Here's a minimal test case that simulates periodic polling using a hard-coded array in a timer (see it live at http://jsfiddle.net/DWrmP/). Notice that the text selection is cleared every 500ms due to the elements being deleted and recreated.
HTML
<body ng-app="myApp">
<table ng-controller="MyController">
<tr ng-repeat="item in items | orderBy:'id'">
<td>{{item.id}}</td>
<td>{{item.data}}</td>
</tr>
</table>
</body>
JavaScript
angular.module('myApp', []).controller(
'MyController', [
'$scope', '$timeout',
function($scope, $timeout) {
$scope.items = [
{ id: 0, data: 'Zero' }
];
function setData() {
$scope.items = [
{ id: 1, data: 'One' },
{ id: 2, data: 'Two' },
{ id: 5, data: 'Five' },
{ id: 4, data: 'Four' },
{ id: 3, data: 'Three' }
];
$timeout(setData, 500);
}
$timeout(setData, 500);
}
]
);
For those finding this from google, the page below describes a feature in AngularJS 1.2 that helps with this problem:
http://www.bennadel.com/blog/2556-Using-Track-By-With-ngRepeat-In-AngularJS-1-2.htm
Edit to add: The most important sentences from the linked post, in case the link ever dies:
With the new "track by" syntax, I can now tell AngularJS which object property (or property path) should be used to associate a JavaScript object with a DOM node. This means that I can swap out JavaScript objects without destroying DOM nodes so long as the "track by" association still works.
I belive this article will explain how ngRepeat works
http://www.bennadel.com/blog/2443-Rendering-DOM-Elements-With-ngRepeat-In-AngularJS.htm
So if you are keeping objects in collection - then yes ( i e $hashKey persist )
Otherwise - no
I'm planning to build the following solution myself eventually though it's still in my product backlog.
The problem with ng-repeat is it will remove items from the DOM when it needs to so for a table it would mean it will resize and such, but if the data is dynamic, it may flicker because the data changed around and the table size is shifting. Particularly during paging because the whole page may not have loaded yet.
To get around this flickering, the table must not change its number of rows. Instead have an ng-repeat of "displayed" data and just change it as needed without adding or removing items from the array.
I have a lazy loading dijit.tree which I want to reuse in many places after its data has been loaded. But if I just replace the store object in the other trees with the one which contains most data, the nodes come all expanded. I want to modify the store so that all the items are collapsed before setting it as a store in the new tree. can you tell me how to achieve this?
You cannot use the store for this, as it does not contain any information of the state of the tree nodes. This 'magic' is performed via TreeNode, see some examples here
The 'perfect solution' would be to figure out which paths you need expanded and then set the path of your tree to traverse into the wanted treenodes.
However, since your lazyloading, you need to check the state - while initializing a tree it should be UNCHECKED. However there is a cookie-functionality inbuilt which probably is kicking in, make sure to create new tree's with { persist:false }
You could also extend your tree, so that it will accept collapseChildren(TreeNode) as follows - and then call tree.set("path", [pathsArray]);
collapseChildren : function(top) {
var self = this;
if(!top || !self.model.mayHaveChildren(top.item)) return;
top.getChildren().forEach(function(n) {
if(n.item.children) {
//dojo.style(n.getParent().containerNode, { overflowY: 'hidden', overflowX: 'hidden'});
self._collapseNode(n);
self.collapseChildren(n);
}
});
},
EDIT:
If the autoExpand flag is passed to the constructor, the Tree is initially shown with all nodes expanded.
You can then call collapseAll() and expandAll() to collapse and expand the Tree, respectively.
http://livedocs.dojotoolkit.org/dijit/Tree-examples#id3