Showing posts with label dojo. Show all posts
Showing posts with label dojo. Show all posts

Ways Of Writing Javascript

The way developers write javascript code has changed enormously over the years.

Here are the pros & cons of the three options in the way you can write javascript these days.
  1. Hand code everything:
    • Write all javascript code from scratch.
    • Deal directly with browser compatibility issues.
    • Very flexible (upto the point, you can decide on the name of your temporary variable ;) ).

  2. Use Generic Utilities:
    You get/download the generic javascript utilities written by others (friends, fellow developers), & with little customization use them in your applications.
    • Saves some time to get things working.
    • Browser compatibility issues may be taken care by the generic utilities
    • You should deal with the customization errors
    • Flexible, until you are faced with a bug/error which was not faced by the person who created the utility. (Could be because your application required slightly different behavior)

  3. Use Javascript Toolkits/Frameworks:
    Use the popular javascript toolkits available on internet (for free). Some of the widely used toolkits are jQuery, prototype, YUI, dojo, etc.,
    • Download & use directly in your applications. As simple as including a js file & start using all the wide range of features provided.
    • Little/No javascript experience required.
    • Provides ready to use UI widgets. (like Plug & Play)
    • Provides AJAX facilities
    • Browser compatibility is no more an issue. All toolkits takes care of these internally.
    • Flexibility is less: If you want to change anything for your taste, understanding & changing the core code is time consuming.
    • Most of these toolkits provide a way for developer to extend & create your own widgets

  4. As we can see, using javascript toolkits is the best option.

    In my next article, we'll look into each of these toolkits & compare them.

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).

Creating a Custom Dojo Widget

A Typical pagination widget should have links to navigate between pages. Generally First, Next, Previous & Last are the four links that will be present in a pagination widget.

Custom Widget - First Step
Create a Javascript file, named Paginator.js (For simplicity sake, keep this file under dijit folder)
Skeleton code for Paginator.js
dojo.provide("dijit.Paginator");
dojo.require("dijit._Templated");
dojo.require("dijit.layout.ContentPane");

dojo.declare("dijit.Paginator",
	[dijit.layout.ContentPane, dijit._Templated], 
{
});


The above code creates a widget by name dijit.Paginator, and tells dojo that a widget by this name is now available.

Adding functionality to the widget.
Creating the required HTML snippet for the widget.


