Saturday, June 21, 2008

Cloning an Entity Using JavaScript

Originally wrote this piece of code for v3.0 for a client who requested a simple cloning mechanism for incidents. The idea behind the code is fairly simple because all CRM controls share a common property called DataValue.
basically all you need to do is create a simple toolbar button that opens a fresh entity page and traverse the controls DataValue.

The first requirement I stumbled on was to identify the cloning process. The problem was that In v4.0 ,by default, ms disallows the passing of unknown query string parameters. although you can change this behavior by modifying a registry settings, this would be extremely unsupported and unwise if ms would choose to delete this option on it’s next CRM version. Instead I used the window.open name parameter.


var EntWindowName = 'Cloned' + crmForm.ObjectTypeName; // e.g. ‘Clonedaccount’
window.open( EntWindowUrl , EntWindowName , EntWindowFeatures );


An alternative solution would be to add a new bit attribute to the entity form e.g. new_iscloningproc and pass a value indicating that this is a cloning process.
For example: “/userdefined/edit.aspx?new_iscloningproc=1 “(true);
MS supports this new type of parameter referencing in v4.0, take a look at the following URL for more information: URL Addressable Forms and Views

The second thing I stumbled on was getting the entity layout and URL information. I could have wrote a simple mechanism for that, however, ms already exposes a GetWindowInformation function so I decided to use that instead.


var EntUrlInfo = GetWindowInformation(ObjectTypeCode); // e.g. 1 - account
var EntWindowFeatures = 'toolbars=0,width=' + EntUrlInfo.Width + ',Height=' + EntUrlInfo.Height + ',Left=10,top=90';


The last issue I came across was to correctly set the URL format for vanilla and custom entities. Vanilla entities use a well defined folder structure e.g. "/sfa/accts/edit.aspx" ( account form ) whereas
custom entities use a logical URL e.g. “/userdefined/edit.aspx?etc=1”. ms uses the etc query string parameter to differentiate between the requested entities. In order to solve that I added a simple check on the crmForm.ObjectTypeCode.


var EtcQsParameter = (ObjectTypeCode > 9999)? '?etc=' + ObjectTypeCode : '';
var EntWindowUrl = '/' + ORG_UNIQUE_NAME + '/' + EntUrlInfo.Url + EtcQsParameter;


Here is the entire isv.config toolbar button.


<Button Icon="/_imgs/ico_18_quota.gif"
JavaScript="
function CloneEntity()
{
var ObjectTypeCode = crmForm.ObjectTypeCode;
var EntUrlInfo = GetWindowInformation(ObjectTypeCode);

var EntWindowFeatures = 'toolbars=0,width=' + EntUrlInfo.Width + ',Height=' + EntUrlInfo.Height + ',Left=10,top=90';
var EtcQsParameter = (ObjectTypeCode > 9999)? '?etc=' + ObjectTypeCode : '';
var EntWindowUrl = '/' + ORG_UNIQUE_NAME + '/' + EntUrlInfo.Url + EtcQsParameter;
var EntWindowName = 'Cloned' + crmForm.ObjectTypeName;
window.open( EntWindowUrl , EntWindowName , EntWindowFeatures );
}
CloneEntity();
" Client="Web">
<Titles>
<Title LCID="1033" Text="Clone X" />
</Titles>
<ToolTips>
<ToolTip LCID="1033" Text="Clone X" />
</ToolTips>
</Button>
<ToolBarSpacer />


Paste the code inside the entity onload event and your done.


function OnCrmPageLoad()
{
/* Indentify that this is a cloning process. */
if( window.name == 'Cloned' + crmForm.ObjectTypeName && crmForm.FormType == 1 )
{
GetOriginalEntityFields();
}
}

