Use Multiple Modules in an Extension

When developing extensions, there are cases where it is best to use multiple modules to organize the files and assets for your customization. For example, it may be easier to manage and maintain your extension if you organize the files for each separate function that the extension performs into separate modules. And, multiple modules are ideal for scenarios where you want to run different code in different parts of the site, but you want to contain all the code within the same extension.

Note:

The examples in this topic assume that you are familiar with extension files and folders and have some experience developing and testing extensions. If not, refer to these topics for more information, Extension Development Files and Folders, Anatomy of an Extension, Get Started with Extensions.

These are the basic steps to include multiple modules in your extension:

  1. Add an additional module to the Modules folder.

  2. Add needed files to the new module. An additional entry point file is not necessary.

    Note:

    If you do add another entry point file with a mountToApp() function, it is not automatically called when the module is loaded.

  3. Add the files from the second module as dependencies to the files in your first module, as required.

  4. Call the methods from the files within the additional module like you normally do with the methods from the original module.

Read the following sections to learn more:

Folder Structure

As described in Extension Development Files and Folders, the Workspace directory contains a subdirectory for each extension you are creating. And a Modules folder is included in the subdirectory for each extension. The Modules folder usually contains one module/folder that is also named after your extension, like so:

<TopLevelDevelopmentDirectory>
   Workspace
      MyCoolExtension
         assets
         Modules
            MyCoolModule
         manifest.json 

However, the name of the folder for each module is flexible and you are not required to name each module after your extension. This topic guides you through several examples that are based upon the following directory structure:

<TopLevelDevelopmentDirectory>
   Workspace
      MyCoolExtension
         assets
         Modules
            ModuleOne
               JavaScript
                  ModuleOne.js
            ModuleTwo
               JavaScript
                  ModuleTwo.js
         manifest.json 

To create entry point files, add code like the following to the ModuleOne.js and ModuleTwo.js files:

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

  return {
    mountToApp: function ()
    {
      console.log('ModuleOne loaded!');
    }
  }
}) 
Note:

Be sure to change ModuleOne in the preceding code sample to ModuleTwo for the ModuleTwo.js file.

Entry Points

When developing customizations for the Kilimanjaro (2017.2) and earlier releases of SCA, you needed to create an entry point for each module. However, with the introduction of the extension framework and extensions in the Aconcagua release, this is no longer necessary. When working with extensions, there is typically one entry point for the entire extension as illustrated in Single Entry Point for an Extension. The only exception is when you want different entry point files for the different applications on your site as shown in Different Entry Points for Site Applications.

Single Entry Point for an Extension

If you created the files and folders described in Folder Structure, you can walk through this example to see how a single entry point works.

In the extension’s manifest.json file, add the path to the ModuleOne.js file as the entry point for each application your extension will be applied to. For example, if your extension only applies to the checkout application, set the entry point as shown in the following example:

