Test out the Extensibility API with Three Examples

Among the fanfare of new features, improvements and refactoring of the Elbrus release, the SuiteCommerce developers have hidden three little gems in the codebase that you may have missed. If you take a little look in Modules > extensions, you'll see three sample extensions that can be used as the basis of new customizations for your site:

  1. MyExampleCartExtension1@1.0.0
  2. MyExamplePDPExtension1@1.0.0
  3. MyExamplePDPExtension2@1.0.0

In the parent directories for each of these modules is a README.md that contains a simple explanation of what the module does as well as how to install it on your site.

Also in each parent directory are all the files that make up the customization and the dev team have added in some inline comments to explain what they're doing. While they are perhaps not things you would want to implement as-is, they provide a solid groundwork on which you can build your own extensions. So, with that, let's take a look at them and see how we could use them.

Cart Extension Example

The first example extension is a SuiteScript file that modifies add to cart behavior. Specifically, it ensures that a shopper may not add a particular item to the cart more than 10 times. The entirety of this solution is contained within SuiteScript > MyExampleCartExtension1.js.

Before we dive into the solution, let's take a brief look at the problem this file solves and how we might have approached it. So, our business administrator has told us that in order to ensure that there's enough stock to go around, they don't want customers buying more than 10 of a particular item (at least, in one order). It's up to us to prevent this happening on the ecommerce site.

Now, we could approach this by simply putting up a note on the site telling people if they would be so kind as to not put through orders with more than 10 of a particular item. But that's not good enough, right? I mean, we're not even validating anything. They could put the items in the cart and check out and no would stop them, right?

We could try adding validation to the frontend, you know: change the quantity box so that it only accepted numbers smaller than 11. But, firstly, what's stopping some clever person editing the page and just removing that check (or disabling JavaScript)? Also, what would happen if someone were to add 10 items and then add some more items afterwards?

So that's where we are: we need to run validation on the cart. We need to make sure that no matter how hard they try, the shopper can never put in more than 10 items. If they try to, they should be prevented from doing so and told what happened.

Knowing that we need to look at the backend of the cart, we can head over to the module that governs it: Cart > SuiteScript. In there we have Cart.Component.js; in this file we have a list of the all the methods that are attached to the cart. There's two here that are particularly useful for us: addLine and updateLine. Why? Because these are the methods used to increase the items in the cart. These are ones we need to listen to.

I say 'listen' because these are events, and as events we can wait for them to be called and then tell the application to do some actions either before or after they've finished running. We talked a little bit about backend events when we looked at the new service controllers.

In MyExampleCartExtension1.js we are making use of events to create a listener to fire before the addLine method's code is run.

//Get the cart component
var cart_component = Application.getComponent('Cart');

//Listen to the beforeAddLine event.
//This is triggered before each line is added even when addLines is used.
cart_component.on('beforeAddLine', function(event_data){
  //Get the line from the event data
  var line = event_data.line
  //Get all lines from the cart component
  , lines = cart_component.getLinesSync()
  //Calculates the items count
  , items_count = _.reduce(lines, function(count, line){
      return count + line.quantity;
    }, 0);

  if(items_count + line.quantity > 10)
  {
    //Thows an Error in order to cancel the execution of the addLine method
    throw new Error('You can not add more than ten items');
  }

});

We're getting the cart and putting a listener on it. From there, we take the data and get the line item that's being added. With that data we can count the items and their quantities and then perform a conditional check on them. If that number exceeds we throw an error, stopping the code; if it doesn't, then the rest of the code runs as expected.