function GetOriginalEntityFields()
{

if( !opener )
{
alert('Missing opener');
return;
}

var re = new RegExp("INPUT|TEXTAREA|SELECT","gi");
var originalEntityFormElements = opener.document.all.crmForm.all;
var currentEntityFormElements = crmForm.all;

for( var i = 0 ; i < originalEntityFormElements.length ; i++ )
{

var OriginalElement = originalEntityFormElements[ i ];
var OriginalElementId = OriginalElement.id;

if( OriginalElementId != '' )
{
var currentElement = currentEntityFormElements[OriginalElementId];

if( typeof(currentElement.req) != "undefined" )
{
currentElement.DataValue = OriginalElement.DataValue;
currentElement.Disabled = OriginalElement.Disabled;
}
else if( re.test(currentElement.tagName) )
{
currentElement.value = OriginalElement.value;
}
}
} //end for
} //end function

OnCrmPageLoad();

49 comments:

Anonymous said...

This code works great! I do have a problem though. It works great as long as I am connected to my network. If I use the web client from within my domain the clone button works fine. When I click on the Clone button using IFD I get a page not found - 404 error. Any suggestions?

Unknown said...

I managed to make this code work if I take the CloneEntity() function and put it in the onLoad section of the entity. However, if I leave it in the ISV file, then the button appears, and tries to do something when clicked, but dosnt actually manage to bring up a cloned window. Any ideas?

Adi Katz said...

Hi Tom,

Try concatenating the Full qualified domain crm server name (FQDN) before the org name on line 10.
So instead of using a relative url e.g. /[My Org]/userdefined/edit.aspx?etc=10000
You should have http://[crm server].domain.com:[port]/[My Org]/userdefined/edit.aspx?etc=10000

Adi Katz said...

Hi teknogeez,

Try debugging the code.

If you put an alert inside the file at the top of the page, can you see it?
This will ensures that the script is loaded into the crmForm.

If it does I suggest you enable debugging and put the debugger keyword inside the GetOriginalEntityFields function and see where it breaks.

Stelmuze said...

Hi adi,

first of all - to thank you for all your shared code. I have difficulties with clone entity code. I get an error: There was an error with this field's customized event. Field: window Event:onload Error:object doesn't support this property or method. After I click OK, I get cloned entity form.

At the begining I had an error as teknogeez, I had to add the line to isv.config with button's javascript: CloneEntity();

Nick Doelman said...

This is great little function but it doesn't work with date fields.

I am trying to resolve myself and will post if I figure it out unless someone beats me to it.

Adi Katz said...

Hi Nick,

I tested the function both with Date Only and DateTime fields and didn’t see any particular problems.

The blog UI scrambled a few lines so I reedited it.

Try grabbing the code again and tell me if the problems went away.

Nick Doelman said...

Excellent, that fixed it.

Anonymous said...

Well first of all TY for sharing!

Secondly I had to add CloneEntity(); to the ISV. Otherwise I just had a dummy button. Once I did that a new page opens as if it's creating a new account with all the data. Which is great until I save and then it bombs with an error. And Yes the generic MS error. Any ideas?

Adi Katz said...

Hi David,

I suggest debugging the code.

enable debugging, then insert the debugger keyword at the top of the GetOriginalEntityFields function and see where it breaks.

This code was tested on various entities. Which entity are you trying to clone?

Adi

Anonymous said...

I have problems with the account entity like one above where the new account is filled out right but I can't save it, any solution for this ?

Anonymous said...

I can clone an appointment but NOT if the message " At least one recipient does not have an e-mail" is on the top of the original.
I guess it just counts one more field that's not on the new one.

How can we get passed this issue ?

Anonymous said...

Well adi, TY!!!!

I removed all the onload scripts I had and recopied yours. Now it works. I do have a slight twist and that if I copy quotes or salesorders I do not get products coming through to the new entry. I'm wondering if I could do this in a workflow or script?

Adi Katz said...

Actually, if you’re working with a compound (e.g. quote or order) entity your best choice would be a server side code fired from a toolbar button.

Unknown said...

I can get the new form to open and populate the form fields, however, when I attempt to save the record, I get a generic error message.

Has anybody else run into this problem?

Adi Katz said...

Which entity are you trying to duplicate? Is this a client side or server side error?
If it’s server side, open the trace log and locate the exact error message.

Angel said...

Hi Adi,
I changed the code to get the contact clone and just like others, when I try to save the cloned contact entity it gives error. I have seen that the control is also going to onload when the save button is clicked. Can you please suggest anything for this?

