There have been numerous blog posts on Internet talking about loading external JavaScript files in MSCRM form for reuse purpose. I am trying to stir the water with some of my thoughts.
Why External JS Files?
There could be a number of reasons that you might want to use external JS files for MSCRM form development, as it provides a number of advantages when comparing to embedding JS code in the CRM form itself.
- Using external JS files can make your script files reusable across CRM forms, possibly even across CRM projects. It's very common in every CRM project to have some shared code to be used on different forms, it's always not best practice to simply copy/paste the same code here and there, which will cause quite some maintenance headache in the future.
- The text editor provided by CRM customization tool is not really a productive tool when being used on a day-to-day basis. The editor doesn't have intellisense, no auto-completion, not even syntax highlighting. Storing form script in external files, you can use any development tool of your own favorite to write code faster with less errors.
- Using external JS files make it possible to version control your JS code using your own version control software. Although you can store your CRM customization files in your SCM repository, it is very difficult to track what changes have been made from version to version due to the size of customization file.
- You might want to use third-party JavaScript libraries in your form script, such as jQuery (I usually try to avoid this but you may have your own reason for doing this), in which case you may find that it doesn't make much sense to copy a whole big chunk of such library code to every form that you might need to use.
How do you do it?
It's well-known in the CRM community that there are two approaches to help you reference external JS files.
- The first approach injects the JS files to the head tag of CRM form's HTML file, which is basically a DOM-based technique.
// Load external JS file - CrmServiceToolkit.js. // ******* Not my recommendation though ******* var script2Load = document.createElement("script"); script2Load.language = "javascript"; script2Load.src = "/ISV/CrmServiceToolkit/CrmServiceToolkit.js"; document.getElementsByTagName("HEAD")[0].appendChild(script2Load); script2Load.onreadystatechange = function () { if (event.srcElement.readyState == "loaded") { // Do stuff here } };
- The second approach uses IE browser's XMLHttpRequest object to download the script files, then uses window.eval() or window.execScript() function to execute the code in a synchronous fashion. This approach was inspired by Robert Amos's load_script code.
// Function to load external script function loadExternalScript(url) { var x = new ActiveXObject("Msxml2.XMLHTTP"); x.open("GET", url, false); x.send(null); window.execScript(x.responseText); } loadExternalScript("/ISV/MyApp/Scripts/Common.js"); loadExternalScript("/ISV/MyApp/Scripts/FormScripts/Account.js");
Note: Be advised, window.execScript is an IE proprietary function, which means that if MSCRM ever becomes a cross-browser application in the future, this technique will not work. Hopefully by then, MSCRM will officially support external custom script files. ;)
My preference is the second approach due to its simplicity and synchronous fashion.
Using the first approach, if you ever need to load more than one JS file (which is often the case), you will have to check each file's ready status before running any of your JS code. CRM MVP Adi Katz has devised a smart solution to help address this issue that allows you to load multiple JS files using one single JS function. However the code still seems too complicated for its own simple purpose.
You may have noticed that Odynia's orignial code has a few extra lines of code than mine, as he used eval() function, which involves a tricky eval scope issue. When eval() function is used, any variable or function defined in the external JS files in the following format, which you might be expecting them living in the global scope, are actually running in the eval local scope, so as soon as the eval() finishes, your functions or variables defined in external JS files are out of scope, which makes them useless. So Odynia used the extra lines of code to make them explicitly global citizens.
// If you define you variable or function this way in external JS files, // you will have to use Odynia's extra code to make them available in global scope. var myVal = 1; function myFunc() { // Do something }
Note: If a web browser other than IE is used, there is a way to use eval() function to evaluate such variables or functions to global scope, but simply not for IE, which is the only browser supported by MSCRM at this moment.
For this reason, there is a derived simplified version at Henry's blog (section of Addition 2) based on Odynia's code using eval() function. However, there is a catchy when using Henry’s code (InjectScript function of Addition 2), you will have to make any variables or functions defined in the external JS files as implicit global ones in the following format, otherwise you will run into the eval scope issue which I just mentioned above.
// Implicit global variables and functions myVal = 1; myFunc = function () { // Do something }
Note: Be advised, implicit global variables are usually considered bad coding practice.
Another option is to explicitly define the scope of your variable and function in global window object, as shown below:
// Explicit global variables and functions window.myVal = 1; window.myFunc = function () { // Do something }
Either of the above code will have certain impact on your code’s maintainability.
Best Practices of CRM Form Script Development
With the above code handy, I think I am ready to offer some suggestions about the best practices of CRM form script development.
- In order to reuse JavaScript code and have CRM form script being version controlled in SCM, it's recommended to use a common function to load all project shared JavaScript library and form-specific code in the form’s OnLoad event. The location of commonly shared JavaScript library shall be "/ISV/MyOrgName/Scripts/", or "/ISV/MyAppName/Scripts", and the form-specific script should go to its sub-folder called FormScripts. So an entity form’s OnLoad event code might look like this:
// Function to load external script function loadExternalScript(url) { var x = new ActiveXObject("Msxml2.XMLHTTP"); x.open("GET", url, false); x.send(null); window.execScript(x.responseText); } loadExternalScript("/ISV/CrmServiceToolkit/CrmServiceToolkit.min.js"); // Third party libraries loadExternalScript("/ISV/MyApp/Scripts/Common.js"); // Shared JS library for the project loadExternalScript("/ISV/MyApp/Scripts/FormScripts/MyEntity.js"); //Form specific JS code
Note: The number of shared JavaScript files should be kept to minimum.
In case you may wonder what the heck CRM Service Toolkit is, please check out its homepage at codeplex and my another blog post for more details.
- Taking advantage of the above script for the benefit of code reusability and better maintainability, each CRM entity should have its own JavaScript files using the following naming convention:
Item Name Entity OnLoad event <EntityName>.js Entity OnSave event <EntityName>_OnSave.js JavaScript code shared by the entity’s OnLoad and OnSave event <EntityName>_Shared.js Attribute OnChange event <AttributeName>_OnChange function, which resides in <EntityName>.js file
Note: In most cases, you don't need 2nd and 3rd file, so most likely your entity will only need one JavaScript file, which is <EntityName>.js.
Note: As mentioned in the above table, you should avoid putting CRM attribute’s onchange event in separate JS files, as it only causes client-side lag and increases server load. You can include such event function in <EntityName>.js file, such as:
/* JS File: /ISV/MyApp/Scripts/FormScripts/account.js */ // BEGIN: CRM Field Events PrimaryContact_OnChange = function() { // Do stuff }; // END: CRM Field Events
- Using or modifying anything through HTML DOM is usually considered unsupported unless that has been documented in Microsoft Dynamics CRM Client-side SDK. Such code may not be compatible with future version of Microsoft Dynamics CRM. Avoid the following unless no alternative choice:
- Removing elements from the DOM.
- Moving elements in the DOM.
- Modifying any one of the form controls.
- Reusing undocumented crmForm functions.
- Anything that affects the structure of the DOM.
Note: If you ever need to write unsupported script, you should try to make them centralized.
- Avoid event handler assignment unless you really intend to do so, as doing so will overwrite all existing event handler. In most cases, it’s more preferable to use attachEvent function.
// Not recommended crmForm.all.name.onmouseover = function() { // Implementation of the event function }; // More preferable crmForm.all.name.attachEvent("onmouseover", function() { // Implementation of the event function });
Hope this helps.