Lazy loading Wijmo tree child nodes - lazy-loading

How can I lazy load the child nodes of a Wijtree? Means initially only the top-level parent nodes will be loaded by making an AJAX server call ($.getJSON() method). When user expands a parent node (nodeExpanded event), then its child nodes will be loaded by making another AJAX server call to get the child nodes for the expanded parent node. The purpose for such lazy loading is better performance in rendering the tree.
Sample code will be highly appreciated.

I have been able to implement lazy loading in Wijmo tree in the following way:
Only the tree nodes at the top-most hierarchy level are first loaded into the Wijmo tree. However, as none of the nodes will have a child node, the node expand icon will not appear at the left of the nodes and hence nodes cannot be expanded. To solve this issue, a “dummy” child node is added to each top level node, so that they now become parent nodes and hence node expand icon appears which can be clicked to reveal the child nodes. On expanding a parent node, the “dummy” child node is removed and actual child nodes are loaded beneath the parent node. This strategy can be implemented for any number of nested child nodes which have sub-child nodes.
Here is my sample code using AngularJS, but this can be implemented using jQuery as well, following the same logic.
View code:
<div data-ng-controller="DomainCtrl">
<div id="domainEntitiesTreeView">
<wij-tree id="wijtree" nodes="treeList">
<ul>
</ul>
</wij-tree>
</div>
</div>
AngularJS Controller code:
//Global array to store domain entity ids
var domainEntityIdList = [];
var REST_SERVICE_URL = 'http://DevServer1/MyApplication/Api/DomainEntities';
function DomainCtrl($scope, $http) {
var domainEntityList = [];
//AJAX call to the RESTful service to get list of domain entities
$http({ method: 'GET', url: REST_SERVICE_URL })
.success(function (data) {
$(data).each(function (i, val) {
var domainEntity = data[i];
var domainEntityId = domainEntity.Id;
//Keep the domain entity ids in array for later use
domainEntityIdList.push(domainEntityId);
var domainEntityName = domainEntity.Name;
var treeNodes = [{ text: "" }]; //dummy child node
domainEntityList.push({
text: domainEntityName,
nodes: treeNodes // dummy child node added to each node
});
})
//Model for the tree
$scope.treeList = domainEntityList;
})
.error(function (data, status) {
alert('Unable to load data: ' + status);
});
}
Each parent node here (representing a domain entity) needs to be assigned the corresponding domain entity id, so that on expanding the node, we can get and pass the domain entity id as a parameter to a RESTful service which returns the list of sub domain entities for the given domain entity id. These sub domain entities are then added as child nodes to the expanded node. Code below:
Code for on-demand child nodes loading:
$(document).on("ready", function (e) {
$("#wijtree").wijtree({
autoCollapse: true,
showExpandCollapse: true,
//nodeExpanded event is triggered on expanding a domain entity node
nodeExpanded: function (e, data) {
AssignNodeId();
LoadChildNodesOnDemand(e, data);
}
});
});
function AssignNodeId() {
//Assign each node's (li) id attribute to domain entity id
$("#wijtree li").each(function (index) {
$(this).attr("id", domainEntityIdList[index]);
});
}
function LoadChildNodesOnDemand(e, data) {
var node = data.element;
//First remove the dummy child node that was added in each node of the tree
var nodes = node.wijtreenode("getNodes");
$(nodes).each(function () {
node.wijtreenode("remove", 0);
});
var domainEntId = node.attr("id");
//RESTful service URL for getting the sub domain entities for a given domain entity id. In production-ready application, the URL should not be hardcoded and should come from constant file
var SUBDOMAIN_SERVICE_URL = 'http://DevServer1/MyApplication/Api/SubDomainEntities?domainEntityId=' + domainEntId;
//AJAX call to the RESTful service to get list of sub domain entities
$.ajax({
url: SUBDOMAIN_SERVICE_URL,
success: function (data) {
$(data).each(function (index, val) {
var subDomainEntity = data[index];
//Add the subentity as a child node
node.wijtreenode("add", subDomainEntity.Name, index);
//Get the newly added child node and assign its id to Sub Domain Entity Id
var childNode = nodes[index].element;
childNode.attr("id", subDomainEntity.Id);
});
},
error: function (data, status) {
alert('Unable to load data: ' + status);
}
});
}
Do let me know if it helps or if you have any questions.

Related

Q-Tree How to add checkbox for all nodes

I have dynamic data which is fetched from server. I'm using Vue.js and Q-Tree by Quasar framework.
When the data is static the checkbox (tick-strategy = 'leaf') works.
But when data is fetched from server I cannot understand why the checkbox is not shown, and one more interesting thing - the checkbox will be visible when I expand level until leaf
the code is a standard:
In mount life circle I have promise which fetch the data.
data () {
return {
allData: []
}
}
mount () {
Promise.all(....)
.then (data => { this.allData = data;})
}
So and now in template I have:
<q-tree
:nodes="allData"
node-key="id"
#lazy-load...
:tick-strategy="leaf"
:ticked.sync="ticked"
...
So, now the nodes are shown without checkbox, and appears until expand to leaf.