Angel said...

Hi Adi,
Following is the code for your reference. There is a small change in the ISV which I have modified.
I have called the method CloneEntity() in the javascript of ISV. Since the call was not made in your code, the clone was not popping up. Hence I made the change. Please get back as soon as possible. I am stuck up very badly on this.
Thanks in advance

Angel said...

Sorry I could not attach the code earlier.

//Code Added in OnLoad()
function OnCrmPageLoad()
{
/* Indentify that this is a cloning process. */
if( window.name == 'Cloned' && crmForm.FormType == 1 )
{
GetOriginalEntityFields();
}
}

function GetOriginalEntityFields()
{

if( !opener )
{
alert('Missing opener');
return;
}

var re = new RegExp("INPUT|TEXTAREA|SELECT","gi");
var originalEntityFormElements = opener.document.all.crmForm.all;
var currentEntityFormElements = crmForm.all;

for( var i = 0 ; i < originalEntityFormElements.length ; i++ )
{

var OriginalElement = originalEntityFormElements[ i ];
var OriginalElementId = OriginalElement.id;

if( OriginalElementId != '' )
{
var currentElement = currentEntityFormElements[OriginalElementId];

if( typeof(currentElement.req) != "undefined" )
{
currentElement.DataValue = OriginalElement.DataValue;
currentElement.Disabled = OriginalElement.Disabled;
}
else if( re.test(currentElement.tagName) )
{
currentElement.value = OriginalElement.value;
}
}
} //end for
} //end function

OnCrmPageLoad();
//End:Script for Cloning"

Adi Katz said...

The ISV button should not call the CloneEntity function. The ISV function should only open a new contact window. When that happens the new window onload event fires and identifies the cloning process by looking at the window name ClonedContact in your case. If you still get an error then enable debugging and put the debugger keyword inside the CloneEntity function then step through the code until it breaks.

Angel said...

Hi Adi,
Thank u so much for the reply. But the problem is not resolved. Without calling the CloneEntity() in the ISV, the Clone Contact Page does not open up. Though I put a Debugger to the CloneEntity(), the button click is not getting triggered.

Following is the JavaScript I am using.
Button Icon="/_imgs/ico_18_quota.gif"
JavaScript=" debugger;
function CloneEntity()
{
var ObjectTypeCode = crmForm.ObjectTypeCode;
var EntUrlInfo = GetWindowInformation(ObjectTypeCode);

var EntWindowFeatures = 'toolbars=0,width=' + EntUrlInfo.Width + ',Height=' + EntUrlInfo.Height + ',Left=10,top=90';
var EtcQsParameter = (ObjectTypeCode > 9999)? '?etc=' + ObjectTypeCode : '';
var EntWindowUrl = '/' + ORG_UNIQUE_NAME + '/' + EntUrlInfo.Url + EtcQsParameter;
var EntWindowName = 'Cloned';
window.open( EntWindowUrl , EntWindowName , EntWindowFeatures );
}
" Client="Web">

I feel here CloneEntity is just a definition provided and actual function call is not happening anywhere. The modifications I did are as follows.
Button Icon="/_imgs/ico_18_quota.gif"
JavaScript=" debugger;
function CloneEntity()
{
var ObjectTypeCode = crmForm.ObjectTypeCode;
var EntUrlInfo = GetWindowInformation(ObjectTypeCode);

var EntWindowFeatures = 'toolbars=0,width=' + EntUrlInfo.Width + ',Height=' + EntUrlInfo.Height + ',Left=10,top=90';
var EtcQsParameter = (ObjectTypeCode > 9999)? '?etc=' + ObjectTypeCode : '';
var EntWindowUrl = '/' + ORG_UNIQUE_NAME + '/' + EntUrlInfo.Url + EtcQsParameter;
var EntWindowName = 'Cloned';
window.open( EntWindowUrl , EntWindowName , EntWindowFeatures );
}
CloneEntity(); " Client="Web">


Please let me know how should I carry on with this.

Angel said...

Also after including the method in ISV as mentioned earlier, the clone form is getting loaded. But onSave is failing. I tried with one more modifications to OnLoad Code.