This is the HTML code that the widget will use to display on the browser.
There are two ways of letting dojo engine know about using this HTML code for rendering on browser. Using templateString property, or templatePath property of the widget.
For this example, templateString is used to provide the HTML code for the widget.
dojo.declare("dijit.Paginator",
	[dijit.layout.ContentPane, dijit._Templated], 
{
	templateString: '
' + '
' + ' ' + ' |<' + '   ' + ' ' + ' <<' + '   ' + '  ' + ' ' + ' >> ' + '  ' + ' ' + ' >|' + ' ' + '
' + '
' });

The html required by the widget is assigned to a property templateString, this property is the one used by dojo when rendering/parsing the widget on the browser. (templatePath can also be used, by providing path for HTML file) dojoAttachEvent is the way of attaching an event handler for any node (HTML node/tag).
Adding the required properties for the widget.
	currentPage: 1,
	totalPages: 0,

These are two properties that will be initiated by the pages using the widget.
Adding a method for refreshing the widget according to the current state of the widget. Meaning, the first/prev links should be hidden when the page shown is the first page, and similar case with last/next links. Here is the code for refresh method.
	refresh: function () {
		if(this.currentPage <= 0) this.currentPage =1;
		this.pageNumInfoNode.innerHTML = this.currentPage + " of " + this.totalPages;

		if(this.currentPage == 1) {
			this.prevNode.style.display = "none";
			this.firstNode.style.display = "none";
		} else {
			this.prevNode.style.display = "";
			this.firstNode.style.display = "";
		}

		if(this.currentPage >= this.totalPages) {
			this.nextNode.style.display = "none";
			this.lastNode.style.display = "none";
		} else {
			this.nextNode.style.display = "";
			this.lastNode.style.display = "";
		}
		
	}

pageNumInfoNode points to the node in the widget, which displays the current page number. It shows the information, something like "1 of 5", "3 of 10" etc.,
Whenever a dojo widget has been rendered & created on browser, dojo engine calls the postCreate method of the widget to perform any operations upon creation of the widget. This method can be generally used for initializing state of the widget. Use this method for refreshing/initializing the widget for the initial view.
	postCreate: function(){
		this.refresh();
		this.inherited(arguments);
	}

this.inherited(arguments) calls the super/parent widgets postCreate up the hierarchy. (Similar to super in java).
Add event handlers for each of the pagination links.
	firstClick: function () {
                if(this.currentPage <= 1) return;
		this.currentPage = 1;
		this.refresh();
		this.onPageChange(this.currentPage);
	},
	prevClick : function() {
                if(this.currentPage <= 1) return;
		this.currentPage = this.currentPage - 1;
		this.refresh();
		this.onPageChange(this.currentPage);
	},
	nextClick : function() {
                if(this.currentPage >= this.totalPages) return;
		this.currentPage = this.currentPage + 1;
		this.refresh();
		this.onPageChange(this.currentPage);
	},	
	lastClick: function () {
                if(this.currentPage >= this.totalPages) return;
		this.currentPage = this.totalPages;
		this.refresh();
		this.onPageChange(this.currentPage);
	},

onPageChange will be the way of the widget to communicate with outer world. Whenever user does any activity on the widget, like changing the page, onPageChange event will be triggered and it passes the current page number as the parameter. Then the page can take required actions to show the data appropriate for that page number.
Anyway, there should be a default implementation for this onPageChange method in the widget.
So, here is the default implementation of onPageChange, which should and must be overridden by page using the widget to get any meaningful functionality.
	onPageChange: function (currPage) {
		// User should override this method to communicate with the widget & perform required actions for particular page.

	},

Putting it all together, Here is the complete code for Paginator.js
dojo.provide("dijit.Paginator");

dojo.require("dijit._Templated");
dojo.require("dijit.layout.ContentPane");

dojo.declare("dijit.Paginator",
	[dijit.layout.ContentPane, dijit._Templated], 
{

	
	templateString: '
' + '
' + ' ' + ' First ' + '   ' + ' ' + ' Prev ' + '   ' + '  ' + ' ' + ' Next ' + '  ' + ' ' + ' Last ' + ' ' + '
' + '
', currentPage: 1, totalPages: 0, firstClick: function () { this.currentPage = 1; this.refresh(); this.onPageChange(this.currentPage); }, prevClick : function() { this.currentPage = this.currentPage - 1; this.refresh(); this.onPageChange(this.currentPage); }, nextClick : function() { this.currentPage = this.currentPage + 1; this.refresh(); this.onPageChange(this.currentPage); }, lastClick: function () { this.currentPage = this.totalPages; this.refresh(); this.onPageChange(this.currentPage); }, onPageChange: function (currPage) { // User function called. passes the currentPage }, refresh: function () { if(this.currentPage <= 0) this.currentPage =1; this.pageNumInfoNode.innerHTML = this.currentPage + " of " + this.totalPages; if(this.currentPage == 1) { this.prevNode.style.display = "none"; this.firstNode.style.display = "none"; } else { this.prevNode.style.display = ""; this.firstNode.style.display = ""; } if(this.currentPage >= this.totalPages) { this.nextNode.style.display = "none"; this.lastNode.style.display = "none"; } else { this.nextNode.style.display = ""; this.lastNode.style.display = ""; } }, postCreate: function(){ this.refresh(); this.inherited(arguments); } });
Example of using the newly created widget.




In this example, whenever the user clicks on any of the links on the widget, notification is sent to myFunction which notifys the page change.