Post Featured Image

Add a Third-Party JavaScript Library to an Extension

This blog post is written for sites using the commerce extension framework. Therefore, it is only appropriate for SuiteCommerce sites, or SuiteCommerce Advanced sites running Aconcagua or newer.

Way back in 2015, when we started this project, a common request from developers was for an explanation on how to add a third-party JavaScript library to a site. You know, you've got your site set up, but you want to add a cool random number generator, jQuery plugin, or maybe just some random JS that infects your global namespace. With the advent of extensions, the question has arisen again because the old tricks no longer apply.

Well, I'm here to tell you that there is a way to add third-party JS to your site, and it's not too difficult. Before I do, however, note that what we're about to talk about is a bit of a workaround. You see, we're currently looking into the best way of adding the concept of library files to the framework, so what we're about to learn may become obsolete when we provide a better, 'official' way of doing it.

Nevertheless, we reckon that the following methods are pretty solid for the timebeing and they cover a few scenarios:

  1. AMD-compatible libraries
  2. AMD-compatible libraries that don't seem to work with RequireJS
  3. Non-AMD-compatible libraries

We'll look at each of these in turn, so let's get started!

General Philosophy

Before we do, I want to talk about some general best practices before approaching this.

Our devs think that if you're going to use this, it's probably best that you approach the situation with a 'one library per extension' mindset. What this means that if your site requires a number of separate, independent library files, break them up so that they are separate, independent extensions.

This isn't a requirement, but the idea is that should your site's circumstances change, you can quickly turn off a particular library by re-activating your site's extensions without it enabled. If you have all of your libraries in one place, then you'll need to edit the code, remove any files you don't want, and then re-upload and re-activate. You'd be just giving yourself extra work.

For the purposes of this tutorial, I'm going to be lazy and put all of my libraries in one extension, but only because this is a teaching experience. Do what I suggest, not what I do.

Next, for situations where libraries aren't AMD-compatible, consider putting in the work to make them so. This may mean wrapping an entire file in a define statement, converting its functionality into public methods, etc. If this isn't possible, we'll look at how you can create an AMD wrapper, which calls the file when needed and let's you do some dependency management.

Basic Setup

So, as I said, I'm going to use a single extension for all of my library files. In your developer tools, create an extension — I'm naming mine Libraries and setting myself as the vendor.

After doing the initial prep work, head into Workspace directory and create a folder structure like this:

Libraries

  • assets
  • Modules
    • Libraries
      • Libraries.js
  • manifest.json

We're going to add more folders for each of the library files we're going to experiment with, but for now this is fine.

AMD-Compatible Libraries

Remember, SuiteCommerce relies on RequireJS, which is how manage dependencies. Every JavaScript file is given a name using define and an opportunity to declare which JavaScript files they depend on. It's all very clear and civilized.

So, if your library file is already written to be AMD-compatible... you can just include it in your file as normal in the define statement.

You can create a folder for it in your modules directory, along with a JavaScript folder, and then add it to that. After that, just go into whatever file you want to reference it in an extension and it as a dependency, give it a name, and then call its methods or properties. This is pretty standard and shouldn't present any problems. That is unless they error... 🤔

AMD-Compatible Libraries That Error

Did you get this error?

Error: See almond README: incorrect module build, no module name

You're probably looking at the code the author provided and saw that it contains a whole bunch of statements that seem to check what kind of dependency management system exists, and then return the correct statement for it. Well, there's an additional nuance to what SuiteCommerce uses; it's called almond.

Essentially, there is a need to load AMD dependencies and compute the final JavaScript. We use almond because it is lighter weight for AMD loading than the native RequireJS one. However, one nuance is that it mandates that you declare a name at the time of the define statement; quite often, third-party developers won't include these in their definitions, creating anonymous modules.

You can read more about this on their GitHub page.

Anyway, as long as the file is written to be AMD-compatible, we can work with it; we just need to tinker with it a bit. It becomes more than just making it AMD-compatible: it's now about making it compatible with almond.

Simple Example: jquery.matchHeight.js

jquery.matchHeight is a jQuery plugin that a customer recently asked me about (specifically about adding it as a library). As you can read on their GitHub, it is a jQuery plugin that makes the height of all specified elements (using a jQuery selector) equal. This might be useful for SuiteCommerce sites on product list pages, where, for example, differences in the lengths of item names can cause irregularities in the presentation of product list pages.

To illustrate this, take a look at this screenshot from my website:

The third item in this row has a name that fits on one row, while all the others go onto two; this has the knock-on effect of pulling up all the other elements in the item cell. This is the sort of thing that this plugin fixes and it's something we can add to our site.