function OnCrmPageLoad()
{
/* Indentify that this is a cloning process. */
if( window.name == 'Cloned' && crmForm.FormType == 1 )
{
GetOriginalEntityFields();
window.opener.close();
}
}

function GetOriginalEntityFields()
{

if( !opener )
{
alert('Missing opener');
return;
}

var re = new RegExp("INPUT|TEXTAREA|SELECT","gi");
var originalEntityFormElements = opener.document.all.crmForm.all;
var currentEntityFormElements = crmForm.all;

for( var i = 0 ; i < originalEntityFormElements.length ; i++ )
{

var OriginalElement = originalEntityFormElements[ i ];
var OriginalElementId = OriginalElement.id;

if( OriginalElementId != '' )
{
var currentElement = currentEntityFormElements[OriginalElementId];

if( typeof(currentElement.req) != "undefined" )
{
currentElement.DataValue = OriginalElement.DataValue;
currentElement.Disabled = OriginalElement.Disabled;
}
else if( re.test(currentElement.tagName) )
{
currentElement.value = OriginalElement.value;
}
}
} //end for
} //end function

OnCrmPageLoad();

I added window.opener.close(); method. This closed the parent form as expected. But on save gave an unhandled exception in one of the Date fields. I dont know which date field failed exactly. But after window.opener.close(); the onload was not called.

Adi Katz said...

First of all remove the line that closed the parent window. You can add it after the code works. The ISV button contains both function definition and function call (last line). You should open the debugger using ie toolbar view -> script debugger -> open. Then refresh the child (cloned) window (f5) and see exactly which attribute is causing the problem. If that does not reveal which field is failing then you might need to change the conditions inside the for loop and intercept date fields.

Angel said...

I removed the window.opener.close(); code. I also added a debugger.
I had added the function call in the ISV button as it was not there in the code you had published.
I have also removed the date fields from getting added to the form. Still the control moves to onLoad() when clicked on "Save" or "save and Close".
1. Why should the control move to OnLoad when clicked on Save?
2. Should the function call of the javascript be included in onLoad() or in ISV?

Please let me know what should be done.Waiting for your reply.

Code for your refrence:
OnLoad()
{
debugger;
function OnCrmPageLoad()
{
/* Indentify that this is a cloning process. */
if( window.name == 'Cloned' && crmForm.FormType == 1 )
{
GetOriginalEntityFields();
}
}

function GetOriginalEntityFields()
{

if( !opener )
{
alert('Missing opener');
return;
}

var re = new RegExp("INPUT|TEXTAREA|SELECT","gi");
var originalEntityFormElements = opener.document.all.crmForm.all;
var currentEntityFormElements = crmForm.all;

for( var i = 0 ; i < originalEntityFormElements.length ; i++ )
{

var OriginalElement = originalEntityFormElements[ i ];
var OriginalElementId = OriginalElement.id;

if( OriginalElementId != '' && OriginalElement.className != "ms-crm-DateTime")
{

var currentElement = currentEntityFormElements[OriginalElementId];

if( typeof(currentElement.req) != "undefined" )
{
currentElement.DataValue = OriginalElement.DataValue;
currentElement.Disabled = OriginalElement.Disabled;
}
else if( re.test(currentElement.tagName) )
{
currentElement.value = OriginalElement.value;
}
}
} //end for
} //end function

OnCrmPageLoad();
}


//ISV Code
Button Icon="/_imgs/ico_18_quota.gif"
JavaScript=" debugger;
function CloneEntity()
{
var ObjectTypeCode = crmForm.ObjectTypeCode;
var EntUrlInfo = GetWindowInformation(ObjectTypeCode);

var EntWindowFeatures = 'toolbars=0,width=' + EntUrlInfo.Width + ',Height=' + EntUrlInfo.Height + ',Left=10,top=90';
var EtcQsParameter = (ObjectTypeCode > 9999)? '?etc=' + ObjectTypeCode : '';
var EntWindowUrl = '/' + ORG_UNIQUE_NAME + '/' + EntUrlInfo.Url + EtcQsParameter;
var EntWindowName = 'Cloned';
window.open( EntWindowUrl , EntWindowName , EntWindowFeatures );
}
CloneEntity();" Client="Web"

