Tuesday, November 25, 2014

[Salesforce / Lightning] Loading scripts with RequireJS



UPDATE

Due to the introduction of the ltng:require, this post is no more a valid solution. Refer to the official Lightning documentation.

The following is a post that summarizes this blog post's solution (if you are not a TLDR; reader you will find there all my trials and errors).

Dreamforce 2014 came out with new awesome features on the Salesfoce platform: one of the most interesting has been the introduction of the new Lightning framework for fast development of reusable components.
In addition to the (still in pilot) Lightning App Builder (an amazing drag & drop tool for easy creation of apps), it's not hard to figure out how far this new technology will lead the Force.com platform.

The main reason you want to develop a new Lightning component is the fact that you can compose components like a puzzle making them communicating using events: read the official development guide for more details on how to build your first lightning component.

One of the things you'll be doing while developing new components, is using external libraries to give more and more features to your applications.
You'll face the following constraints:
  • You can only get external libraries loaded from a static resource
  • You cannot use the {!$Resource.resourceName} expression because we are not inside a VisualForce page, so you have to simply refer to "/resource/[resourceName]" in your <script> tags

When loading more than one external scripts, sometimes happens that the order of loading is not the expected one (even if you have inserted the script tags in the right order).

You could use RequireJS library to overcome this problem (being sure that he'll be responsible for loading libraries in the correct order).

But the question is: who is responsible to load RequireJS script?

Who will be executing the RequireJS loading code ONLY AFTER the library has been loaded?

Trying to get the right answer (see more details on the "fail" steps here and the community thread that brought me to the solution), I came to the following solution.

The solution is simple (to ease its understanding): you can improve it to add more features (e.g. style sheets loading) and to make a component out of it.

This is the main code (all the code has been packaged into this GitHub repository):

BlogRequireJSDinamic.app

<aura:application>
        <aura:handler event="forcelogic2:BlogRequireJSEvent" action="{!c.initJS}"/>
        <aura:registerEvent type="forcelogic2:BlogRequireJSEvent" name="requireJSEvent"/>
        <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
        <div id="afterLoad">Old value</div>
    </aura:application>

BlogRequireJSDinamicController.app

({
    /*
        Sets up the RequireJS library (async load)
    */
    doInit : function(component, event, helper){
        
        if (typeof require !== "undefined") {
            var evt = $A.get("e.forcelogic2:BlogRequireJSEvent");
            evt.fire();
        } else {
            var head = document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            
            script.src = "/resource/RequireJS"; 
            script.type = 'text/javascript';
            script.key = "/resource/RequireJS"; 
            script.helper = this;
            script.id = "script_" + component.getGlobalId();
            var hlp = helper;
            script.onload = function scriptLoaded(){
                var evt = $A.get("e.forcelogic2:BlogRequireJSEvent");
                evt.fire();
            };
            head.appendChild(script);
        }
    },
    
    initJS : function(component, event, helper){
        require.config({
            paths: {
                "jquery": "/resource/BlogScripts/jquery.min.js?",
                "bootstrap": "/resource/BlogScripts/boostrap.min.js?"
            }
        });
        console.log("RequiresJS has been loaded? "+(require !== "undefined"));
        //loading libraries sequentially
        require(["jquery"], function($) {
            console.log("jQuery has been loaded? "+($ !== "undefined"));
            require(["bootstrap"], function(bootstrap) {
                console.log("bootstrap has been loaded? "+(bootstrap !== "undefined"));

                $A.run(function(){
                    //do whatever GUI initialization you want
                    //in the aura context
                    $("#afterLoad").html("VALUE CHANGED!!!");
                });
                
            });//require end
        });//require end
    }
})

This is what you'll get in the developer console of your browser:
RequiresJS has been loaded? true
    jQuery has been loaded? true
    bootstrap has been loaded? true 

And you will see "Old value" string replace with "VALUE CHANGED!!!" in the page body.

What has happened?

When the app loads (init event) the doInit function tries to understand if the RequireJS library has been loaded.
If so fires a BlogRequireJSEvent event.
If not yet loaded, dynamically creates a <script> tag with the path to RequireJS, binding an onload handler, which in fact will inform that the library has been loaded, using the same event.

The same app is also an handler for the BlogRequireJSEvent event trhough the initJS function: it will load sequentially jQuery and Bootstrap libraries: this way you are pretty sure libraries will be loaded in the correct order.

The next step is to make a component out of this app so you can use a RequireJS loader component in all your apps and make all your components handling the BlogRequireJSEvent event.

No comments:

Post a Comment