Javascript

dijit.Tree and Sorting

Today I want to talk about the Dojo Toolkit 1.6 version dijit.Tree and Sorting feature.
This will be a very very short tutorial on how to make it work. I recently had to make it work on an already existing application that had a few years old version of Dojo installed. Although looking at the documentation then it doesn’t seem like the new version is much better in this particular area.

dijit.tree

To get the sorting working nicely you need 3 main components: store, model and the tree. Here’s my examples on those. I’ll be using an ItemFileWriteStore to get my tree data with Ajax query to in a JSON format. We then need a TreeStoreModel that basically is just a wrapper for our tree nodes.

var myStore = new dojo.data.ItemFileWriteStore({url:"/datasource.php?format=json"});
var treeModel = new dijit.tree.TreeStoreModel({store:myStore,query:{id: "0"}});
var myTree = new dijit.Tree({
    id: 'myTree',
    model: treeModel,
    labelAttrs: "label",
    betweenThreshold: "8",
    dndController: dijit.tree.dndSource,
    checkAcceptance: dragAccept,
    checkItemAcceptance: dropAccept
}).placeAt('treeDiv');

By now you should have a working dijit.Tree with sorting functions, but no actual storage is happening yet. We’ll get to that in a second. My personal show-stopper was the fact that I tried to use dijit.tree.ForestStoreModel initially which is wrong. No matter how hard I tried – the tree just didn’t allowed sorting nodes with it.

Also, special note on the “betweenThreshold” flag – You need this property(nr of pixels) to tell the tree that the pages can be sorted within it’s current parent, not only dragged between parents. If you don’t have betweenThreshold then Dojo Toolkit 1.6 will not allow you to sort within your current parent.

So far so good. Now you might have noticed I defined dragAccept and dropAccept methods as checkAcceptance and checkItemAcceptance. You can add custom logic to those methods to define what items can be dragged to what parent nodes. This allows you to make some items incompatible with others and can prevent you from mixing Apples with Oranges in your tree. For the purpose of this tutorial we keep them VERY simple and allow sorting between all nodes. This is probably the typical case:

function dragAccept(source,nodes){ return true; }
function dropAccept(target,source,position) { return true; }

In order to store the new sorting order after you release the item from dragging you need a method to save it to the server. You could do it by saving the whole order of the whole tree into array of inputs or some other hack, but I chose the option of sending an ajax POST request on each sorting action. This means every time we release an item we will send a background POST request to the server. This means we don’t need a special SAVE button in the end of our tree. Do achieve that we have to connect to the onPaste method of the treeModel.

If we want to allow modifications to our data via drag-n-drop, we can implement the pasteItem() method and set the drag-n-drop controller for the tree.

dojo.connect(treeModel, 'pasteItem', function(childItem, oldParentItem, newParentItem, bCopy, insertIndex) {
    entityID = childItem.id[0]; // The node that was sorted
    parentID = newParentItem.id[0]; // The new(could be the old one) parent of the node
    order = 0; // We set the order to 0 first
    if (insertIndex != undefined) { // And only override it when the order is defined.
        order = insertIndex;
    }
    // Send POST to server side.
    dojo.xhrPost({
        url: "/saveorder.php?id="+entityID+"&parent="+parentID+"&order="+order,
        load: function(data, ioargs) {
            // Notify user about successful save here.
            alert('Your order was saved!');
        }
    });
});

That’s IT! You should be done now from the javascript side. The server and JSON parts should be easy as the internet is full of documentation on those. If you are using a PHP backend then it’s just a matter of using json_encode method and reading some $_POST params.

Some more reference material can be found here: http://dojotoolkit.org/documentation/tutorials/1.6/store_driven_tree/

If you have a question or having trouble getting it working then feel free to write a comment and I’ll do my best to help you out. If you would like to read more about Dojo Toolkit related subjects then also make sure to comment on that!

AUI Autocomplete with service builder json datasource

Since it’s not so trivial to correctly understand AlloyUI Autocomplete that’s shipped with Liferay 6.2 then here’s an example on how to make an autocomplete that uses a custom service builder json web service as a datasource.
We will use AUI Autocomplete widget and then user Liferay Service library to fetch the result from our own custom service builder built JSON web service.

This is the look we are trying to achive:
aui autocomplete service builder

Full portlet view.jsp code:


<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %>

<portlet:defineObjects/>
<liferay-theme:defineObjects />

<aui:input name="contactName" type="text" />

<aui:script use="autocomplete-list,aui-base,aui-io-request-deprecated,autocomplete-filters,autocomplete-highlighters,datasource,datasource-get,datatable-datasource">

// Please note that this contact portlet service is a service builder generated JSON web service.
// We pass the groupId as a query param because our service expects it. Liferay has a nice javascript method for finding the group id.
var contactSearchDS = new A.DataSource.IO({source: '/api/jsonws/contact-portlet.contact/get-contacts-by-name?groupId='+Liferay.ThemeDisplay.getScopeGroupId()});

var contactSearchQueryTemplate = function(query) {
        // Here's an example on how to pass additional parameters to the query for you service
        // In our case we are fetching only the first 20 items and specify the ordering by name
	var output = '&name='+query.trim()+'&sort=name&dir=asc&start=0&end=20';
	return output;
}

var contactSearchLocator = function (response) {
	var responseData = A.JSON.parse(response[0].responseText);
// For debugging you can do: console.debug(responseData);
    return responseData;
};

var contactSearchFormatter = function (query, results) {
	return A.Array.map(results, function (result) {
// For debugging: console.debug(result.raw);
		return '<strong>'+result.raw.fullName+'</strong><br/>'+result.raw.mobile+' '+result.raw.phone+' '+result.raw.email;
	});
};

var contactSearchTextLocator = function (result) {
// This is what we place in the input once the user selects an item from the autocomplete list.
// In our case we want to put contact full name in there.
	return result.row.fullName;
};

var contactSearchInput = new A.AutoCompleteList({
	allowBrowserAutocomplete: 'false',
	resultHighlighter: 'phraseMatch',
	activateFirstItem: 'false',
	inputNode: '#<portlet:namespace/>contactName',
	render: 'true',
	source: contactSearchDS,
	requestTemplate: contactSearchQueryTemplate,
	resultListLocator: contactSearchLocator,
        resultFormatter: contactSearchFormatter,
	resultTextLocator: contactSearchTextLocator		
});

</aui:script>		

We’ll post an example of “multi-autocomplete” input also soon. That one requires a lot more code but allows selecting multiple contacts and shows them as “tags” above the input box. Much like the facebook name finder works.

As always, if you have questions or suggestions then please comment below.