Add a Widget Without Overriding a Module

Let's say that you've created a new widget. Now, the widget itself is reusable and could be found across a number of places on your site, and you want to add it to a module on your site. Overrides are best when you want to make extensive changes to an existing module, but what if you just want to 'inject' a bit of code?

With such a small change, and a desire to keep the widget separate and modularized, creating an override seems like overkill. Furthermore, if you want to make such a small change, creating an override makes upgrading difficult. This article takes you through the best way of doing it.

Get the Code

For the purposes of this article, I'm providing all the code in a zip file (if you want to quickly test it yourself). It's been prepared for me by one of our developers, and the widget is called ItemPriceEvolution; the idea being that you might want to show a chart showing how a matrix item's prices change over time depending on the item chosen. The module code itself is not important, but rather the method used to implement it in the modules we want.

To implement it, download the source files and add it to your sub-folder that contains your site's customizations. After that, you'll need to go through the usual steps of including it in your distribution (i.e., updating distro.json). Although not necessary for the demo, we have included a service and backend model.

After that, run gulp local to build and deploy your code to your local server.

How it Works

Visit a product detail page. You should see a "Hello world" message below the add to cart button.

So what's happened? The answer lies in ItemPriceEvolution.js:

define('ItemPriceEvolution'
,   [
        'underscore'

    ,   'ItemDetails.View'
    ,   'PluginContainer'
    ,   'ItemPriceEvolution.View'
    ]
,   function (
        _
    ,   ItemDetailsView
    ,   PluginContainer
    ,   ItemPriceEvolutionView
    )
{
    'use strict';

    return  {

        mountToApp: function (application)
        {
            // install the plugin container in the Itemdetails.View class
            ItemDetailsView.prototype.preRenderPlugins = ItemDetailsView.prototype.preRenderPlugins || new PluginContainer();

            // install a plugin that will add a box in the PDP, right before before .item-details-main-bottom-banner
            ItemDetailsView.prototype.preRenderPlugins.install({
                name: 'ItemPriceEvolutionContainer'
            ,   execute: function ($el, view)
                {
                    $el
                        .find('.item-details-main-bottom-banner')
                        .before('<div data-view="ItemPriceEvolution"></div>');
                    return $el;
                }
            });

            ItemDetailsView.prototype.childViews.ItemPriceEvolution = function()
            {
                var view = new ItemPriceEvolutionView({model: this.model});
                return view;
            };
        }

    };
});

So the first thing you notice is that we are adding ItemDetails.View as a dependency to the plugin. Normally, with an override, you'd do this the other way around.

After that we are mounting the module to the app as a plugin. This plugin is installed at the pre-render stage of ItemDetailsView. The plugin is passed the view's DOM element just before being appended to the document, so the plugin has a chance to make modifications. In this particular case, we're asking it to find a particular element with the item-details-main-bottom-banner class and then put an element with data-view on it before it. This'll be our child view, which contains the widget.

Backbone.View.render.js

For this to work, we're making use of an 'extra' included in the suitecommerce distribution under BackboneExtras, particularly Backbone.View.render.js. So the above example works by using pre-rendering, which is one of four plugin containers that you can call. For reference, here is the complete list, which can be found in Backbone.View.render.js; note that all are plugin containers that operate class level, meaning all plugins registered with them affect all Backbone.View classes:

  1. Backbone.View.preCompile
    • Plugins registered here are executed before the template function is executed and generates an HTML string.
    • Each execute method of each plugin receives the template function, the view, and the context where the template will execute.
  2. Backbone.View.postCompile
    • Plugins registered here are executed after the template function is executed and generates an HTML string.
    • Each execute method of each plugin receives the template string (the result of having run the compileTemplate function) and the view.
  3. Backbone.View.preRender
    • Plugins registered here are executed before the template result is appended to the DOM.
    • Each execute method of each plugin receives the template DOM object (before it's inserted into the DOM) and the view.
  4. Backbone.View.postCompile
    • Plugins registered here are executed after the template results is appended to the DOM.
    • Each execute method of each plugin receives the template DOM object (after it's inserted into the DOM) and the view.

Backbone.View.Plugins.js

If you want an idea of how these work, take a look at another Backbone extra: Backbone.View.Plugins.js. This file contains a number of 'default' plugins we've included with SCA. They are executed by the Backbone.View.render plugin container to do things common to all views. Take, for example, debugTemplateName. If you've ever looked at your SCA site's source code in a browser you may have seen something like the following:

What the debugTemplateName plugin does is wrap two HTML comments (one prepending and the other appending) around the template string using the postCompile method.

PluginContainer.js

Note the syntax for each plugin in Backbone.View.Plugins.js: they all invoke a install method, and specify a name, priority and execute function. Also note how Backbone.View.render.js has a dependency called PluginContainer. Take a look at PluginContainer > PluginContainer.js.

I include this last because there isn't much to say about, but looking at it (including the comments) you'll get a sense of what it does and what you can do. Pay attention to the number assigned to each plugin's priority: before execution the plugins are ordered and then executed, the one with the smallest number first. If you do not specify a value for priority then that plugin executes after the ones with values.

Appendix

Included in the zip file is another file, ItemPriceEvolution_usingEvents.js. This is an alternative that another developer came up with during discussion. In short, this achieves the same thing but instead uses events. However, we prefer the plugin approach as method wrapping is not needed and can cause complications later on.

For example, if you were to adopt this method for all your plugins then you run the risk of overriding a method that a third-party library is using, or vice versa. Running MyClass.foo() could behave unexpectedly because the method is in ThirdParty.js rather than MyClass.js.

With that said, there are times when you may feel it appropriate to use method wrapping (or the only way to achieve your goal), but in most cases use the PluginContainer module.

Summary

When adding your customizations to the site, particularly those that make a very small addition, you can consider an alternative method: adding it as a plugin that adds a child view without requiring you to override the module. It's good for widgets that are re-usable as it keeps the code separate from the places it appears in, making it easier to swap out or put in new places.