Showing posts with label tree. Show all posts
Showing posts with label tree. Show all posts

Lazy loading data in dojo tree

In dojo tree, the data to be shown will normally be fetched from a json file. Though the data can be in any format: an array, a txt file, or a http request. Dojo tree just needs a dojo's data store object to display the data.
Anyone who has worked in dojo for a while, will know how to create a store & use it in tree.
How to load data lazily, i.e., how to load data only when a node is expanded.

In this article, I will show an simple way of loading data when a node is expanded, and data will be loaded from ajax call to server. (Using DWR to get data from server).

A new widget "TreeCustom" be created, extending from default dojo tree, and make changes to expandNode function to allow an option of loading the child items lazily. This is done by checking the state of the node being expanded & if it's UNCHECKED, then call "lazyloadItems" method, which will be overriden by the user to add new child items to that node.

Here is the extended Tree source code:

dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");

dojo.declare("dijit.TreeCustom", dijit.Tree, {
 
 _expandNode: function (node) {
  if(node.state == "UNCHECKED") {
   this.lazyLoadItems(node , function() {
    node.unmarkProcessing();
   });
  }
  this.inherited(arguments); 
 },
  
 lazyLoadItems: function(node, callback_function) {
  // Default implementation. User overridable function.
  callback_function();
 }
});


That's it for custom tree widget code.
The real job of lazy loading items is done in the function which overrides lazyLoadItems method. As shown below.

1) Create a tree (Custom tree)

 var myTree_Data = {
    identifier: 'name',
    label: 'name',
    items: [
    {name: 'World', type:'world',
    children:[{_reference:'India'}, {_reference:'Australia'} , {_reference:'United States'} ]},
    {name: 'India', type:'country', isStub:'true'},
    {name: 'Australia', type:'country', isStub:'true'},
    {name: 'United States', type:'country', isStub:'true'}
    ] 
 };


 var myTree_Store = null;

 dojo.addOnLoad(function(){
  myTree_Store = new dojo.data.ItemFileWriteStore({data: myTree_Data});
  createTree();
 });

 function createTree () {
  var newModel  = new dijit.tree.TreeStoreModel({
   store: myTree_Store,
   query: {type:'world'},
   childrenAttrs: ["children"]
  });

  if(dijit.byId("myTree") != null) dijit.byId("myTree").destroy();
  myTree = new dijit.TreeCustom({
   id: "myTree",
   model: newModel,
   lazyLoadItems: loadCities
  });

  dojo.byId("treecontainer").appendChild(myTree.domNode);
 }
 function loadCities(node, callback_function) {
  // Load cities for this 'node'
 }

Notice isStub property in the data for country items. Only the nodes with isStub set to true requires the data be still loaded from server.
And the data is loaded only when user wants it, i.e., when user expands that particular node.

2) Code for loadCities method (lazy load data for this node):
 function loadCities(node, callback_function) {
  // Load cities for this 'node'
  var store = this.tree.model.store;
  
  var isStub = store.getValue(node.item, "isStub");
  var country = store.getValue(node.item, "name");
  if(isStub) {
   store.setValue(node.item, "isStub", false); // Make stub as false
   
   // make DWR call to server to the cities for this country.
   CitiesHandler.getCities(country, { callback:function(datafromServer) {
    dwrCallbackMethod(datafromServer, node, callback_function); }
   });
  }

 }
 
 // Assuming the DWR method from server returns list of city names.
 function dwrCallbackMethod(data, parentNode, callback_function) {
  if(data == null) {
   callback_function();
   return;
  }

  // add the cities to the parentNode.
  var parentItem = parentNode.item;
  var store = this.tree.model.store;

  for(i=0; i < data.cities.length; i++) {
   var cityInfo = {name: data.cities[i], type:'city'};
   var cityItem = store.newItem(relationshipNode, {parent: parentItem, attribute:"children"} );
  }
  callback_function();
 }

In loadCities method, isStub property of the node is checked if its true/false. If true, a request to server is made to get the data from server. DWR callback method gets the list of city names as array. All the items in the array is added to the store, which directly reflects in the tree view.