Adi Katz said...

I added the CloneEntity function call to the ISV button. Somehow along the way it got deleted. Anyway, when you click on the CloneEntity a new cloned window is opened and its onload event is fired. When you save the cloned window the page reloads again and so the onload is also called. However, the condition wrapping the call to GetOriginalEntityFields ensures the script is not executed again. If for some reason the GetOriginalEntityFields does fire then something is wrong with the wrapping condition.

Basically . the if (crmForm.FormType == 1) ensures that the cloning is done for a new entity and not for an existing one (after you save the record)

Angel said...

Adi,
Thanks again. Yes I agree on Save button it will reload. But atleast it should not reload on "Save and Close". So here atleast it should not call the onLoad().
I have tested "Save" and "Save and Close" buttons for any normal entity, if they call onLoad(). But it does not.So even On Clone, it should not go to OnLoad() according to me.
And yes, the crmForm.FormType for Save should not be 1, it should be an edit form. But no idea why the formType still shows as 1 on Cloning.

Adi Katz said...

You should remove the call to the function that does the cloning and replace it with a simple alert. Now open the child (cloned) window using the isv button > enter some data in the cloned window required fields and save. Are you still able to replicate the issue and see the alert?

Angel said...

When the CAll to the Cloning function is removed(Commented GetOriginalEntityFields() and added an alert there), the clone form opend up. I added some values and Clicked on "Save" or "Save and Close", the slert did not appear. the record got saved successfully.

Adi Katz said...

In line 29 add the following condition and tell me if it helps

if ( OriginalElementId != “” && OriginalElement.type != “hidden”)
{
//rest of code
}

Adi Katz said...

In line 29 add the following condition and tell me if it helps

if ( OriginalElementId != “” && OriginalElement.type != “hidden”)
{
//rest of code
}

Angel said...

Hi Adi,
I tried the code. It doesn't fetch any data from the parent form to the clone form if we use this. After replacing the code, even the debugger dint work for me.

I am sorry for the late reply.

Angel said...

Sorry Adi.
The code is working fine.. I had mistyped the spelling. thank u so much for ur help

Angel said...

Hi Adi,
I dont want all the fields tto be cloned. Can I clone only parentCustomer, address1, Lastname. can u please let me know how to do?

Adi Katz said...

Change line 20 from
var originalEntityFormElements = opener.document.all.crmForm.all;
to
var originalEntityFormElements = [“field1id”,”field2id”,”field3id”];

and line 26 from
var OriginalElement = originalEntityFormElements[ i ];
to
var OriginalElement = opener.document.all.crmForm.all [ originalEntityFormElements [i] ];

Angel said...

Thank you so much. It worked.

Angel said...

Thanks a lot for helping me Adi. If time permits, please help me in one more thing. I have an iframe in contact entity. It has some records related to the contact. I cannot relate those records to the clone until I save it. So I decide for a plugin, which will act asyncronously after saving the clone. Can u help me in the plugin. I have no idea on that.

Adi Katz said...

Writing your first plug-in is certainly outside of this post scope.

You should post your questions on ms forums as you go along developing it.

Unknown said...

Hi

I use the clone for calls which works great. however I can't seem to close the original form with the code.
Do you know any command that would do this?

Thanks

Steve

Tim Ganun said...

Hello Adi, thank you for great information! You mentioned that for Quotes and Sales Orders a server side solution would be best. Can you describe that or describe what is different?

Adi Katz said...

The cloning in this solution is done on the client side i.e. open a new window  go to opener  copy fields. When you require a deep cloning i.e. also cloning related (child) records you need to pass the current record id to a webservice retrieve the record and its related records information and use sdk to do the cloning. After the cloning is done return the new clone record id to the client and open a new window pointing to the returned id.

Angel said...

Hi Adi,
I have a problem here. When there are any money fields, the Currency Name' for the money field is populated as USD(which is the local currency), even though 'Currency' is not USD.


Please help me in resolving this issue by letting me know how to get the currency symbol proper.

jphuebner said...