If you comment out the bottom function (the one beginning cart_component.on('afterAddLine'), follow the steps in README.md to add this module to your distro, and then deploy it, you should see the functionality in action. Try adding less than 10 of an item to your cart and you'll be fine, add more than 10 and you should see an error. Great stuff.

Now, this function only handles when items are added to the cart — what about when you're on the cart page and you increase an item's quantity? Try it. Uh oh, you can still have more than 10 of an item by simply increasing its quantity. How do we deal with this?

Well, if you take a look at Cart.Component.js again, you'll see that we have a method for when we want to update a line: updateLine. We can re-use the code we've just written along with this method and write something to deal with this scenario too. For example:

cart_component.on('beforeUpdateLine', function(event_data){
  var line = event_data.line

  if(line.quantity > 10)
  {
    throw new Error('You can not add more than ten items');
  }

});

We removed a bit of code to do with items count and kept the parts about quantity; we then turned this all the way up to 10. Save and deploy and then head over to your cart page. Now when you try to increase a quantity level to 11 or more, you should see something like this:

Awesome.

Now, one final note about this: you're probably wondering where things like beforeUpdateLine come from; well, if you scroll down in Cart.Component.js, you'll see an array of objects which produces a map of events. In there you'll see the events we've used in our code, as well as, perhaps, more familiar 'versions' of these events. In other words, in the article on service controllers I offered examples of events in the form of before:[method] and after:[method]; in this array, we can see them mapped together.

Note, however, that there's also ICart.Component.js — this is the file we're using to actually create a 'component' in this new extensibility API. You'll see some of the aforementioned methods as well as a whole host of unimplemented ones.

From here, you can see all the different cart events that you can listen to and, thus, attach additional (and conditional) code to. From here we can move on to the PDP and get a sense of what the API can do for us there.

PDP Extension Example 1

Let's take a look at the first example extension example, MyExamplePDPExtension1@1.0.0. Head over to the entry point file. One of the first things you may notice is that, like the cart extension, we're doing this thing where we 'get a component' of the application. We can dig a little deeper on this now.

I mentioned in my article on SuiteCommerce Standard that one of the things we're working on is an API to aid developers create plugins for the product. I also mentioned how a number of architectural changes enabled development of new features that, while they're a boon to SCA users, are essential for SCS users. This application.getComponent method is part of the extensibility API that will aid developers build plugins for SCS, but also enable SCA developers to quickly and easily build customizations for their site.

The idea is that we have a stable API that provides big abstractions in the form of components. Each component is an abstraction of an aspect of SCA with a centralized way to handle it.

So we've worked with the cart component already, and we were able to get information about line items and attach a listener with our code. But what about the product detail page? Let's look at the code for the first PDP example.

MyExamplePDPExtension1@1.0.0 offers a number of simple extensions that might conceivably come up:

  1. Add a new child view
  2. Remove a child view
  3. Pre-render validation
  4. Pre-select item options after render

If you take a look at the entry point file, you can see how we're doing this. We start, as usual, with the mountToApp function and the first thing we do is call the component for the PDP by making use of application.getComponent('PDP') — note, importantly, that while this API is available in both JavaScript and SuiteScript, you do not capitalize application in JavaScript files.

If you perform a console log of the PDP component, you'll get a sense of what this API is capable of:

Indeed, addChildViews is one method we're going to look at now.

Add a New View

We set the parent view we want to modify by declaring it as a variable, and then use the addChildViews method to add a new child view to it in the format of:

[component].addChildViews([parent view],[child view object])

From here we declare an object, which nests further objects, depending on how deep we want to go. In our example, we're going to add a child view to the price view of the PDP view. Having added our new as a dependency, our code looks like this:

pdp.addChildViews(
  view_id
, {
    'Product.Price':
    {
      'new_price_view':
      {
        childViewIndex: 1
      , childViewConstructor: function ()
        {
          return new MyProductPriceView({pdp: pdp});
        }
      }
    }
  }
);

If you've been with us since before Elbrus you'll notice how this looks different to before as we now have two new properties: childViewIndex and childViewConstructor. The index value controls the order in which the child views are rendered; if they are being rendered into one container (eg, in our case this is going to be Product.Price) then the ones with the lowest values will be rendered first and 'on top'. The constructor is the familiar thing: it is the function or subclass of Backbone.View that defines what the child view is.

This whole method is completely different to how we normally do it. There's no explicit prototyping or extending of objects. We're using API methods to actually add child views to our parent view by supplying it with a JavaScript object. Nice and simple.

Then, if we head over to the new child view's JavaScript file, we don't see anything too new except that our initialize function defines this.pdp = options.pdp. Then, within the getContext function, we can easily pluck out the information we need:

, getContext: function ()
  {
    var item = this.pdp.getItemInfo().item;
    return {
      price: item.onlinecustomerprice_detail.onlinecustomerprice_formatted
    };
  }

.getItemInfo() and boom we have all the information from the model. We don't have to specify the model, we tell it to pull the data from the PDP; the API figures out the rest.

When we combine that with a simple template, we get the following (super-helpful) message:

Nice.

Remove a Child View

Another feature of the API is the ability to simply remove a child view. There isn't really very much to say about this, other than you can see the code:

pdp.removeChildView(view_id, 'Global.StarRating');

It follows the same format as the method to add a child view: specify the parent view you want to modify, then the child view you want removed.

Add Event Listeners

We've covered frontend Backbone events before and so this shouldn't come as too much of a surprise. Much like the backend listeners we did earlier, we're waiting for a particular event to happen and then running some code either before or after.

The first example of this in the code is the solution to the problem raised by site administrators, I'm sure, all the time: how can we stop shoppers from buying — nay, seeing — products that are worth more than $100?

What we want to do is intercept the showContent function and run a check beforehand. If the price of the item is too high (over $100) then don't show the intended view and, instead, show a new (error) view. So, in our extension file, we have the following code:

pdp.on('beforeShowContent', function(){
  //Get the pdp item in order to get the item's price
  var price = pdp.getItemInfo().item.onlinecustomerprice_detail;

  if(price.onlinecustomerprice > 100){
    //Show an error view when the price is greater than 100
    application.getLayout().showContent(
      new MyErrorView(
        {
          message: price.onlinecustomerprice_formatted+' IS TOO EXPENSIVE'
        }
      )
    );

    //Throw an exception in order to cancel the showContent execution
    throw new Error();
  }
});

Then all we need is the error view and template, which accepts the message we send it. Thus, when we visit the PDP for a product that is over $100, we get something like this:

Doing this throws an exception, which does create a little red flag in the developer console, but you get the idea. The point is that the exception stops the rest of the code running.

The final bit of code that we have in this file relates to code that we want to run after the content has been rendered. This event is the last opportunity to manipulate code with event listeners, and it is typically reserved for JavaScript that that manipulates the DOM. In our example, we're running a bit of code that goes through all of the options available for a matrix item and then selects the last available options:

//Subscribe to the afterShowContentEvent
pdp.on('afterShowContent', function(){
  _.each(pdp.getItemInfo().item.options, function(option)
  {
    //For each set of item's options selects the last available option
    option.values.length && pdp.setOption(option.cartOptionId, _.last(option.values).internalid);
  });

});

return;

We're using another interesting method returned from our API: .setOption() — this lets us automatically set options (ie without requiring the user to do it). This could have very useful applications, such as pre-selecting colors that the shopper has refined by (or, if like me, you're trying to shift products of a certain color).