Vuetify Tree-view --- Get parent node along with the children node

when I am trying to select a node in the Vuetify tree-view in leaf mode, I am getting only the leaf nodes in the v-model. Is there a way that I could get all the children node along with the selected parent node.
Vuetify Version: 2.2.18
Link to the code: https://codepen.io/pen/?&editable=true&editors=101
Result After selection:
Actual Result:
Child #1
Child #2
Grandchild #1
Grandchild #2
Expected Result:
Root
Child #1
Child #2
Child #3
Grandchild #1
Grandchild #2
The problem is that vuetify removes the parent nodes, as these contain all the child nodes. One solution is to build a computed flattened copy of the items that contains references to the parent nodes.
This can then be recursively looped through via a second computed property (_selection) which adds the parents of the selected items.
working example: https://codepen.io/ellisdod/pen/MWwqYBB?editors=1010
computed : {
_items () {
const replaceChildren = (obj,parent) => {
const clone = Object.assign({},obj)
delete clone.children
if (parent) clone.parent = parent
return clone
}
const addItems = (arr,parent) => {
const items = arr.reduce((acc,x)=>{
acc.push(replaceChildren(x,parent))
if (x.children) {
acc.push(addItems(x.children, x.id))
}
return acc
},[])
return items.flat()
}
return addItems(this.items).reduce((acc,x)=>{
acc[x.id]=x
return acc
},{})
},
_selection () {
const proxy = {}
addParents = (x, all) => {
const parentId = this._items[x.id].parent
if (parentId) {
if (all) addParents(this._items[parentId])
proxy[parentId] = this._items[parentId]
}
}
this.selection.forEach(x=>{
addParents(x,this.allParentNodes)
proxy[x.id] = x
})
return Object.values(proxy)
}
},
EDIT:
The recursion can be toggled with the allParentNodes property.
data : ()=> ({
allParentNodes : true,
})
TLDR: use selection-type="fix your god damn documentation"
I know that a moment has passed since this question was asked but I actually found out something interesting having the same problem
Turns out you can achieve desired result with a change in one line of code
You just have to specify selection-type as any string that isn't leaf or independent. Note that selection-type must not be omitted for desired result (it would otherwise default into leaf)
Personally I use fix your god damn documentation to relieve my frustration with said thing (info on described behaviouor is nowhere to be found in there)
Place that lead me to that solution: https://gitmemory.com/issue/vuetifyjs/vuetify/9088/543108633
Codepen: https://codepen.io/liath44/pen/RwoKxVB
I had the same problem and solved it by iterating over the internal nodes array of the treeview component, whenever the selection changes.
It seems the nodes array is a flattened version of the array containing an isSelected property along with the item.
In template:
<v-treeview
ref="tree"
v-model="selection"
...
></v-treeview>
In script:
data() {
return {
selection: [],
selectionWithParents: []
}
},
watch: {
selection() {
let _selectedNodes = []
let _treenodes = this.$refs['tree'].nodes
for (const key in _treenodes) {
if (Object.hasOwnProperty.call(_treenodes, key)) {
const node = _treenodes[key];
if (node.isSelected) _selectedNodes.push(node.item)
}
}
this.selectionWithParents = _selectedNodes;
}
}
Works fine in Vuetify v2.4.4.
const indexParent = this.data.findIndex(parent => parent.id == itemClicked.id_of_parent)
const indexChildren = this.data[indexParent].children.indexOf(indexChildren)

Search in dijit.Tree