"javascript": {
        "entry_points": {
            "shopping": "Modules/ModuleOne/JavaScript/ModuleOne.js" 

Now if you run the gulp command to update the extension manifest:

gulp extension:update-manifest 

And then run a local test of your extension with this gulp command:

gulp extension:local 

When you access your local site and check your browser’s developer console, you see this message:

ModuleOne loaded! 

This result is expected because, although the extension’s manifest file has been updated and includes the ModuleTwo file in its list of files, you have not called the second module.

When your extension is loaded, only the entry point file in the manifest is called. Therefore, if you want additional code to be called, you need to add calls to that entry point file’s mountToApp() method. To learn more, see Calling Files in Other Modules.

Different Entry Points for Site Applications

If you created the files and folders described in Folder Structure, you can walk through this example to see how to set up different entry points for the applications on your site.

If you need to have different entry points for the applications on your site, you can modify the entry_points object in your extension manifest as shown in the following example:

"javascript": {
    "entry_points": {
        "shopping": "Modules/ModuleOne/JavaScript/ModuleOne.js",
        "myaccount": "Modules/ModuleTwo/JavaScript/ModuleTwo.js",
        "checkout": "Modules/ModuleTwo/JavaScript/ModuleTwo.js"
    }, 

If you modify your extension’s manifest.json file, as shown in the preceding code sample, and save it, you can restart your local server to test this modification. Now when you access the checkout or customer account areas of your local site, the following message displays in your browser’s developer console:

ModuleTwo loaded! 

This approach works well when you want to run different code in different parts of the site, but you want to contain all the code within the same extension. This approach also helps you manage dependencies.

Calling Files in Other Modules

If you created the files and folders described in Folder Structure, you can walk through this example to see how to call files in other modules.

Reset the manifest.json file so that all three applications use the same entry point as shown in the following code sample:

"javascript": {
    "entry_points": {
        "shopping": "Modules/ModuleOne/JavaScript/ModuleOne.js",
        "myaccount": "Modules/ModuleOne/JavaScript/ModuleOne.js",
        "checkout": "Modules/ModuleOne/JavaScript/ModuleOne.js"
    }, 

If you modify ModuleOne.js to add ModuleTwo as a dependency, you can then call it from within ModuleOne.js. For example, you can call ModuleTwo’s mountToApp() method as shown in the following code sample:

define('ModuleOne'
, [
    'ModuleTwo'
  ]
, function
  (
    ModuleTwo
  )
{
  'use strict';

  return {
    mountToApp: function ()
    {
      console.log('ModuleOne loaded!');
      ModuleTwo.mountToApp();
    }
  }
}) 

If you save these changes and then restart your local server to test these modifications, the following message displays in your browser’s developer console:

ModuleOne loaded!
ModuleTwo loaded! 

This output verifies that calling the file in the second module is working as expected.

Calling files in other modules is not limited to entry point files. This approach works for other types of files in your extension, too. You can reference any file, like views or models, if the file has been properly defined and made available to the application. So, instead of having an entry point file in your second module, you could have a view, model, or any other type of supported file instead. As shown in the preceding code sample, you can simply add it as a dependency and call one of its methods.

Asset Organization and File Reuse

If you want to organize the files in your extension’s top-level assets directory to group them by module, you can. The following sample directory structure shows how you may organize your images:

assets
      fonts
      img
            ModuleOne
                  img.jpg
            ModuleTwo
                  img.jpg
      services 

With your image files organized in this way, you could write code to:

  • Load the ModuleOne image file when the ModuleOne entry point file is loaded.

  • Load the ModuleTwo image file when the ModuleTwo entry point file is loaded.

All you need to do is create a dynamic path to the image based on the entry point loaded.

For example, you may want text and a small image to appear in your site’s header, but you want the text and image to be different, depending on which application is in use.

The following example shows how to make this scenario work by reusing the view and template files from ModuleOne in ModuleTwo. The only thing that is different is that you duplicate the ModuleOne entry point file and change its name (and a value in the file that captures its name).

First you edit the manifest file so that ModuleTwo’s entry point is loaded when the shopper accesses the checkout application:

"javascript": {
    "entry_points": {
        "checkout": "Modules/ModuleTwo/JavaScript/ModuleTwo.js",
    }, 

The ModuleOne entry point file looks like this:

define('ModuleOne'
, [
    'ModuleOne.View'
  ]
, function
  (
    ModuleOneView
  )
{
  'use strict';

  return {
    name: 'ModuleOne'

  , mountToApp: function (container)
    {
      console.log(this.name + ' loaded!');
      var Layout = container.getComponent('Layout');

      var self = this;
      Layout.addChildView('cms:header_banner_top', function ()
      {
        return new ModuleOneView({moduleName: self.name});
      });
    }
  }
}) 

The ModuleTwo entry point file looks identical, except for the name in the opening of the define statement and the value of name. The ModuleTwo entry point still adds the same view and also dynamically passes the module name to the view constructor:

define('ModuleTwo'
, [
    'ModuleOne.View'
  ]
, function
  (
    ModuleOneView
  )
{
  'use strict';

  return {
    name: 'ModuleTwo'

  , mountToApp: function (container)
    {
      console.log(this.name + ' loaded!');
      var Layout = container.getComponent('Layout');

      var self = this;
      Layout.addChildView('cms:header_banner_top', function ()
      {
        return new ModuleOneView({moduleName: self.name});
      });
    }
  }
}) 

The view for this example scenario looks like this:

define('ModuleOne.View'
, [
    'Backbone'
  , 'module_one.tpl'
  ]
, function
  (
    Backbone
  , module_one_tpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: module_one_tpl

  , getContext: function ()
    {
      return {
        message: 'This is ' + this.options.moduleName
      , image: 'img/' + this.options.moduleName + '/img1.jpg'
      }
    }
  })
}) 

When the view is created, it receives whatever options the constructor is given and attaches them to its own options property. For this case, you pass it the name of the module, which you are using to build the path for the image URL, as well as a message to be passed to the template.

The template for this example scenario is simple:

<p>{{message}}</p>
<p><img src="{{getExtensionAssetsPath image}}"></p> 

If you save these changes and then restart your local server to test these modifications:

  • When the ModuleOne entry point is loaded, the image.jpg stored in assets/img/ModuleOne is displayed

  • When the ModuleTwo entry point is loaded, the image.jpg stored in assets/img/ModuleTwo is displayed

Both the view and template are completely reusable in this context, and you can build them out however you want. If you need to diverge and create a customization that is better suited to another scenario, you could modify the view and templates with more conditionals, or you could simply go back to having separate files. It's really up to you. The main point of this example is to illustrate that you can use cross-module files in your extensions.

Related Topics

Best Practices for Developing Commerce Extensions
General Best Practices
Use Custom Handlebars Helpers

General Notices