Introduction
A while back, I wrote an article Build Visualforce Hierarchy Trees with EXT JS on how to build tree structures in Visualforce using the extjs JavaScript Framework. The disadvantages are the this Framework is not open-source, the learning curve is high and the solution I presented involved writing a controller and two visualforce pages.
I promised to write a follow up post with an alternative, open source, much simpler to use jQuery plugin or library. This me keeping that promise.
Use Case
Build a tree which displays the account hierarchy. The parent field concept can be used on virtually any object to depict a hierarchy relationship between records of that object. So visualising this hierarchy is a pretty common request you will come across. Using a tree is always a good choice.
In this special case, we will assume we had a requirement to open a Visualforce page which displays child account information in a special way ...
As of Winter 14, it is possible to hide the "View Hierarchy" link.
We will hide this link and create our own "View Hierarchy" link and add it to the Account page layout. We will then replace the standard functionality which happens when "View Hierarchy" is clicked with a the jstree.
We can add event listeners to the jstree such that if a child record is cliked, we can open the Visualforce page to display that child account. Let's begin.
For this task, we will require the following
- Custom "View Hierarchy" link on Account
- jQuery
- jstree (after downloading, you will find what you need in the dist folder)
- a controller class (CtrAccountHierarchy)
- a visualforce page (AccountHierarchy)
So let's look at how you can accomplish this.<br />
Apex
public Account acct { get; set; }
public CtrAccountHierarchy() {
String acctId = ApexPages.currentPage().getParameters().get('acctId');
if(acctId != null){
acct = [SELECT Id, Name FROM Account WHERE Id =: acctId];
}
}
The account Id will be passed along with the URL of the custom link. Alternatively you could use a Standard Controller and save the select statement in the controller by doing (Account)standController.getRecord(). Mind you that the select statement hier is note necessary at all. I have included it because I want the name of the account, which I could also pass as a parameter. The point is always think about what you are doing every step of the way.
Next we want to take a look at the "Remote" method which will be called by the JavaScript on our page. Notice that is method is marked by the @RemoteAction notation and is public and static.
@RemoteAction
public static List getChildAccounts(Id acctId){
List<Account> aw = new List<Account>();
Map<Id, Id> acctMap = new Map
<Id, Id>();
<Id, Account> childAccounts = new Map
Map ([SELECT Id, Name FROM Account WHERE ParentId =: acctId]);
<Id, Account> potentialParents = new Map
Map ([SELECT Id, ParentId FROM Account WHERE ParentId IN : childAccounts.keySet()]);
for(Account acct : potentialParents.values()){
if(childAccounts.get(acct.ParentId) != null){
acctMap.put(acct.ParentId, acct.ParentId);
}
}
for(Account acct : childAccounts.values()){
if(acctMap.get(acct.Id) != null){
aw.add(new AccountWrapper(acct, true));
} else {
aw.add(new AccountWrapper(acct, false));
}
}
return aw;
}
In the code snippet above, we first get all children of the account we are viewing. Secondly we need to know if these children are themselves parents of other accounts. The reason is, if they are not parents of other accounts, then there is no need to make a call to the controller when they are clicked on the tree. Additionally they could be styled differently from the other nodes.
Notice the AccountWrapper class in the code snippet above. This class is used to indicate if an account is a Parent or not.
public class AccountWrapper{
public Account acctRecord { get; set; }
public Boolean isParent { get; set; }
public AccountWrapper(Account acct, Boolean parent){
this.acctRecord = acct;
this.isParent = parent;
}
}
Visualforce
Begin by setting your controller. Don't forget that you can also use a standard controller here
<apex:stylesheet value="{!URLFOR($Resource.tutorials, '/css/ext-theme-gray/ext-theme-gray-all.css')}" />
<apex:page controller="CtrAccountHierarchy" showHeader="true" sidebar="true" tabStyle="Account"} />
Then next you include your JavaScript resources:
<apex:includeScript value="{!URLFOR($Resource.tutorials, '/js/jquery-1.9.1.js')}" />
<apex:includeScript value="{!URLFOR($Resource.tutorials, '/jstree/jstree.min.js')}" />
<apex:stylesheet value="{!URLFOR($Resource.tutorials, '/jstree/themes/default/style.min.css')}" />
Next let's look at the JavaScript that does the heavy lifting:
<script type="text/javascript";>
j$ = jQuery.noConflict();
j$( document ).ready(function() {
createAccountHierarchy();
});
function openChildAccount(id){
console.log('Child Account Id: ' + id);
alert('Child Account Id: ' + id + '. You can now do whatever you want with the child account!.')
}
function createAccountHierarchy(){
j$("#acctHierarchy").jstree({
"plugins": ["themes", "search", "wholerow"],
"core" :{
animation : 0,
check_callback : true,
themes : { "stripes" : true },
data : function (node, cb) {
buildTreeNodes(this, node, cb);
}
}
}).bind('select_node.jstree', function(e, data){
openChildAccount(data.node.id);
});
}
function buildTreeNodes(tree, node, cb){
var parentAcctId = "{!acct.Id}";
var nodeId = node.id == "#" ? parentAcctId : node.id;
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.CtrAccountHierarchy.getChildAccounts}',
nodeId,
function(result, event){
if (event.status) {
console.log('Yeahhhh : ' + event.status);
var nodes = [];
result.forEach(function(record){
nodes.push({
id: record.acctRecord.Id,
text: record.acctRecord.Name,
children: record.isParent,
icon: record.isParent
});
});
cb.call(tree, nodes);
} else if (event.type === 'exception') {
j$("#responseErrors").html(event.message + " - " + event.where);
console.log(event.message + " - " + event.where);
} else {
j$("#responseErrors").html(event.message);
console.log(event.message);
}
},
{escape: true}
);
}
</script;>
Notice that we call createAccountHierarchy function to build our tree once the page is loaded. This function defines any 3 main things, plugins, data and event listeners. The data attribute calls the buildTreeNodes function and passes it a reference to the tree, the current node and a callback which will display the tree after the data is fetched. Initially buildTreeNodes will load children for the current account and indicate if any of the children are themselves parents. Notice the call to the Controller getChildAccounts and the parameter nodeId passed along to this function. Notice the plugin's push method which has attributes "children" and "icon". The isParent Boolean variable will determine if the node will be clickable and if it will have the icon indicating that it is clickable. Look at the image below:
The rest of the code is self-explanatory i hope. Notice the bind method that gets the id from the selected node and passes it along to the openChildAccount method. Now you have fully customise the "View Hierarchy" functionality and can open the child records which ever way you choose.
Finally do not forget to create a custom "View Hierarchy" link and add it to the Account Layout.
Conclusion
I hope the steps were simple and clear enough. In comparison to this example Build Visualforce Hierarchy Trees with EXT JS , this method of creating trees is a piece of cake and can be usefully applied to meet a couple of requirements and different scenario. Special credit goes to this blogpost Hierarchies with Remote Objects and jsTree by Peter Knolle which uses jstree to build generic trees using the concept of Visualforce Remote Objects. Well written and offers a great use case for remote objects.
Leave a comment if you have one. I salut you
IN RESPONSE TO STEVE'S COMMENT BELOW
hi Steve, I can't give you the entire code for this due to time concerns but I will point you in the right direction. There are two ways I will do this, the inefficient and the efficient way
A.) First the inefficient way will be to do the following steps
1.) Select All Accounts your are interested in and put them in a map
Map<Id, Account> allAccounts = new Map<Id, Account>();
for(Account acct : [SELECT Id, Name, ParentId, Company_Code__c FROM Account WHERE put your condition here]){
allAccounts.put(acct.Id, acct);
}
2.) The second step will be to determine the Ultimate parent to a chosen level. I explain. Your account hierarchy could be any level deep.
Imagine you wanted to view the hierarchy of an account 9 levels deep, you will have to traverse the tree backwards 9 levels to get the ultimate parent. This is very inefficient as you will have to do a lot of manuel processing to get this information. Secondly if your hierarchy is deep enough you will hit some limit at one point, probably the CPU time limit. See step 3 for a more compelling reason not to use this method. But step two could look like this
Id accountId = {this is the account whose tree hierarchy you want}
Integer levels = {limit to the number of back steps you wish to take to get the ultimate parent}
Id ultimateParent = {Id of ultimate parent}
if(allAccounts != null && !allAccounts.isEmpty()){
Account currentAccount = allAccounts.get(accountId);
if(currentAccount.ParentId != null){
for(Integer i = 0; i < levels; i++){
if(allAccounts.get(currentAccount.ParentId) != null
&& allAccounts.get(currentAccount.ParentId).ParentId != null){
currentAccount = allAccounts.get(currentAccount.ParentId);
} else {
if(currentAccount.ParentId != null && allAccounts.get(currentAccount.ParentId) == null){
ultimateParent = currentAccount.Id;
} else {
ultimateParent = currentAccount.ParentId;
}
break;
}
}
}
}
As you can see this method is very cumbersome. I will not recommend it if your account goes beyond a few levels and by a few I mean 3 to 5. But this also depends on what ever processing you are doing on your page
So now you have the ultimate parent. What do you do?
3.) You need to get all Children for that parent since you want to show the entire tree. Your code could look like this
Boolean isEndOfTree = false;
Set<Id> immediateParentIds = new set<Id>();
immediateParentIds.add(ultimateParent);
Set<Id>idsOfAllAccountsInTheHierarchy = new set<Id>();
idsOfAllAccountsInTheHierarchy.add(ultimateParent);
while(!isEndOfTree){
Map<Id, Account> immediateChildren = new Map<Id, Account>([SELECT Id, ParentId FROM Account WHERE ParentId IN: immediateParentIds]);
if(!immediateChildren.isEmpty()){
immediateParentIds = immediateChildren.keySet();
idsOfAllAccountsInTheHierarchy.addAll(immediateChildren.keySet());
} else {
isEndOfTree = true;
}
}
List<Account> allAccountsInHierarchy = [SELECT Id, Name, ParentId FROM Account WHERE ParentId IN : idsOfAllAccountsInTheHierarchy];
So now we have a list of all accounts in the tree hierarchy but we have just done something that if you ever do on a project, your boss or tech lead might have you jailed, shot in the foot or something worst. So what did we do above? we put a SOQL Query in a while loop. But this is the only way to go with this method.
So at this point I will stop and switch to a second method. I just showed you this so you will better understand what I am about to suggest.
The next idea is to have a field on account called ultimateParentId and use a trigger to make sure that the right account is stored in this field when you create the account. Make sense?
In this case we will have just a single SOQL query to get all the Accounts in a hierarchy.
Account acct = {selected account whose tree hierarchy we want}
List<Account> allAccountsInHierarchy = [SELECT Id, Name, ParentId FROM Account WHERE ultimateParentId = : acct.ultimateParentId];
So now we have all the children in the Hierarchy, what next. Before you proceed, take a look at the documentation again https://www.jstree.com/docs/json/ and specifically at "Alternative JSON format". The idea is to build a JSON Structure that will be passed back to the page and the page will use to construct tree hierarchy. First you will need two wrapper classes like this
public class TreeWrapper{
public String id { get; set;}
public String parent {get; set; }
public String text { get; set; }
public Boolean icon { get; set; }
public StateWrapper state { get; set; }
public TreeWrapper(Account acct, String parent, Map<String, Schema.SObjectField> acctFields, StateWrapper sw){
this.id = acct.Id;
this.parent = parent;
this.text = acct.Name;
this.icon = false;
this.state = sw;
}
}
public class StateWrapper{
public Boolean opened { get; set; }
public Boolean selected { get; set; }
public StateWrapper(Boolean selected){
this.opened = true;
this.selected = selected;
}
}
Above the text could be anything you want, not just the account name.
Finally you will then build your tree as follows; Read the js documentation then you will understand the code better.
At this point you should have the following:
Map<Id, Account> allAccounts = {all accounts}
List<Account> allAccountsInHierarchy = {all accounts in hierarchy}
Account ultimateParent = {Account on top of hierarchy)
Id acctId = {selected account for which we want to view hierarchy}
List<TreeWrapper> tw = new List<TreeWrapper>();
First, check if the selected Account is the root of the tree. If it is we will select it. Code could look like this:
if(ultimateParent.id == acctId){
tw.add(new TreeWrapper(ultimateParent, '#', acctFields, new StateWrapper(true)));
} else {
tw.add(new TreeWrapper(ultimateParent, '#', acctFields, new StateWrapper(false)));
}
Then go through the rest of the accounts in the tree as follows;
if(!allAccountsInHierarchy.isEmpty()){
for(Account acct : allAccountsInHierarchy.values()){
if(allAccounts.get(acct.Id).ParentId != null){
if(acct.Id == acctId){
tw.add(new TreeWrapper(acct, allAccounts.get(acct.Id).ParentId, acctFields, new StateWrapper(true)));
} else {
tw.add(new TreeWrapper(acct, allAccounts.get(acct.Id).ParentId, acctFields, new StateWrapper(false)));
}
} else {
if(acct.Id == acctId){
tw.add(new TreeWrapper(acct, '#', acctFields, new StateWrapper(true)));
} else {
tw.add(new TreeWrapper(acct, '#', acctFields, new StateWrapper(false)));
}
}
}
}
tw is the JSON structure you will return back to the client
You could also us the follwoing code to determine whether or not an Icon should be displayed on a node or not. An Icon will be displayed for instance when a tree node is a parent itself.
/* Go through the tree an check which nodes are parents. These nodes will have the folder icon added to them
* This is done in two steps. First go through the tree and add parents to a set and then go through the tree
* a second time and add icons to all nodes whose ids are in the set created above
*/
public static List<TreeWrapper> addIconTotree(List<TreeWrapper> tw){
Map<Id, List<TreeWrapper>> twMap = new Map<Id, List<TreeWrapper>>();
Set<String> twIds = new Set<String>();
for(TreeWrapper record : tw){
if(record.parent != '#'){
twIds.add(record.parent);
}
}
for(TreeWrapper record : tw){
if(twIds.contains(record.id)){
record.icon = true;
}
}
return tw;
}
Then before you return the tree json data (tw), you can do the following;
tw = addIconTotree(tw);
Last but not least, you could then modify the function on you page like this,
function(result, event){
if (event.status) {
JSON.stringify(result)
j$('#acctHierarchy').jstree({
'core' : {
'data' : result
}
}).bind('select_node.jstree', function(e, data){
openNewPrimaryTab(data.node.id);
});
} else if (event.type === 'exception') {
j$("#responseErrors").html(event.message + "<br/>\n<pre>" + event.where + "</pre>");
console.log(event.message + "<br/>\n<pre>" + event.where + "</pre>");
} else {
j$("#responseErrors").html(event.message);
console.log(event.message);
}
}
This is way to much than I had intended to write, but I hope it is easy to follow and you can use this to implement what you want. Try to follow my thought process and look at the documentation of jstree and all should make sense.
Cheers and happy coding
Hey, I'm quite new to visual and salesforce. I was wondering if I could ask a question about the code you provided.
ReplyDeleteI tried a simple copy/paste into apex class salesforce editor and got an error. Do I need to define the List and Map parameters such as List and Map or should your code compile correctly the way it is?
Please get back to me at your earliest convenience. Thank you,
-SF
Sorry, List(Id) and Map(Id, Account)
DeleteHi Steve,
Deletetotally my mistake which I have corrected. You do not instantiate list and maps the way i did above, totally wrong. I have corrected it. See the code above.
Also take a look at the documentation of list and maps
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_list.htm
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_map.htm
Cheers
forgot this blog removes anything in between ("<>").
Delete@RemoteAction
public static List(AccountWrapper) getChildAccounts(Id acctId){
List (AccountWrapper) aw = new List(AccountWrapper)();
Map(Id, Id) acctMap = new Map(Id, Id)();
Map()Id, Account) childAccounts = new Map(Id, Account)
([SELECT Id, Name FROM Account WHERE ParentId =: acctId]);
Map (Id, Account) potentialParents = new Map (Id, Account)
That worked for me.
DeleteAlso, I haave a question if you have some time. If its not too much of a hassle, would it be possible to edit either the apex class, vf page, or custom link as to show the entire hierarchy of the specific account id that is used when you click on an account's "view hierarchy link". IE Test-->Child1-->Child2-->Child3-->Child4-->Child5-->Child6-->Child7 so when I click "view hierarchy" (the custom link that was created" on child6, I wouldn't just see its children (which is what your code does now), I would see the entire Test tree.
DeleteI'm finding this to be quite difficult and if you could figure it out, it would save me lol
Hi Steve, see the section below the conclusion. Too long to paste here in the comments section
ReplyDeleteThank you very much. In an attempt to solve some problems until i received your response, that's partially what i did. I created a custom field called UltimateParent__c as a lookup field with respect to Accounts. A trigger was then created which would automatically associate the Account with an UltimateParent based on the other Parent Accounts. Then in the custom link "view hierarchy" I added an if statement to acctId=IF... so I am able to get all the children from the ultimate account and down. My problem still persists that I would like to see the ultimate parent when I click the view hierarchy link. IE now if I click on child6 from my previous example, I only see Child1-->Child2-->Child3-->Child4-->Child5-->Child6-->Child7 because I'm getting all children from the ultimate which is Test, but I want to see the ultimate Account as well in the hierarchy so I would want to see Test-->Child1-->Child2-->Child3-->Child4-->Child5-->Child6-->Child7. I'm wondering if there is a quick fix in your original Apex class code that could help me accomplish this?
Delete@Steve can you please email me the working code? to srinathnitb@gmail.com
ReplyDeleteHi Srinath, do you have the working code from Steve? If yes, could you send me via empcya@gmail.com?
Delete