Create Modules > jquery.matchHeight > JavaScript and put the latest version of jquery.matchHeight.js into it. You'll notice that it fits the above description: a conditional-laden attempt to ensure that it's compatible with various AMD loaders:

;(function(factory) { // eslint-disable-line no-extra-semi
    'use strict';
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof module !== 'undefined' && module.exports) {
        // CommonJS
        module.exports = factory(require('jquery'));
    } else {
        // Global
        factory(jQuery);
    }
})(function($) {
...

Note the conditional that checks whether define is a function — this is the one that will trigger for SC sites, but it'll fail because, as you see, we add jQuery as a dependency, but don't set a name.

Lucky for us, however, this is all a simple change. Replace the code sample above with the following:

define('jquery.matchHeight', ['jQuery'], function ($)
{

We now have a proper define statement, in terms almond requires. Seriously, that's it (except, just make sure you remember to put a closing }) at the end!).

From here, we can work it into our main code. For the purposes of this tutorial, I'm going to add it to Libaries.js (but you probably want to add it to the place where you keep all of your PLP customizations):

define('Libraries'
, [
    'jQuery'
  ]
, function
  (
    jQuery
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      this.PLP = container.getComponent('PLP');
      this.testJqueryMatchHeight();
    }

  , testJqueryMatchHeight: function testJqueryMatchHeight ()
    {
      if (this.PLP)
      {
        this.PLP.cancelableOn('afterShowContent', function ()
        {
          jQuery('.facets-item-cell-grid-title').matchHeight()
          jQuery('.facets-item-cell-grid-link-image').matchHeight()
          jQuery('.product-line-stock-description-msg-description').matchHeight()
        });
      }
    }
  }
});

As it's now an almond-compatible module, we add it as a dependency, just like any other. If we don't, it won't be included in the compilation process and we won't be able to access it.

Then, in our mountToApp method, we're going to set up the PLP component and call a method to test that it works.

This test method, testJqueryMatchHeight(), is going to run the jQuery after the PLP has run its showContent() method (which renders the page). At that time, we'll call the matchHeight() method that the library adds, by targeting the class that every item title has.

If we spin up our local server to test this, we can see that it has taken effect and that it now looks like this:

Looking good 😎

If we wanted, we could add in additional jQuery calls to standardize the heights of the images, stock descriptions, etc.

Non-AMD-Compatible Libraries

Let me preface this section that, where possible, you should make use of AMD. If you can, put in the work to convert the library you are working with AMD-compatible.

Next up, I'm going to look at the Mersenne-Twister pseudo-random number generator. You may remember it from when I took us on a technological diversion in my post on how to build a random prize competition service. We might use MT for business-critical scenarios that require random numbers, as the built-in Math.random() gives sub-optimal random numbers.

In that post, I put in some work to convert the file so that it was AMD-compatible, which included adding in a define statement, removing its global namespace, returning its functions as public methods, etc. Well, normally I would suggest doing that but let's say that you don't want to (or can't) modify the file; or, perhaps, you like the idea of having the namespaced constructor to return random numbers (eg MersenneTwister().random()) — what do you do?

In these scenarios, if you really must, then you can call the script via AJAX. The general idea is this:

  1. Create a separate module or extension for the library
  2. Add the library to the extension's assets folder
  3. Use jQuery.ajax() to call the script in its entry point file
  4. Return the deferred object so that we can wait for it to load before using its functionality

I should stress that this may prove to be problematic not only in the future but in how you code. The prime problem with this is that you must wait for the script to load before you can use it as you are not using normal SuiteCommerce code to manage dependencies. Anyway, if you're sure, let's take a look at how we might do this.

Basic Setup

I'm going to build this into my existing extension, though, like I said, you should consider splitting it off.

Start by creating Libraries > Modules > MersenneTwister > JavaScript > MersenneTwister.js.

Then, in Libraries > assets, create a folder called javascript and put a copy of mersenne-twister.js in it. Importantly, before we can test this locally, you will need to upload a copy of this to your site as it will be served from NetSuite. You can do this by deploying your extension and activating, or by uploading it to the place it's going to be served from in the file cabinet. For testing purposes, I'd recommend the latter. For me, this is going to be in Web Site Hosting Files > Live Hosting Files > SSP Applications > NetSuite Inc. - SCA Aconcagua > Development > extensions > Steve Goldberg > Libraries > 1.0.0 > javascript — adjust this for your site but I recommend following the pattern that the extensions framework will use as we're going to rely on one of its new helper functions.

General Philosophy

With everything in place, we can get to work on calling it.

For this, we're going to use jQuery.ajax() because it gives us a few extra features that will be useful: in particular, we can attach success and error handlers, which means that we don't have to use the deprecated async: false XHR flag. If you don't know, AJAX calls are made asynchronously by default (the clue's in its full name) which means that when we call the script, we don't lock the browser until it finishes (as this is a bad user experience). Note, however, that depending on your library, you may still want to do this, although web standards are changing and the recommendation is that you don't. Loading this script asynchronously will mean that we will have to do a bit of extra work in our code whenever we want to use it; or you may consider the library so important that you want to have finished loading before, say, the page is rendered. I'll leave that decision up to you (but I recommend asynchronous loading generally).

The reason we can rely on asynchronous loading is because we're going to use the fact that the ajax() returns a deferred object to ensure we don't run code that relies on the file before it's finished loading. Deferred objects pop up all the time in SuiteCommerce code — for example, when we fetch data using a model or collection, we will create a deferred object and only resolve it when we know the data has finished being sent from the server.

Another general idea is that we still want to do AMD as much as possible, so we're going to create a module for the library, with the entrypoint file acting as an API for it. So, for example, we can add a random() method to the file that wraps the random() method of the MersenneTwister namespace. That way, after we've added the file as a dependency to our other file, we can just use MT.random(). This might be too much for your uses, but this is up to you.

Set Up the Entrypoint File

Anyway, in MersenneTwister.js, put the following code:

define('MT'
, [
    'jQuery'
  , 'HandlebarsExtras'
  ]
, function
  (
    jQuery
  )
{
  'use strict';

  var promise = jQuery.ajax({
    url: getExtensionAssetsPath('javascript/mersenne-twister.js')
  , dataType: 'script'
  , complete: function (e)
    {
      console.log('Mersenne-Twister: ' + e.statusText)
    }
  , cache: true
  });

  return {
    promise: promise

  , random: function random ()
    {
      return new MersenneTwister().random()
    }
  }
})

First, we are calling the file MT rather than MersenneTwister because the latter is the global namespace that the library itself uses; if we were to call our file that, we could run into some issues if we use the same name when we require it in another file as a dependency (so best to pick an alternative).

Then, after adding jQuery as a dependency, we make the AJAX call. Note that we're using getExtensionAssetsPath(), which is actually a Handlebars helper we provided to make the handling of versioned assets easier. Yeah, we're not in the context of Handlebars here, but this is what the method is designed to do: fetch the correct path for an asset file.

dataType specifies that we're fetching a script, and complete is simply a callback that runs when the request has a response (success or fail) from the server (this is optional, so you can remove it if you like); we include cache: true to store a copy of the file on the user's device so that subsequent calls are fetched from the their cache, rather than the web server (this should help cut down on redundant calls, should they occur).

Then we just return the promise object, which will be used to check whether the file has been loaded successfully before trying to use its methods. You'll note that we're using also returning an interface method for the random() method: as previously mentioned, you can use this so that you never directly use the constructor's functions... or not, it's up to you. This library creates a global namespace, so we don't have to worry about it in this example, but you may need to do it depending on the file.

Call The Library In Another File

So, it's kinda set up and ready to use, so let's just take a look at this.

Back in Libraries.js, add MT as a dependency, and then the following code to your mountToApp() function:

MT.promise.done(function ()
{
  console.log(MT.random());
});

What this will do is check the state of the promise and when it's done, run some code. In our simple example, we're just going log a random number to the console, but if you were going to do something more complex, that whole code would need to be contained in a done() method for the promise, or else you risk throwing errors stating that the thing you're calling is undefined.

Anyway, if you save your files and restart your local server, you should see this in your console when you visit any page:

🎉

Final Thoughts

We're looking into formalizing better mechanisms for deploying and managing third-party libraries on SuiteCommerce sites. However, in the meantime I've provided some instructions on what we consider our best practices for working around the situation.

Here are my thoughts, condensed down:

  1. It's probably better to have each individual library in a separate extension so that it is easier to switch off ones that are no longer needed
  2. AMD compatibility is best
  3. Converting non-AMD-compatible files to AMD is second best
  4. If you can't do that, create an AMD-compatible wrapper for the library file, where the library itself is uploaded as an asset
  5. The wrapper calls the file using AJAX and returns the deferred object
  6. Use this object to wait for when the file is done loading before trying to use its contents
  7. Synchronous AJAX calls can be used to help mitigate this, but they're deprecated and may cause alternative problems

If you want to see a final version of my code, see Libraries.zip.