This example assumes that user is aware of configuring DWR in server side, and how to use dwr. If not, check directwebremoting.org to download DWR & also for instructions on setting up DWR.
Any suggestions/queries, i will be happy to help. :)

Multi Select in Dojo Tree

Dojo comes with a very nice widget "Tree".
Tree is bundled in dijit, it allows programmer to represent data in a tree view.
With dojo 1.2, creating an tree is quite easy. The 3 simple steps to create a simple tree are:
1) Create a data store
2) Create a model from this store
3) Create a tree, and assign the model to pick up the data from.

Using the default tree, user can expand/collapse a node, click on the node, and with just a little change in code have the ability to drag and drop nodes. But one thing that the default tree lacks is the ability to select multiple nodes in the tree. In this article, i'll show the simple way of customizing the dojo tree to have the ability to multi select nodes.

The best way to customize an existing widget is to extend the widget & create a new widget from it.
In this article, we'll create a new widget "dijit.TreeCustom", which extends from dijit.Tree.
In the custom widget, following four methods of dijit.Tree should be overridden

1) postCreate
2) _onClick
3) focusNode
4) blurNode

Custom Tree Widget:
Here is the skeleton code for custom tree widget, extending from default dojo tree.
dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");
dojo.require("dojo.dnd.Manager");

dojo.declare("dijit.TreeCustom", dijit.Tree, {
});
(If you need more details on creating an widget, refer my previous article "Creating custom widgets in DOJO")

Adding new properties for Custom tree widget
	lastFocusedNode: null, // To store the currently focused node in the tree.

	allFocusedNodes: [], // array of currently focused nodes. eg., user holds cntrl key and selects multiple nodes.

	ctrlKeyPressed: false, // Flag to find out if ctrl key was pressed, when click happened.
The properties are self-explanatory.

Overriding postCreate:
When dojo engine has finished creating & parsing the TreeCustom widget on browser, postCreate method is called, where in the initilization stuffs can take place. Here is the postCreate method for TreeCustom widget, initializing the properties to proper values.
	postCreate: function(){
		this.inherited(arguments); // Call parent's postCreate
		this.onClick = this.onClickDummy;
		this.allFocusedNodes = [];
		this.lastFocusedNode = null;
	},
Notice that, default onClick is dummied out, since the new tree widget will have to send out array of selected nodes for onClick event, rather than just one selected node. So, a new customOnClick event will be added for this purpose. (See below)

Dummy onclick method (Does nothing)
	onClickDummy: function(item, node) {
	},

Overriding _onClick:
_onClick is the method called by default tree whenever user clicks on the tree node.
	_onClick: function(/*Event*/ e){
		if(e.ctrlKey) {
			this.ctrlKeyPressed = true;
		} else {
			this.ctrlKeyPressed = false;
		}
		this.inherited(arguments); 
		dojo.stopEvent(e);
	},
This method, stores the state of ctrl key pressed, and passes on to default tree's _onClick, which inturn calls focusNode method, which is also one of the method that will be overridden for custom tree widget.

Overriding focusNode:
	focusNode: function(/* _tree.Node */ node){
		this.inherited(arguments); 
		
		this.lastFocusedNode = node;
		if(this.ctrlKeyPressed) {
			// Ctrl key was pressed
		} else {
			// Ctrl key was not pressed, blur the previously selected nodes except the clicked node.
			for(i=0; i < this.allFocusedNodes.length; i++) {
				var temp = this.allFocusedNodes[i];
				if(temp != node)
					this._customBlurNode(this.allFocusedNodes[i]);
			}
			this.allFocusedNodes = [];
		}
		var isExists = false; // Flag to find out if this node already been selected
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var temp = this.allFocusedNodes[i];
			if(temp.item.id == node.item.id) isExists = true;
		}
		if( ! isExists)
			this.allFocusedNodes.push(node);
		this.customOnClick (node.item, node, this.getSelectedItems() );
		this.ctrlKeyPressed = false;
	},