Using this method is preferable from running some arbitrary JavaScript to select the DOM elements because using the API is more stable. In other words, building up a jQuery selector and then using something like .trigger() is hacky — our fancy extensibility API isn't.

PDP Extension Example 2

So the last example looked at adding new views; MyExamplePDPExtension2@1.0.0 builds on that by answering some questions that could come up when you use it in a complex situation.

As the readme details, we're going to take a look at adding multiple child views into the same placeholder and, in particular, how we can use childViewIndex to control their order.

Deploy the code (which should be doable by simplying changing MyExamplePDPExtension1 to MyExamplePDPExtension2 in distro.json) and refresh your PDP. You should see some new buttons appear. Now take a look at MyExamplePDPExtension2.js.

The first block of code adds two buttons to the PDP with the requirement that one appears before the button to add to cart, and then one after. Now, I don't know about you but the code comment about how, by default, the add to cart button has an index of 10 seems wrong — when I run the code on my site, both of the new buttons are above it. That's because there's some code just after the next code comment that increases the add to cart button's index. For now, you can tinker with the number of the second button (say, increasing it to 200), you'll see it move afterwards.

There isn't much to say here: you can see it working and then it is interacting with the rest of the code. In other words, unlike some solutions for this problem that require to use JavaScript to inject what you want into the view, this method goes a little deeper and considers the other children's positions before rendering them.

After that we have a code comment and the code I mentioned earlier:

pdp.setChildViewIndex('ProductDetails.Full.View', 'MainActionView', 'MainActionView', 30);

This line of code is defined in SC > JavaScript > SC.BaseComponent.js. The four parameters it's taking are view_id, placeholder_selector, view_name, index. You can see how it is manipulating the child view's index and that commenting it out (and restoring the default value of the 'after' button) orders things as expected.

The code after that isn't too exciting: it's recreating the PDP functionality but for the quick view modals.

What Makes a Component

As mentioned, we have SC.BaseComponent.js which creates the foundation for the components used within SCA. If you look at the functions and the comments, you'll see the sorts of things that the components offer: before/after show content listeners, the index setter, child view adder/remover, etc. We also have something for the backend in the SuiteScript folder.

I already mentioned ICart.Component.js for the cart, but you can also look at the one for the PDP. Head over to ProductDetails > JavaScript > ProductDetails.Component.js. Looking at the code, you can see how we start by extending the base component, defining it and then adding in our methods. There are also all the other event listeners we can use, for example around option selection.

With this, we could listen to when a user selects a (non-color) option and then offer to set the item's color to orange (gotta shift those orange shirts!):

pdp.on('beforeOptionSelection', function(item) {
  if (item.optionCartId !== 'custcol_gen_color')
  {
    var confirm = window.confirm('Do you want to make it orange too?');
    if (confirm == true)
    {
      pdp.setOption('custcol_gen_color', '7');
    }
  }
});

Of course, we wouldn't actually ever do this but you can see how we can listen to option selection and then do something with this.

Final Thoughts

With this methodology, you can see how we're approaching components and how relatively easy it will be to add new ones (you could add your own!). While I can't guarantee that we'll add new components, it's certainly looks that we will. After all, we're working on this extensibility API to make developing plugins easier — it would make sense to expand into other areas of the site, as well as deepen the methods and events available.