Nine Essential Customization Tidbits for Beginners

Despite having nearly a couple years' worth of experience on SuiteCommerce Advanced, I still frequently find myself trawling through old code to find little snippets of things I do regularly. I don't know about you, but sometimes the less frequently used stuff can be hard to find.

So I had a little think about the things that have come up in my own day-to-day adventures and put them together in this list. I think it'll serve as a good point of reference for newbies and old hands alike. If not, at least it'll help me.

Extend All The Things

As we know, the most common way of getting a customization into existing code is to take the existing object and then extend, prototype and/or wrap it up with our new code. Using .extend() is easy enough: take the existing object and then make a (shallow) copy of it with some new bits tacked on.

But there are some things that can't be extended this way. For one, extending returns a new object — so what about when you want to return the existing object? What about an existing function?

Add New Properties to an Existing Object

So, you want to use an existing object but you don't want to create a brand new, separate object: you want new properties added and that's it. For this you use prototyping.

Do the following prep:

  1. Have a module set up, with its JavaScript included in the same application as the one you're extending
  2. In a JavaScript file in your new module, include the existing object as a dependency

From there you need to prototype the object with your new name. For example, let's say I want to add a new function to my home view. I could add the following:

mountToApp: function(application)
{
  HomeView.prototype.greet = function()
  {
    console.log('Salutations!')
  }
};

Thus, within the view I can now call this.greet() and it will log my greeting in console.

Override Existing Properties of an Existing Object

If you want to completely replace a property or method of an object then you can use the above method and then use the name of existing method. For example, if you wanted to completely replace the initialize method, then you could change greet in the above example to initialize.

Add New Code to Existing Properties of an Existing Object

Overriding an entire property with another is generally not a good idea as it can make maintenance difficult. When you first create the override, you're effectively copying and pasting the original code into yours and then adding in your own, but this is messy and unnecessary. Why not just write code that takes the existing function and then adds in the code you want?

Such a thing comes up when you want to add new properties to the context object. For example, say you override a template so you can add in new stuff, but you find out that you need new stuff in the context object too. To do that, you need to prototype the property and then wrap it. For example:

HomeView.prototype.getContext = _.wrap(HomeView.prototype.getContext, function(fn)
{
  var context = fn.apply(this, _.toArray(arguments).slice(1));
  context.greeting = "Howdy partner"

  return context;
});

In other words, take the existing function that generates the context, pass it through a new function, add a new property to it, and then return it (overwriting the old one).

Don't forget that when this function is performed, it's in the context of the original function. In other words, if you need to pull something from a model or some other dependency of the original object, you can without adding it as a dependency of your customization file.

Add a Custom Facet (Modifying the Configuration)

When we introduced the fancy-pants configuration tool in Vinson, it meant that you could do a lot of configuration for your site via the backend, rather than in the code. That's great. However, we know that there are limitations.

Personally, the facets configurator is pretty comprehensive and let's you do a lot of manipulation to existing/standard facets. But it becomes slightly more difficult to deal with when what you're doing is non-standard. For example, the dropdown where you select the template from is pre-populated with values pulled from its configuration file.

There's two of doing this: modify the configuration object after its been pulled from the backend, or modify the configuration file used to generate the menu in the backend. Let's first look at modifying the object.

Modify Configuration Objects

There are a number of configuration objects you can poke at: one for each application and then one global one. For facets, we need the shopping configuration file, so you'll need to add SC.Shopping.Configuration as a dependency (while calling it ShoppingConfiguration) to your customization file and then put something like the following in, say, the mountToApp function:

ShoppingConfiguration.facets = _.extend(ShoppingConfiguration.facets,[
{
  id: 'custitem_mynewfacet'
, name: _('Whatever I want to call it').translate()
, template: custom_template_i_made_tpl
, uncollapsible: true
}
])

Thus, all we need to do is redefine the object by extending the facets property with our new property. Make sure you swap custitem_mynewfacet for the ID of your facet. Note that this method is a bit hacky, as it nullifies the point of the backend configuration.

Modify JSON Configuration Files

The above method effectively intercepts the configuration files when they come back from the server and then modifies them. One of the problems that made it necessary was that the custom template I made isn't available in the dropdown. So one of the things you can do is modify that dropdown so that it is available.

It may be tempting to modify the configuration manifest file (ie the file that is generated and contains all of the configuration options to be uploaded to the backend) but this an awful idea. Instead, let's just tell it to make the changes we want.

If you're running Vinson or newer then you'll be familiar with the configuration files that can accompany modules: they're in the Configuration directories. If you want to modify one, you may not be aware that we've actually got something in place that allows you to modify the manifest without editing or overwriting the existing file.

So, to put in our facet change, your customization module will need a configuration file. Once you have that, you'll need to do something like the following:

{
  "type": "object",
  "modifications":
  [
    {
      "target": "$.resource.template.facet-navigation-item",
      "action": "add",
      "value": "custom_template_i_made.tpl"
    }
  ]
}

After deploying your code, you'll be able to make the selection from the dropdown:

So, like the configuration files, they're in JSON but we're using JSONPath, which is a proprietary library to target and transform JSON. You'll note the value of target, which you may surmise is referring to an object in a configuration file. It is, in fact, pointing to the configuration file for the Facets module, which is where the configuration for facets is determined:

Referring to our original JSONPath, we can see the target and that the action we're defining is for adding a value — and that value is our new template.