In one of my projects I use a dijit.Tree control. I need to add a search to the tree and show only those nodes/leafs which have the searched term in them. However I can't seem to figure out how that can be achieved. Could anybody please help me?
im not entirely sure that your question entirely but it should give hint whereas to go.
Lets use reference documentation example as offset, there is 1) a store 2) a model and 3) the tree
var store = new ItemFileReadStore({
url: "{{dataUrl}}/dijit/tests/_data/countries.json"
});
var treeModel = new ForestStoreModel({
store: store,
query: {"type": "continent"}, // note, this bit
rootId: "root",
rootLabel: "Continents",
childrenAttrs: ["children"]
});
new Tree({
model: treeModel
}, "treeOne");
Interpret the above as such; You have loaded all known countries and continents but 'user' has selected only to show continents by using query on the model - and the hierachy is then represented in a tree structure.
You want a textbox with searching capeabilities, so we hook into onChange
new dijit.form.TextBox({
onChange: function() {
...
}
});
First bit, getting variables
var searchByName = this.get("value");
var oQuery = treeModel.query;
Next, set a new query on the model - preserving the old ones with an object mixin
treeModel.query = dojo.mixin(oQuery, { name: '*'+searchByName+'*' });
Last, notify the model and its tree that changes has occurred - and requery the visible items.
treeModel._requeryTop();
NB If the top-level item (for ForestModel) is not visible, none of its child elements will show, even if the search-string matches those. (Examplewise, Alabama is not shown if US Continent is not matched by query)
EDIT
As OP has the agenda to go by the 'NB', this may not fit needs 100% but its what dojo offers with dijit.Tree.. As it will get rather a lengthy process to recode the model/store queries to include parentbranches up until root i will not do this here - but there are a few tricks still ;)
var tree = new dijit.Tree( {
/**
* Since TreeNode has a getParent() method, this abstraction could be useful
* It sets the dijit.reqistry id into the item-data, so one l8r can get parent items
* which otherwise only can be found by iterating everything in store, looking for item in the parent.children
*
*/
onLoad : function() {
this.forAllNodes(function(node) {
// TreeNode.item <-- > store.items hookup
node.item._NID = node.domNode.id
});
},
/* recursive iteration over TreeNode's
* Carefull, not to make (too many) recursive calls in the callback function..
* fun_ptr : function(TreeNode) { ... }
*/
forAllNodes : function(parentTreeNode, fun_ptr) {
parentTreeNode.getChildren().forEach(function(n) {
fun_ptr(n);
if(n.item.children) {
n.tree.forAllNodes(fun_ptr);
}
})
}
});
(non-tested, but might just work) Example:
// var 'tree' is your tree, extended with
tree.forAllNodes = function(parentTreeNode, fun_ptr) {
parentTreeNode.getChildren().forEach(function(n) {
fun_ptr(n);
if(n.item.children) {
n.tree.forAllNodes(fun_ptr);
}
})
};
// before anything, but the 'match-all' query, run this once
tree.forAllNodes(tree.rootNode, function(node) {
// TreeNode.item <-- > store.items hookup
node.item._NID = node.domNode.id
});
// hopefully, this in end contains top-level items
var branchesToShow = []
// run fetch every search (TextBox.onChange) with value in query
tree.model.store.fetch(query:{name:'Abc*'}, onComplete(function(items) {
var TreeNode = null;
dojo.forEach(items, function(item) {
TreeNode = dijit.byId(item._NID+'');
while(TreeNode.getParent()
&& typeof TreeNode.getParent().item._RI == 'undefined') {
TreeNode = TreeNode.getParent();
}
branchesToShow.push(TreeNode.item);
});
}});
// Now... If a success, try updating the model via following
tree.model.onChildrenChange(tree.model.root, branchesToShow);

Which events are attached to an element?

How can I receive all events attached to an element with dojo?
dojo.query('#mydiv') // which events does #mydiv has?
To get all events on a DOM element:
// Get my div
myDiv = dojo.byId("myDiv");
// Obtain all event-related attributes
var events = dojo.filter(
myDiv.attributes,
function(item) {
return item.name.substr(0, 2) == 'on';
}
);
// Execute first found event, just for fun
eval(events[0].value);
If you get myDiv using dojo.query, remember that dojo.query returns an array, so your element would be in myDiv[0].
This solution does not work with events attached with dojo.connect. There probably is a way to extract this info from Dojo inner workings, but you would have to delve into the source code to understand how.
Another option is that you explicitly manage all dojo.connect events with a global registry. You could use dojox.collections to make this easier. For example, creating a global registry whose keys will be the dom nodes, and values will be the handles returned by dojo.connect (these handles contain the dom node, the type of event and the function to execute):
// On startup
dojo.require(dojox.collections.Dictionary);
eventRegistry = new dojox.collections.Dictionary();
...
// Registering an event for dom node with id=myDiv
var handle1 = dojo.connect(dojo.byId("myDiv"), "onclick", null, "clickHandler");
// Check if event container (e.g. an array) for this dom node is already created
var domNode = handle1[0];
if (!eventRegistry.containsKey(domNode))
eventRegistry.add(domNode, new Array());
eventRegistry.item(domNode).push(handle1);
...
// Add another event later to myDiv, assume container (array) is already created
var handle2 = dojo.connect(dojo.byId("myDiv"), "onmouseover", null, "mouseHandler");
eventRegistry.item(domNode).push(handle2);
...
// Later get all events attached to myDiv, and print event names
allEvents = eventRegistry.item(domNode);
dojo.forEach(
allEvents,
function(item) {
console.log(item[1]);
// Item is the handler returned by dojo.connect, item[1] is the name of the event!
}
);
You can hide the annoying check to see if event container is already created by creating a subclass of dojox.collections.Dictionary with this check already incorporated. Create a js file with this path fakenmc/EventRegistry.js, and put it beside dojo, dojox, etc:
dojo.provide('fakenmc.EventRegistry');
dojo.require('dojox.collections.Dictionary');
dojo.declare('fakenmc.EventRegistry', dojox.collections.Dictionary, {
addEventToNode : function(djConnHandle) {
domNode = djConnHandle[0];
if (!this.containsKey(domNode))
this.add(domNode, new Array());
this.item(domNode).push(djConnHandle);
}
});
Using the above class you would have to dojo.require('fakenmc.EventRegistry') instead of 'dojox.collections.Dictionary', and would simply directly add the dojo connect handle without other checks:
dojo.provide('fakenmc.EventRegistry');
eventRegistry = new fakenmc.EventRegistry();
var handle = dojo.connect(dojo.byId("myDiv"), "onclick", null, "clickHandler");
eventRegistry.addEventToNode(handle);
...
// Get all events attached to node
var allEvents = eventRegistry.item(dojo.byId("myDiv"));
...
This code is not tested, but I think you get the idea.
If its only for debugging purpose. You can try dijit.byId("myId").onClick.toString(); in your firebug console and you can see the entire onclick code this works even if the function is anonymous you can view the content of anonymous content.

