An alternative approach to loading form scripts in Dynamics CRM

With each browser update, full ES6 support has been getting closer and closer. But this is still sometime away, and transpilers like traceur or 6to5 can help bridge the gap in some areas. One ES6 functionality I am very much interested in, is module. As of today, no browser natively supports this, and I would have to transpile my code to get this functionality.So, I once again would like to use my favorite module loader, requirejs for doing this.

The last time I did this (https://dreamingincrm.com/2014/04/15/using-requirejs-in-crm2013/) I had to use some unsupported tricks to get this working in CRM2013. This time, my approach is to do away with CRM script loading mechanism altogether and use requirejs to load the form scripts. Here is how my resources are organised.

Events.html is the HTML webresource that will be embedded in the entity form. Here is the code for events.html;
Form Events
  <!-- data-main attribute tells require.js to load
  scripts/main.js after require.js loads. --><script src="scripts/require.js" data-main="scripts/main"></script></pre>
<ul id="events"></ul>
<pre>
main.js is the entry point into the form processing code.
(function () {
  var defaultConfig = {
    shim: {
      'lodash': {
        exports: '_'
      }
    },
    deps: ['lodash', 'common', 'ryr_eventform'],
    callback: function() {
      console.log('callback before requirejs has been loaded');
    },
    onError: function(err) {
      console.log(err.requireType);
      if (err.requireType === 'timeout') {
        console.log('modules: ' + err.requireModules);
      }
      throw err;
    }
  };
  defaultConfig.callback = function() {
    console.log('callback after requirejs has been loaded');
  };
  requirejs.config(defaultConfig);
})();
I want the scripts to load in the following order: lodash->common->ryr_eventform. If you use the form area to reference your script, you really don’t have any control over the sequence, as they are loaded async and may not be loaded in the same order you added them in the form (http://www.develop1.net/public/post/Asynchronous-loading-of-JavaScript-Web-Resources-after-U12POLARIS.aspx). Until Microsoft changes this functionality, there are two ways to overcome this issue.

1.) Bundle all your scripts in the order of their dependencies
2.) Check if the dependency has loaded. (See the waitForScript technique in the develop1 link)

I am loading the scripts using requirejs, but the triggering page is an external web resource. This way I can keep this a supported method.

This is common.js, which is required by ryr_eventform.js.

define(['lodash'], function (_) {
    var common = {
      log: function(message) {
        var e = document.createElement("li");
        e.innerHTML = new Date().toString().split(' ')
        .filter(function(d,i){ return i>0 && i<=4})
        .join(' ') +': '+ message;
        document.getElementById('events').appendChild(e);
      }
    };
    common.log('Loading common_script.js');
    //can use lodash, as it is specified as a dependency and should have been loaded
    common.log('Lodash Version: '+_.VERSION);
    return common;
});
This is ryr_eventform.js.
define(['common', 'lodash'], function (common,_) {
 common.log('Loading ryr_eventform.js');
 //can use lodash, as it is specified as a dependency and should have been loaded
 common.log('Lodash Version: '+_.VERSION);
 var Xrm = parent.Xrm;

 var form = {
  onSave: function(context) {
   common.log('Form Save Event');
  },
  onLoad: function() {
   common.log('Form Load Event');
   if(Xrm){
    Xrm.Page.data.entity.addOnSave(this.onSave);
    Xrm.Page.getAttribute('ryr_name').addOnChange(function(context) {
     common.log('Name Change Event: ' + context.getEventSource().getValue());
    });
    }
   else{
    common.log('Web Resource has not been embedded inside a CRM form');
   }
  }
 };

 form.onLoad();
 return form;
});
Here is how the form looks in the design mode.
I have not added any scripts to the form.

Since requirejs will start loading the scripts, you don’t need to worry about this.

Lets start looking at some form events now and how the script behaves.
Form Load
Name field changed
 As you, can see a script can do exactly the same things, even though it has not been been loaded through the CRM form script loading mechanism. These are are two key things that help to achieve this.
1.) The webresource folder structure
2.) Referencing Xrm object from webresource using parent.Xrm

The impetus for this post is this: I have got “The form has changed. Would you like to save your changes” dialog more than a few times and I have no idea what is the reason for this dialog.

If the change has made by a script, I have no way of knowing what the change was, and which script triggered this, unless I have added some console.log message the scripts. This is not possible if I can’t change the script. You could live edit the script using the DevTools, but I don’t want to do that.

The disadvantages of this techique, that I can see are

  1. html webresource has to be added to the form
  2. Tablet support
If CRM Client API exposes some sort of event listening capability, this would help the devs to listen to certain events like form save, form load, field onchange from the devtools console and figure out what is happening with the form, without using the debugger step through.

CRM itself, uses custom events and listeners internally, to figure out what scripts to execute for a particular event. But this functionality is not exposed externally for everyone to use. Until this is made available down the line, into some sort of Client API – Dev Mode, I can use this to control the form script loading process and audit of form events.

4 comments

  1. You list multiple ways of ensuring that JavaScript is loaded in the correct order. I’m currently using Typescript, but am not doing anything special to ensure proper js file execution. What path do you currently recommend going down?

  2. Are you using modules? If your answer is yes, and these modules are spread across multiple files, you can either have a build process that merges all the dependencies into one js or use requirejs to load the modules at run time. If you are going to head this route, have a look at https://dreamingincrm.com/2015/10/25/using-typescript-and-gulp-for-crm-client-side-scripting/

    I prefer loading the modules at runtime ondemand, but directly referencing requirejs using the standard form scripts functionality is still hacky.

    The Iframe technique referred in this post works, but it uses “parent.” to reference the global Xrm object. I don’t think this is a problem, but I haven’t tested the form behaviour in the mobile app, as the scripts are loaded in the custom IFrame context and not inside the ClientApiWrapper IFrame.

    The ghost form script that merges the dependencies on demand – I did this as a proof of concept and not using in production. I have used these two techniques: requirejs with on demand module loading and browserify to merge all dependencies into one script. The requirejs is minimal effort in terms of setting everything up, but I spent days to get the initial setup right, with the gulp+tsc+browserify route.

Leave a comment