There are actually a lot of different kinds of things you can do with this functionality, such as adding to arrays and objects, modifying values, or removing them altogether. The whole point is so that you never need to modify the configuration manifest. See our documentation for more information.

Adding Child Views and Converting Views into Composite View

Let me start by saying that adding a child view is an example customization that we cover in our documentation. It's also the basis of a tutorial I wrote on how to add a free shipping indicator. If you're looking for a code snippet, your extension file needs something like the following:

CartSummaryView.prototype.childViews.FreeShippingBar = function()
{
  return new FreeShippingBarView({
    model: this.model
  })
}

In other words, you include the view you want to extend as a dependency and then you prototype the childViews property of it. Then you treat it like you would any other child view — in this case, we're adding in the view (which is a dependency of the extension file) and supplying it the model to use (which is necessary in this particular case as we need the data from it).

However, this assumes that the view you're adding to is already a composite view — so how do you make something into a composite view? For example, let's say I wanted to add the global star rating view to my details view, which isn't already a composite view. I could create a new module (called AddStars, and then create AddStars.js which has the following in it:

define('AddStars'
, [
    'Backbone'
  , 'Backbone.CompositeView'
  , 'GlobalViews.StarRating.View'
  , 'MyReviews.Details.View'
  , 'underscore'
  ]
, function
  (
    Backbone
  , BackboneCompositeView
  , GlobalViewsStarRatingView
  , MyReviewsDetailsView
  , _
  )
{
  'use strict';

  _.extend(MyReviewsDetailsView.prototype,
  {

    initialize: _.wrap(MyReviewsDetailsView.prototype.initialize, function(fn)
    {
      fn.apply(this, _.toArray(arguments).slice(1));
      BackboneCompositeView.add(this);
    })

  , childViews:
    {
      'StarRating': function() {
        return new GlobalViewsStarRatingView({
          model: this.model
        , showRatingCount: false
        });
      }
    }
  });
});

Here you can see we're taking the view we want to add the stars to, extending it, and then adding in our modifications. First, we're making use of the wrap approach I mentioned earlier to take the existing initialize function and then add in the Backbone composite view. Then we add in our childViews function to add in the star rating view.

Note, however, that if your template is not set up with an area to render the new child view, you will need to either override the template (with a new area for your element with data-view parameter) or use the follow method to add content without overriding it.

Add to a Template without Overriding It

The standard way to add something to a template is simply to make a copy of it, add in what you want, and then override the existing template with your copy. However, for small changes (ie one-line additions, such as to add an element for a child view) then you can make use of plugin container functionality.

This is something that I've talked about before so here's the gist. There are four points at which you can inject code:

  1. Before compiling
  2. After compiling
  3. Before rendering
  4. After rendering

To make use of them, you need to include PluginContainer as a dependency in your extension file, and then run its install method. For example:

MyReviewsDetailsView.prototype.preRenderPlugins = MyReviewsDetailsView.prototype.preRenderPlugins || new PluginContainer();

MyReviewsDetailsView.prototype.preRenderPlugins.install
({
  name: 'MyReviewsStarRatingContainer'
, execute: function ($el, view)
  {
    $el
      .find('.myreviews-detail-body-review')
      .before('<p class="myreviews-detail-body-rating" data-view="StarRating"></p>');
    return $el;
  }
});

The first part initializes the plugin container by first checking to see if one already exists on this object, or create one if it doesn't. The second part is the installation method I talked about: you can see that we're targeting an element and then giving it the code we want to inject. In this case, we're inserting an element with our child view parameter on it.

Thus, without having to create a copy of the template and overriding the original, I can just find a specific element and just inject some HTML into it.

Refresh a List View Page When a Model is Updated

This is something I just covered in my article about Backbone events, so I'll keep this brief.

In this scenario, you have a list view and a collection that has numerous details views and models. When one of those models is updated, added or removed, you want the page to 'refresh' automatically so that it shows the latest information. I use the word refresh lightly, because we're not actually refreshing the page in the traditional sense; instead, we're telling the view to listen for when there's been a change to a model, and then re-running the route for that view.

So do this, open up your router and head over to the function that is called when a specific record is called and then add in the following:

view.model.on('sync change destroy reset add', function (model)
{
  Backbone.history.navigate('whateverYourListRouteIs', {trigger: true});
});

Change whateverYourListRouteIs to, well, whatever your list route is. Or, you know, you can set the route to whatever you like (and it'll navigate the user there).

For more context on this, see the tutorial I wrote way back.

Extend a Service Controller

The mechanisms we need to employ for this have already been covered so I'm including this as the closing point to highlight that this is possible and that it's easy.

This topic came up when we learnt how to add reCAPTCHA to the newsletter signup form. In that example, we wanted to change the post function so that when an email address was submitted, the server would send a request to verify the reCAPTCHA first (and throw an error if it failed).

After creating a mock service controller in the SuiteScript folder of your customization module, including the service controller you want to extend, add in the following:

_.extend(OriginalServiceController, {
  post: _.wrap(OriginalServiceController.post, function(fn)
  {
    // whatever your changes are

    return fn.apply(this, _.toArray(arguments).slice(1));
  })
});

Change OriginalServiceController to whatever you named the service controller you're extending. You can also add in any additional functions you want to modify as well.

Final Thoughts

I wrote this because there have been a few useful tidbits I picked up along the way that have either been revelatory or had positive reusability value. In particular, you may note the theme running through this, which is to provide you with the right tools to add in your modifications to existing modules. If you think I've missed any out, let me know.