External js file and CRM

Published 5/27/2008 by Henry in AJAX | CRM | Javascript

While doing a project where MS Dynamics CRM  is used a lot of customizations are performed by JavaScript.
Usually the way to it is to perform some JavaScript actions in the OnLoad of the Page.
MS Dynamics CRM has a extention point, where you can control the OnLoad of Detail Forms by entering JavaScript.

Now when you need to deploy your CRM configuration to more than one system (like we do at my project, it is sold as part of a product), you want to use a centralized Javascript file so you can change your url's etc. all in one place.
To do this (unsupported by Microsoft!) I learnt the following technique from CRM Specialists:

First technique

   1:  var script = document.createElement('script');
   2:  script.language = 'javascript';
   3:  script.src = '/_customscript/customscript.js';
   4:  script.onreadystatechange = OnScriptReadyState;
   5:  document.getElementsByTagName('head')[0].appendChild(script);
   7:  function OnScriptReadyState()
   8:  {
   9:      if (event.srcElement.readyState == 'complete')
  10:     {
  11:          // Perform onload script
  12:          //Doit();
  13:      }
  14:  }

Listing 1

The drawback with this technique is that the first time CRM loads (and every time the cache is empty) the script is not executed. Leaving the user to think the application does not work. After some time it really annoyed me, so I started to ask uncle Google again for a solution. I found the following on http://blog.odynia.org/archives/1-Javascript-Includes.html.

What this guy does is doing an AJAX call, to get the js file.
Next he loads the javascript as a string, eval() it, and imports all functions found into the current namespace, so you can access them.

It needs functionnames a-z, it cannot handle numeric values in the name of the function, but i will fix this before I will use it.
Otherwise I think it rocks! Async technique (no first time drawback)

   1:  function load_script (url) 
   2:  { 
   3:      var x = new ActiveXObject("Msxml2.XMLHTTP"); 
   4:      x.open('GET', url, false); 
   5:      x.send(''); 
   6:      eval(x.responseText); 
   7:      var s = x.responseText.split(/\n/); 
   8:      var r = /^function\s*([a-z_]+)/i; 
   9:      for (var i = 0; i < s.length; i++) 
  10:      { 
  11:          var m = r.exec(s[i]); 
  12:          if (m != null) 
  13:          {
  14:              window[m[1]] = eval(m[1]); 
  15:          }
  16:      } 
  17:  } 
  19:  load_script("/_customscript/customscript.js"); 
  21:  //perform onload scripts
  22:  //DoIt();

Listing 2


As I mentioned numbers in the functionname caused the code to fail. So I changed the regex pattern in line 8 from listing 2 into:

   1:  var r = /^function\s*([a-zA-Z_0-9]+)/i; 

Listing 3

With this regex pattern functions with numbers in the name also are added to the namespace. I added the uppercase A-Z not because functions with uppercase characters in the name where not added, but as a best practice. also you can never be sure browsers keep on using IgnoreCase as default setting.

As you can read in the comments, Marc-Andre uses the following pattern:

   1:  var r = /^(?:function|var)\s*([a-zA-Z_]+)/i; 

He wants some vars (which he uses as constants) to be added to the namespace also, maybe I would add the 0-9 here also. anyway, I think it is a good suggestion to mention here.

Addition 2:

Steve Le Mon made a very good suggestion and tried out a few things, he found a way around the parsing of the functions and/or vars and adding them to the current namespace.
I tweaked his code a little bit and ended up with the following:

   1:  function InjectScript(scriptFile)
   2:  {
   3:      var netRequest = new ActiveXObject("Msxml2.XMLHTTP"); 
   4:      netRequest.open("GET", scriptFile, false); 
   5:      netRequest.send(null); 
   6:      eval(netRequest.responseText); 
   7:  }
   9:  InjectScript('/_customscript/customscript.js');
  11:  //CallFunctionInExternalFile(); 

Listing 4

This technique removes the overhead of the parsing of the functions and vars so will perform faster.

Henry Cordes
My thoughts exactly...

The requirement is:
When a user clicks on a form's tab in a Crm detail form, a record needs to be entered into an (Oracle) database. In case of success show a webpage from a particular url (to an application that reacts to the data in the record), in case of failure show a message.
The most elegant way to react to the onclick event of the Tab, in my opinion is to attach to this event in Javascript, ofcourse it is important not to interfere with the attached handler that is already present and takes care of the showing/hiding of the tab's content.

First I took the IE Developer Toolbar, and with the 'Find' > 'Select Element by Click' functionality, I reduced the Tab's id: 'tab4Tab'.
In the Onload handler, of the Form.Properties I added this:

   1:  var tabVar= crmForm.all.tab4Tab; 
   2:  if(typeof(tabVar) != "undefined" && tabVar != null) 
   3:  { 
   4:       crmForm.all.tab4Tab.attachEvent('onclick',insertRecordAndRedirect, false)
   5:  } 
Listing 1

First the tab object with id: 'tab4Tab' is put into a variable called 'tabVar'.
When this variable is not null and not 'undefined', the function called: 'insertRecordAndRedirect' is attached to the onclick of the 'crmForm.all.tab4Tab' object.

So the function 'insertRecordAndRedirect' is called, the moment the Tab with id: 'tab4Tab' is clicked.
This function is also inserted in the Onload handler, of the Form.Properties (like listing 1):

   1:  function insertRecordAndRedirect()
   2:  {
   3:      var id = crmForm.objectId.value;
   4:      document.all.IFRAME_CrmTab.src ='http://www.henrycordes.nl/?tag=/'+id;
   5:  }
Listing 2

The function reads the value (a Guid) from the objectId property of the crmForm object (the UniqueIdentifier of the database record, from which the details are shown in the form). The aspx page that is called takes care of the inserting of the record into the database, using the provided querystring parameter.

In my case, this works really good, and it also solved an issue I had with GoogleMaps and LiveMaps. Because the tabs on which I put the maps are invisible when the form loads, the maps do not seem to know how big they are.

The result is:

  • In the case of GoogleMaps that the center of the map (the chosen spot on the map is centered) always was on (0,0). Left=0, top=0. When using the 'onclick of the Tab' approach, the map is loaded when the tab is clicked, so the map is visible and the map is centered as intended.
  • In the case of LiveMaps, only the right, lower corner of the map was visible in the left, upper corner of the iframe. But when using the 'onclick of the Tab' approach, the map is loaded when the tab is clicked and the map is completely visible and centered as intended.

Henry Cordes
My thoughts exactly...