Dojo DnD Move Node Programmatically

I would like to know if there is a way to move the node programmatically in dojo Dnd? The reason is I would like to revert the changes to the drag and drop when the web service call triggered a failed save on the database. Here is what I have so far.
In my code, the node Id seems to be unrecognized by the dojo.dnd.Container.DelItem. I cannot just use the selected item on the target because this is a asynchronous webservice function callback. So the user may be selecting another node on the container when this is called.
function OnMoveWSComplete(strResult) {
var resultObj = eval('(' + strResult + ')');
var sourceContainer = eval('(' + objResult.sourceContainerId + ')');
var targetContainer = eval('(' + objResult.targetContainerId + ')');
var targetNodes = targetContainer.getAllNodes();
for (var iNode = 0; iNode < targetNodes.length; iNode++) {
var currId = getLastIdFromStr(targetNodes[iNode].id);
if (currId == resultObj.Id) {
var targetN = targetNodes[iNode];
var Name = targetNodes[iNode].childNodes[0].nodeValue;
targetContainer.delItem(targetNodes[iNode].id);
var origData = { Id: resultObj.Id, Name: Name };
sourceContainer.insertNodes(true, origData);
break;
}
}
}
EDIT: Solution (Thanks Eugene Lazutkin) [2009/11/30]:
/**
* Move one node from one container to the other
*/
function moveNode(nodeId, sourceContainer, targetContainer) {
var node = dojo.byId(nodeId);
// Save the data
var saveData = sourceContainer.map[nodeId].data;
// Do the move
sourceContainer.parent.removeChild(node);
targetContainer.parent.appendChild(node);
// Sync the DOM object → map
sourceContainer.sync();
targetContainer.sync();
// Restore data for recreation of data
targetContainer.map[nodeId].data = saveData;
}
It looks like you assume that delItem removes physical nodes. Take a look at the documentation — probably you want to move nodes between containers instead of deleting them from the map. One simple way to do that just to move DOM nodes between containers, and call sync() on both containers.
Addition: Here is a super-simple pseudocode-like example:
function move(node, source, target){
// normalize node and/or node id
node = dojo.byId(node);
// move it physically from one parent to another
// (from target to source) adding to the end
target.parent.appenChild(node);
// now it is moved from source to target
// let's synchronize both dojo.dnd.Source's
source.sync();
target.sync();
}
Or something along these lines should work. The important pieces:
Move node from one parent to another using any DOM operations you deem appropriate. I used appendChild(), but you can use insertBefore(), or anything else.
Synchronize both sources involved after the move.
Obviously it works if both sources use nodes of the same type and structure. If not, you should do something more complex, e.g., move everything you need emulating a real DnD move by publishing topics described in the documentation.
I have this function which moves selected nodes by button click:
source.forInItems(dojo.hitch(this, function(item, id, map) {
if (dojo.hasClass(id, "dojoDndItemAnchor")) {
target.onDrop(source, [ dojo.byId(id) ], false);
dojo.removeClass(id, "dojoDndItemAnchor");
}
}));
onDrop() is an overridable method, which is called upon item drop, and by default calls to method onDropExternal(source, nodes, copy).
I am doing the same thing right now. I was able to solve by doing the following.
Set the dnd/Source autoSync property to true
<div data-dojo-type="dojo.dnd.Source" accept="widget" class="source" data-dojo-props="autoSync: true">
After dragging it looses the dndtype so I had to re-add it using client side code. Also I remove the dojoDndItemAnchor class after drop.
$(node).removeClass('dojoDndItemAnchor').attr('dndtype', 'widget');