I would like more detail on adapting this to server-side for sales order or quote entity cloning as well.
Please help.
jehuebner2000@yahoo.com

Glora said...

Hi, Thank you for this wonderful code.

I have a small issue and thought you may be able to help. Am using this code to clone a phone call activity record. However, the sender and recipient information is not getting saved. I'm able to see that the teh values get populated but do not get saved. Any thoughts on this. Thanks a ton!

Anonymous said...

Works great !

But not for hidden feilds.
any idea for hidden feilds on form...................


Jameel

Anonymous said...

I used this clone in opportunity entity. it copying all the fields well and when closing the cloned form it throwing error message.

if I clone all the fields except Actual Close Date its working fine. if I use estimated close date it throwing Microsoft error.

Matthew Mead said...

Thanks for this Adi.
I was having issues deploying the button to IFD users. They were getting the error 404 file not found once the button was clicked.
I saw your suggestion to Tom using the Full qualified domain crm server name:
http://[crm server].domain.com:[port]/[My Org]/userdefined/edit.aspx?etc=10000
I was still getting the same error until I realised the [ORG] part was not needed. I used:
http://[crm server].domain.com:[port]/userdefined/edit.aspx?etc=10000
and it works great.

felix said...

Hey Adi,
I have same issue as Glora.

If you try to use this clone function on any activity entity (letter, phone), it won't populate the sender and recipient upon save.

The value is set on the form, but when you save, it loses the value.

Any idea?

Abhijeet said...

Hid Adi,
Very useful post, this is the second time i am applying this to our project.
but have one issue this time

same issue like Glora.. mentioned, I found it's working with single recipient but if i select multiple it creates problem. I tried to skip that error with adding one more line above existing condition
if(currentElement != undefined)
if( typeof(currentElement.req) != "undefined" )

and after clone button clicks recipient appears there but not getting saved

Thanks,
Abhijeet Khake.

Kris Hempel said...

I was having issue with closing the orignal form as well. I believe the problem is caused because certain fields (lookups and dates) are copied by reference instead of value. When the original form is closed it causes a problem since that original DataValue is now gone.

I made the following changes, and things seem to be working for me.

function OnCrmPageLoad() {
/* Indentify that this is a cloning process. */
if (window.name == 'Cloned' + crmForm.ObjectTypeName && crmForm.FormType == 1) {
GetOriginalEntityFields();
opener.close();
}
}

function GetOriginalEntityFields() {
if (!opener) {
alert('Missing opener');
return;
}


var originalOppFormElements = opener.document.all.crmForm.all;
var currentOppFormElements = crmForm.all;
for (var i = 0; i < originalOppFormElements.length; i++) {
var OriginalElement = originalOppFormElements[i];
var OriginalElementId = OriginalElement.id;

if (OriginalElementId != '') {
var currentElement = currentOppFormElements[OriginalElementId];
if (currentElement.req != undefined) {
var name = currentElement.className.split(" ")[0].toLowerCase();
try {

switch (name) {
case "ms-crm-datetime":
currentElement.DataValue = new Date(OriginalElement.DataValue);
break;
case "ms-crm-lookup":
if (OriginalElement.DataValue == null || OriginalElement.DataValue[0] == null) {
continue;
}
lookupData = new Array();
var lookupItem = new Object();
lookupItem.name = OriginalElement.DataValue[0].name;
lookupItem.id = OriginalElement.DataValue[0].id;
lookupItem.typename = OriginalElement.DataValue[0].typename;
lookupData[0] = lookupItem;
currentElement.DataValue = lookupData;
break;
case "ms-crm-selectbox":
case "ms-crm-number":
case "ms-crm-text":
case "ms-crm-money":
case "ms-crm-checkbox":
case "ms-crm-radiobutton":
currentElement.DataValue = OriginalElement.DataValue;
break;
default:
// alert(OriginalElementId + " - " + name + " value: " + currentElement.DataValue);
currentElement.DataValue = OriginalElement.DataValue;
break;
}
currentElement.FireOnChange();
}
catch (e) {
alert(OriginalElementId + " : " + name + " failed.");
}
}
}
}
} //end function