- Toll Free: 1-866-SONOMA1
- Email Newsletter
- Blog
- YouTube
- Contact Us
Sonoma Partners
Streamline the Creation of New Related Records with jQuery
Posted by Jim Steger on October 7, 2009 |Today we welcome our guest blogger and colleague, Jeff Klosinski, who discusses using jQuery to create new associated records in CRM.
Do you or your users frequently create new records by opening a parent record, clicking a related entity on the left nav, and then clicking “New X” from the resulting grid? Wouldn’t it be nice if the “New X” button from the related grid were always at the top of your form?
I will show you how to do just that. On the way, you’ll see how to dynamically load a script and get to use some jQuery, too. Those not interested in the how or why can just skip to the end where I summarize what to copy where. Let’s get started!
My plan is to add an ISV button to the form, but I’ll need to give it either a URL or some JavaScript to execute…which means I need to find out how the “New Expense” button from the related grid works natively. Using the IE Developer Toolbar, I see that clicking the button executes some JavaScript:
locAddRelatedToNonForm(10007,10003,'{2D26CDCB-FB19-DE11-8B45-001EC935CFE9}', '')
I can see from the URL that the guid that the JavaScript is using is the id of the project record I currently have open, so if I give my ISV button that code exactly as I see it, clicking it would always create new Expense records related to “Initech – CRM Implementation”, the Project record I’m currently viewing. This is close to what I’m after, but definitely not right. Instead, I need to dynamically specify the id of the Project record that is currently open. Luckily, all CRM forms have a crmForm.ObjectId property that refers to the guid of the current record, which is what we’ll use in place of '{2D26CDCB-FB19-DE11-8B45-001EC935CFE9}' when I specify the JavaScript for my button. I can further see from the URL that the Project entity's type code (ETC) is 10003, so that tells me that the second parameter calls for the ETC of the current record. Another CRM form property available to me is crmForm.ObjectTypeCode, which can be used to return the correct type code at runtime. So, the XML I will add to my ISV.Config file looks like this:
<Entity name="new_project"> <ToolBar ValidForCreate="0"> <Button JavaScript="locAddRelatedToNonForm(10007,crmForm.ObjectTypeCode,crmForm.ObjectId,'')"> <Titles> <Title LCID="1033" Text="New Expense" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Expense record" /> </ToolTips> </Button> <ToolBarSpacer /> </ToolBar> </Entity>
The next image shows my button on the CRM form:
Great, just what I ordered. Well, almost. Two minor problems exist with this approach. The first is that this approach isn’t portable. It’s possible—and not uncommon—for ETCs of custom entities to be different between development, testing, and production environments. If I were to deploy this button to a client or other environment whose ETCs weren’t exactly the same as mine, my button would create new records of the incorrect type at best, and errors at worst. The second limitation is cosmetic: the button doesn't have an icon associated with it.
If neither of these two limitations concerns you, simply replace the first ETC (10007) with the one relevant to your entity, and use the code above. If they do concern you or, more likely, if you find my writing as difficult to put down as I do, then read on to see how we can use jQuery to solve both problems, resulting in an iconic button that is portable between entities and environments.
Since maintaining JavaScript that lives in the ISV.Config is a hassle, I’ll put the code that would have gone there into the onLoad event of my entity, and make the button’s ISV.Config JavaScript call one of the onLoad functions instead. In order for this to work, though, I will have to make sure to declare the button’s target function as a property of the window object so that is truly global and thus accessible by the button.
Since I’m planning on using jQuery within my code, I’ll first need to load it onto the form somehow. In this example, I dynamically load it from Google’s servers. I’ll also declare entity_OnLoad, the function which will eventually setup my icon, and newRelatedRecord, the global function which will be called by the button. newRelatedRecord will take one parameter, leftNavLabel, which the button will pass in. Here is what the code looks like:
var arr = new Array(), jQueryUrl = 'http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js', obj = document.createElement("<script src='" + jQueryUrl + "' type='text/javascript'>"); arr = document.getElementsByTagName("head"); // insert the script element before the end of the head element arr[0].insertAdjacentElement("beforeEnd", obj); // attach an event to be able to determine when the script is done loading obj.attachEvent("onreadystatechange", Script_OnLoad);function Script_OnLoad()
{
var rs = event.srcElement.readyState;
// check to see if the script is done loading
if (rs == "loaded" || rs == "complete") {
Entity_OnLoad(); // jQuery is done loading and we can continue
}
}function Entity_OnLoad()
{
// nothing yet
}window.newRelatedRecord = function (leftNavLabel)
{
// nothing yet
}
Note: Loading the jQuery library directly from Google’s servers may not be permitted in some environments. Alternatively, you could copy the library locally to your CRM Web server or even paste the code from the library directly to the top of your form’s onLoad event.
Next, I will fill out newRelatedRecord to find the ETC of the associated entity and create the button. To do this, I find the left nav link that pertains to leftNavLabel and extract the ETC from the src attribute of its corresponding img element. I will write a helper function called getIconImgByLeftNavLabel that harnesses the power of jQuery to do this pretty easily.
window.newRelatedRecord = function (leftNavLabel)
{
var img = GetIconImgByLeftNavLabel(leftNavLabel),
relatedTypeCode = img.src.match(/objectTypeCode=(\d+)&/)[1];
// Call the function that will open up the new related record.
locAddRelatedToNonForm(relatedTypeCode, crmForm.ObjectTypeCode, crmForm.ObjectId,'');
}
function GetIconImgByLeftNavLabel(leftNavLabel)
{
var jQuerySelector = '#crmNavBar ' +
'a[title="View ' + leftNavLabel + '"] ' +
'img';
var $img = $(jQuerySelector); // The name I gave this variable starts with a '$'
// to help remind me that it is the img wrapped in
// a jQuery object, not the img element itself.
return $img[0]; // Extract and return the actual img from the jQuery object
}
The regular expression in img.src.match(/objectTypeCode=(\d+)&/)[1] searches the image’s src attribute for a pattern of the form objectTypeCode=X& where X is a number containing at least one digit that is immediately followed by an ampersand (&). The [1] immediately following the match() call gives me the part of the src attributes that corresponds to my first (and only) subexpression: (\d+).
Now let’s take a closer look at getIconImgByLeftNavLabel. Note that the explanation that follows is intended to help provide a basic understanding of how jQuery selector strings work and may not necessarily represent the exact steps jQuery actually follows when processing selector strings.
The first part of jQuerySelector, '#crmNavBar ', tells jQuery to first find the left nav bar (whose id is crmNavBar). jQuery selectors were designed to emulate CSS selectors, so using ‘#’ to denote the id of an element might seem familiar. The space at the end of this part indicates that the rest of the selector refers to elements that are descendants of crmNavBar. The second part of the selector string that I am building up, 'a[title="View ' + leftNavLabel + '"] ', tells jQuery to narrow the descendants of crmNavBar down to hyperlinks whose title attribute is exactly “View Expenses”. As before, the space after the right square bracket character tells jQuery that I’m actually interested in a descendant of the link element jQuery has selected for me. Finally, I tell jQuery to give me the img element that is a descendant of the link it just found for me by specifying 'img'. Not too bad when you break it down, right?
After updating my onLoad code to fill out newRelatedRecord, I’ll also add 3 buttons to the ISV.Config file:
<Entity name="new_project"> <ToolBar ValidForCreate="0"> <Button JavaScript="newRelatedRecord('Expenses')" > <Titles> <Title LCID="1033" Text="New Expense" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Expense record" /> </ToolTips> </Button> <Button JavaScript="newRelatedRecord('Product Families')" > <Titles> <Title LCID="1033" Text="New Product Family" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Product Family record" /> </ToolTips> </Button> <Button JavaScript="newRelatedRecord('Time')" > <Titles> <Title LCID="1033" Text="New Time" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Time record" /> </ToolTips> </Button> <ToolBarSpacer /> </ToolBar> </Entity>
After publishing the entity and importing the updated ISV.Config file, my project form provides me with the following:
I could be done at this point, but without icons these buttons are sorely lacking. I’ll add some code to entity_OnLoad from the onLoad code that will look for my custom buttons, figure out which links they correspond to, and then copy their respective images to the buttons to give them a nice facelift:
function Entity_OnLoad()
{
if (crmForm.FormType !== 2) return;
var lis = $('li[action^="newRelatedRecord"]'),
spans = lis.find('span.ms-crm-MenuItem-TextFirst'),
labels = $.map(lis, function (li) {
return li.action.match( /newRelatedRecord\((\'|\")([^\'\"]+)/ )[2];
});
$.each(labels, function (i, label) {
// find img element using the helper and rewrap as jQuery object
// so that we can use jQuery's clone and insertBefore functions
$(GetIconImgByLeftNavLabel(label)).clone().insertBefore(spans[i]);
});
}
In the code above, I first check to make sure the form is an update form because it wouldn’t make sense to add a related record to a record that doesn’t exist yet (if we were on a Create form) and, natively, CRM doesn’t allow you to add related records to deactivated records (if we were on a Disabled form). Next, I find the elements that I will need. The variable lis is a jQuery collection of li elements whose action attributes start with "newRelatedRecord". Next, for each of the li elements found, I have jQuery give me a collection of descendant spans that have a class attribute of ms-crm-MenuItem-TextFirst and store it into the variable spans. Now I need an array whose cells correspond to each of the elements in lis, the values of which should be the labels I configured as parameters to the newRelatedRecord function when I specified the JavaScript for my buttons in the ISV.Config (i.e. Product Families from newRelatedRecord('Product Families')). I can easily construct this array with jQuery’s map utility function, which allows me to easily apply some processing to each cell in an array, mapping the results to a new, congruent array. In this case, I iterate over each element in lis and return the result of a regular expression that extracts the label in which I am interested. Then, I iterate over each of the cells in my labels array, using each label to grab (using the previously defined helper function) and clone its corresponding image, which I then insert directly in front of each of the corresponding spans I defined previously. Whew! As you can see, we can accomplishing a great deal with very few lines of jQuery code.
This means we’re finally done!
The final code for this sample is below. Add buttons to the entity that will be getting the buttons at the top of its form to the ISV.Config, updating the entity name and the parameter for the newRelatedRecord function with your appropriate values.
<Entity name="new_project"> <ToolBar ValidForCreate="0"> <Button JavaScript="newRelatedRecord('Expenses')" > <Titles> <Title LCID="1033" Text="New Expense" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Expense record" /> </ToolTips> </Button> <Button JavaScript="newRelatedRecord('Product Families')" > <Titles> <Title LCID="1033" Text="New Product Family" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Product Family record" /> </ToolTips> </Button> <Button JavaScript="newRelatedRecord('Time')" > <Titles> <Title LCID="1033" Text="New Time" /> </Titles> <ToolTips> <ToolTip LCID="1033" Text="Create a new Time record" /> </ToolTips> </Button> <ToolBarSpacer /> </ToolBar> </Entity>
Next, paste the following code where you specify onLoad code for the entity to which you added the buttons.
var arr = new Array(), jQueryUrl = 'http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js', obj = document.createElement("<script src='" + jQueryUrl + "' type='text/javascript'>"); arr = document.getElementsByTagName("head"); arr[0].insertAdjacentElement("beforeEnd", obj); obj.attachEvent("onreadystatechange", script_OnLoad);function script_OnLoad()
{
var rs = event.srcElement.readyState;
if (rs == "loaded" || rs == "complete") {
entity_OnLoad(); // jQuery is done loading and we can continue
}
}function entity_OnLoad()
{
if (crmForm.FormType !== 2) return; // return if not an update form
var lis = $('li[action^=newRelatedRecord]'),
spans = lis.find('span.ms-crm-MenuItem-TextFirst'),
labels = $.map(lis, function (li) {
return li.action.match( /newRelatedRecord\((\'|\")([^\'\"]+)/ )[2];
});
$.each(labels, function (i, label) {
$(getIconImgByLeftNavLabel(label)).clone().insertBefore(spans[i]);
});
}window.newRelatedRecord = function (leftNavLabel)
{
var img = getIconImgByLeftNavLabel(leftNavLabel),
relatedTypeCode = img.src.match(/objectTypeCode=(\d+)&/)[1];
locAddRelatedToNonForm(relatedTypeCode, crmForm.ObjectTypeCode, crmForm.ObjectId,'');
}function getIconImgByLeftNavLabel(leftNavLabel)
{
var jQuerySelector = '#crmNavBar a[title="View ' + leftNavLabel + '"] img';
var $img = $(jQuerySelector);
return $img[0];
}
Publish the entity and import the ISV.Config file, and you have a versatile, user-friendly way to create new related records from any entity’s toolbar. This example barely scratches the surface of jQuery, so I hope you will consider it for your own CRM scripting needs. Good luck!
Comments
Post a Comment
Contact Us for a Quote, or Personalized Demonstrationof Microsoft Dynamics CRM for Your Business.
Contact Us
Previous Post
Back to Blog
very helpful post. To help future readers, you may want to update the
"NewRelatedRecord('Expenses')" statements in the xml file to be a lowercase "newRelatedRecord('Expenses')" since that is what is in the javascript.
Posted by: xrmGuru | Oct 10, 2009 11:17:56 PM
Nice example! Thanks, Jim.
Posted by: Tim Klooster | Oct 26, 2009 1:27:37 PM
Thanks. I think this article might come handy sometime :)
Posted by: Kamaldeep | Nov 10, 2009 4:46:28 AM