_customBlurNode method:
	_customBlurNode: function(node) {
		var labelNode = node.labelNode;
		dojo.removeClass(labelNode, "dijitTreeLabelFocused");
		labelNode.setAttribute("tabIndex", "-1");
		dijit.setWaiState(labelNode, "selected", false);
	},
customOnClick method: An event handler, that must be overriden by page using this widget. This method is called when user clicks on any node in the tree.
Passes node, item & array of all selected items to the method.
Here is the default implementation. (to avoid runtime exceptions)
	customOnClick: function (item, node, allSelectedItems) {
		//User overridable method.
	},
Overriding blurNode: Since a custom blur node method was used, the default blurNode is no more required. Overriding to cover the default behavior.
	blurNode: function(){
		// Not using, we've our own custom made blur method. See _customBlurNode
	},
Extra method to get all selected items
An utility method for the custom tree, which can be used to get list of all the selected items in the tree.
	// Returns array of currently selected items.
	getSelectedItems: function() {
		var selectedItems = [];
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var iNode = this.allFocusedNodes[i];
			selectedItems.push(iNode.item);
		}
		return selectedItems ;
	},
Final dijit.TreeCustom JS: Put this in TreeCustom.js file under dijit folder. (DOJO_HOME/dijit).
dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");
dojo.require("dojo.dnd.Manager");


dojo.declare("dijit.TreeCustom", dijit.Tree, {
	
	lastFocusedNode: null, // To store the currently focused node in the tree.

	allFocusedNodes: [], // array of currently focused nodes. eg., user holds cntrl key and selects multiple nodes.

	ctrlKeyPressed: false, // Flag to find out if ctrl key was pressed, when click happened.

	postCreate: function(){
		this.inherited(arguments);
		this.onClick = this.onClickDummy;
		this.allFocusedNodes = [];
		this.lastFocusedNode = null;
	},

	focusNode: function(/* _tree.Node */ node){
		this.inherited(arguments); 
		
		this.lastFocusedNode = node;
		if(this.ctrlKeyPressed) {
			// Ctrl key was pressed
		} else {
			// Ctrl key was not pressed, blur the previously selected nodes except the clicked node.
			for(i=0;i < this.allFocusedNodes.length; i++) {
				var temp = this.allFocusedNodes[i];
				if(temp != node)
					this._customBlurNode(this.allFocusedNodes[i]);
			}
			this.allFocusedNodes = [];
		}
		var isExists = false; // Flag to find out if this node already been selected
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var temp = this.allFocusedNodes[i];
			if(temp.item.id == node.item.id) isExists = true;
		}
		if( ! isExists)
			this.allFocusedNodes.push(node);
		this.customOnClick (node.item, node, this.getSelectedItems() );
		this.ctrlKeyPressed = false;
	},

	blurNode: function(){
		// Not using, we've our own custom made blur method. See _customBlurNode
	},
	
	_onBlur:function () {
	},

	_onClick: function(/*Event*/ e){
		if(e.ctrlKey) {
			this.ctrlKeyPressed = true;
		} else {
			this.ctrlKeyPressed = false;
		}
		this.inherited(arguments); 
		dojo.stopEvent(e);
	},

	_customBlurNode: function(node) {
		var labelNode = node.labelNode;
		dojo.removeClass(labelNode, "dijitTreeLabelFocused");
		labelNode.setAttribute("tabIndex", "-1");
		dijit.setWaiState(labelNode, "selected", false);
	},
	
	// Returns array of currently selected items.
	getSelectedItems: function() {
		var selectedItems = [];
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var iNode = this.allFocusedNodes[i];
			selectedItems.push(iNode.item);
		}
		return selectedItems ;
	},

	onClickDummy: function(item, node) {
	},

	customOnClick: function (item, node, allSelectedItems) {
		//User overridable method.
	}

});



Example of using the Custom Tree
Put this html file under dijit/tests/tree.


	Dijit Tree Test

	

	
	

	
	

	
	

	







Let me know of any suggestion/changes for this custom tree code.

In next article, I'll show how to lazy load data in tree using DWR (Ajax).