Post Featured Image

Add a Button to Remove All Items in the Cart

This blog post covers functionality that is applicable to all versions of SuiteCommerce and SuiteCommerce Advanced, but it is written as extension, which means that its form is only appropriate for sites running Aconcagua or newer. However, the code can easily be adapted for older versions of SCA.

A customer recently asked for help implementing a 'remove all items' button on their shopping cart. They run a B2B site and sometimes their shoppers put a bunch of items in their cart, change their minds, and then want an easy way to remove all of the items without having to click each individual item's Remove button. It's a good idea for them and could be good for other sites which process orders with many lines.

However, what was interesting about their approach was that their idea was to write code that required one click but would fire each items' remove function. In other words, if there were 50 items in the cart, then it would either send 50 DELETE HTTP requests, or one big request that then processed the individual removal calls on the server. Sub-optimal. Especially considering that we have a method in the commerce API for emptying the cart: removeAllItems().

Then there was a really good question: if NetSuite had implemented the method in the commerce API, why wasn't it available on the frontend? I didn't have an answer and looking into it, the team didn't really know. I think the best explanation is that it is not a feature one typically sees on web stores. In other words, we knew it was there and it was available should someone ask for it implemented; from a developer point of view, you were always free to use it if you wanted to.

So, shall we do that?

I'm going to take you through the process of how I would implement this functionality if I were asked to. It's going to be done as an extension, but if you're not running the extension framework yet, then you can take parts of it out and implement them using older customization methods. By the end of the post, you should have something like this:

Design Decisions

Before beginning, I want to explain how I approached this.

You see, SuiteCommerce already has a lot of functionality to do with the cart. We already have methods to perform almost the full complement of HTTP methods: create, read, update and delete. The only one that is missing is to delete all lines — functionality already exists to delete single lines.

Therefore, I was feel that there were two ways you could approach the handling of deletion all items at once:

  1. Write entirely separate functionality
  2. Add functionality to the existing structure

If you're mindful of best practices for building extensions, which you should be, then you might want to lean towards the first as our golden rule is: don't modify base SuiteCommerce classes. However, that always came with the qualifier, unless it makes sense to.

Honestly, I was kinda torn when thinking about this. It seemed wrong to duplicate so much functionality and it didn't really seem that I would be gaining much — would this remain any more stable in the future and across sites if I did it this way?

Crucially, for me at least, it seemed like overkill. While our team would probably not like you to touch the model that drives the cart, we are touching it once and our functionality relies on one crucial bit of its nature.

You see, when you fire a command to delete a line item in SCA, you're relying on the Backbone method sync(). This is what sends the request to the NetSuite service (controller) that handles the request. However, for something as 'simple' as the entire cart, we can just rely on model.destroy(), which just sends a DELETE HTTP call for us. The trick, therefore, is creating call and putting code in place to handle that request.

The service controller for handling the individual order lines already has code for handling a DELETE HTTP call; the one for the order as a whole does not. My suggestion, which is what I've detailed below, is to implement that method. Furthermore, seeing as we already have SuiteCommerce code designed to handle this circumstance, it makes sense to simply implement it using that existing structure. Creating an entirely separate way of handling it just seems really odd. Let's do it natively.

The downside to this approach is that should you upgrade your source code and we (NetSuite) implements this method, then you might have some issues. Specifically, this implementation creates a delete() method in the service controller, and will override one should it already exist. Thus, any future functionality that requires LiveOrder.ServiceController.delete() will use the one defined below. If you run into issues, you'll need to modify the code to fit around future implementations (or simply deactivate this extension).

Basic Setup

As I said, I'm using an extension and I will be using some extensibility API methods, but they are not required.

Create a new extension as normal, set it up and call it something like RemoveAll. You'll need this to run in the shopping application, and you'll need JavaScript, Sass, SuiteScript and templates. For the purposes of this tutorial, I'm using my name as the vendor — substitute your own if you like, but just remember to update the file, folder and class names.

After fetching your theme, arrange your files in the Modules directory like this:

RemoveAll

  • JavaScript
    • RemoveAll.View.js
    • SteveGoldberg.RemoveAll.RemoveAll.js
  • Sass
    • _removeall-removeall.scss
  • SuiteScript
    • SteveGoldberg.RemoveAll.RemoveAll.js
  • Templates
    • stevegoldberg_removeall_removeall.tpl

Take a look at your manifest.json and make sure under javascript and ssp-libraries that you have a property for your entry point files. For example, my entry for ssp-libraries > entry_point is:

"entry_point": "Modules/RemoveAll/SuiteScript/SteveGoldberg.RemoveAll.RemoveAll.js",

You should also run a quick a gulp extension:update-manfiest in the command line in the parent directory of your extension.

Create the Frontend Entry Point File

Remember, the entry point file is the frontend file that is loaded when the application loads the module. It's the starting point for the frontend of the work we're doing.

We're going to use it to create a view that appears in the shopping cart page. For that we're using the addChildViews() method on the cart component. We're also going to pass it the layout component (Aconcagua R2), which we will use later to trigger a confirmation dialog to appear in a modal.

If you're on Kilimanjaro or older, then extending the prototype of the childViews object is how you'd do this on your site. You don't need the layout component specifically, but you will need to pass it the application object instead.

In JavaScript > SteveGoldberg.RemoveAll.RemoveAll.js, put:

define('SteveGoldberg.RemoveAll.RemoveAll'
, [
    'SteveGoldberg.RemoveAll.RemoveAll.View'
  ]
, function
  (
    RemoveAllView
  )
{
  'use strict';
  return  {
    mountToApp: function mountToApp (container)
    {
      // We need two components: one to add a view, and the other to show the modal dialog
      // Pre-Aconcagua sites can extend the childViews object prototype of Cart.Detailed.View – make sure you pass the application as a parameter
      var Cart = container.getComponent('Cart')
    , Layout = container.getComponent('Layout')

      if (Cart && Layout)
      {
        Cart.addChildViews(Cart.CART_VIEW,
        {
          'Item.ListNavigable': // Adding it to this child view means it will only show if there are >0 items in the cart
          {
            'RemoveAll':
            {
              childViewIndex: 99 // Renders the child view at the bottom of the page; set it to 1 to put it at the top
            , childViewConstructor: function ()
              {
                return new RemoveAllView
                ({
                  Layout: Layout
                })
              }
            }
          }
        });
      }
    }
  };
});

The only dependency is the new view we haven't finished yet.

As for the code, it should be pretty familiar by now: we're adding a new view. We check whether the components we want to use are available, and then use Cart.addChildViews(), passing it the parent view we want to modify, the child view we want to add our new view to, the name we want to give our new view, and then any important details to constructor. Specifically, we're setting so that it'll only show if there are items in the cart. By giving it an index of 99, we're saying we want it to appear at the bottom.

Nice 👍

Create the View

We've referenced the new view, so let's create it now. This will be used to create the button that offers shoppers the choice to remove all items from their cart, as well as creating the connection between the event and sending the message through Backbone to NetSuite servers.

In JavaScript >RemoveAllView.js put:


define('SteveGoldberg.RemoveAll.RemoveAll.View'
, [
    'Backbone'

  , 'GlobalViews.Confirmation.View'
  , 'LiveOrder.Model'

  , 'stevegoldberg_removeall_removeall.tpl'
  ]
, function
  (
    Backbone

  , GlobalViewsConfirmationView
  , LiveOrderModel

  , stevegoldberg_removeall_removeall_tpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: stevegoldberg_removeall_removeall_tpl

  , events:
    {
      'click [data-action="remove-all"]': 'removeAll' // Create a listener for when the user clicks our button
    }

    // This public method will be called when the user clicks the button. We're using it to create a modal confirmation dialog
  , removeAll: function removeAll ()
    {
      var removeAllLinesConfirmationView = new GlobalViewsConfirmationView
      ({
        callBack: this._removeAll // If the user confirms, this is the function that's called – note that we just put its name, not this._removeAll() (ie with its brackets)
      , title: _('Remove All Items').translate()
      , body: _('Are you sure you want to remove all items from your cart?').translate()
      , autohide: true
      });

      // Use the layout component to create the modal dialog
      // Pre-Aconcagua sites will need to pass the application to the view constructor in the entry point file and then use this.options.application.getLayout().showInModal(removeAllLinesConfirmationView);
      return this.options.Layout.showContent(removeAllLinesConfirmationView, {showInModal:true});
    }

    // This is a private method, essentially the one that does all the work
  , _removeAll: function _removeAll ()
    {
      var model = LiveOrderModel.getInstance() // The model we use for cart contents is a singleton – one, and only one, version of it may exist throughout the whole site

      // Trigger the DELETE request and then re-render the page with whatever it sends back (it should be empty!)
      return model.destroy().done(function (attributes)
      {
        model.set(attributes);
      });
    }
  });
});

Three dependencies are standard: Backbone (as we're extending the standard view), the global confirmation view so we can create a modal dialog to get the user to confirm that they want to remove every item in the shopping cart, and a template to contain the button.

However, there is one you might not be familiar with: LiveOrder.Model. It controls the cart contents and what goes into an order when a shopper progresses to the checkout. It is in this model that commands to, for example, add, update and delete items are sent.

An interesting thing about it, that I don't think we've talked about before is that we don't need to create a new instance of the model like we might do with other custom method: instead, we we will use its getInstance() method. This model is rather special in that we only allow one to be created throughout the entire application; it is a 'singleton'.

Singletons are things that occasionally come up in programming when having multiple instances of this object could have particularly bad consequences. In our case, we don't support shoppers having more than one shopping cart and we don't want to run into trouble where they, say, add an item to the cart and it gets put into some order model which isn't their actual one. Shoppers have one — and only one — shopping cart; making it a singleton ensures that.

Next, we create the object to map the events. We pass it the listener, which contains the selector for the element that will be in the template. Attached to that event is the function that will be called, specifically removeAll, which we specify next.

As this will be triggered first, this where the code that should run first should go. In our case, we want a confirmation dialog to show before actually processing the command to delete. To do that, we create a new instance of a confirmation view, with our details specified.

The callback is the name of function that we want to run if the user agrees. This is going to be a private method we're about to create called _removeAll. As it will be attached to this view, we specify this._removeAll — note that we don't put the brackets at the end of the name (otherwise it will be called immediately, even before the user confirms). After that, we just pass some basic configuration values (eg the text to show).

Then we invoke the view by using the showContent() method attached to the layout component, being sure to pass it the option to render it in a modal. If you don't have access to the layout component (ie you're pre-Aconcagua R2), then you will have to rely on older methods of creating the dialog.

So, after the user confirms their decision, we call _removeAll(). Here, you'll see the get the instance of the order model and from that, we just trigger the destroy() method. This is built-in functionality to Backbone that I've used before in various blog posts. As previously mentioned, it just triggers a DELETE HTTP method to be sent to its already defined service URL. Now, what happens when the server receives that command? Well, if you've implemented nothing, you'll get a 405 error code back (method not allowed), so let's take a look at the SuiteScript.

Create the Backend Entry Point File

The backend entry what?

Yes, the concept of entry point files has been added to extensions. In the older architecture we had service controllers and models, where the service controllers handle the specific HTTP requests (create, read, update, delete) and models to process the data and perform any necessary transformations. Sites running even older may remember (manually writing) service files. Well, you still need service files, and we still use service controllers and models, but the backend entry point files are useful jumping-off points for SuiteScript work.

Honestly, even in older versions of code, you could run arbitrary SuiteScript files in the backend, but we typically didn't encourage it because we wanted things to be in conceptual realms of either models and services (or service controllers).

In our case, we don't need to create a new model or service controller — we just want to modify existing backend classes. We need to add a new method to the service controller to handle our newly created DELETE request, and then something in the model to process that request (and actually do the work!).

In SuiteScript > SteveGoldberg.RemoveAll.RemoveAll.js, put:

define('SteveGoldberg.RemoveAll.RemoveAll'
, [
    'LiveOrder.Model'
  , 'LiveOrder.ServiceController'
  , 'SC.Models.Init'
  ]
, function
  (
    LiveOrderModel
  , LiveOrderServiceController
  , ModelsInit
  )
{
  'use strict';

  LiveOrderModel.removeAllLines = function ()
  {
    ModelsInit.order.removeAllItems();
  }

  LiveOrderServiceController.delete = function ()
  {
    LiveOrderModel.removeAllLines();
    return LiveOrderModel.get() || {}
  }
});

Three things are listed as dependencies: the order model (to add a new method for removing all lines), the order line service controller (to process the DELETE HTTP request), and SC.Models.Init, which is a class that wraps the commerce API and provides easy-to-use access to its methods.

We start by adding a new method to LiveOrder.Model called, would you believe it, removeAllItems(). When called, it accesses the order object of SC.Models.Init (which is essentially the commerce API) and its removeAllItems() method. This is the built-in functionality I mentioned at the top of the post that will remove all items from the cart.

Then we need to do a similar thing for the LiveOrder.ServiceController class. When it receives the call from the frontend model to do a DELETE, it will run its delete() method which calls the model, runs the above commerce API code, and then returns either the contents of the cart (which should be empty) or just an empty object. Note that this is SuiteScript v1, so it all runs synchronously and we don't have to think about promises and things like done().

And that's basically it. Now it's just time to show the button.

Create the Template

We've done the most serious coding, so now we just need to work on presenting our work.

In Templates > stevegoldberg_removeall_removeall.tpl, put:

<section class="removeall-container">
    <a class="removeall-container-link" data-action="remove-all">
        {{translate 'Remove All'}}
    </a>
</section>

There isn't much to say about this. The only thing I would point is rather basic: note how we have data-action="remove-all" on the anchor tag with no href property. We're obviously not navigating the user anywhere, so there's no href, but the data-action property will be used to link the event to the value we set in the view's events object, which will begin processing the user's desire to remove all of the items.

The rest of the structure of the template is purely for my site's design, so feel free to change it to fit your site.

Create the Sass File

On the subject of design, let's take a look at the styling for this.

I had a brief chat with some of our UX team members about this and they gave me some things to think about regarding how to position and style the link. Essentially, emptying the cart is a big deal because we are irrevocably removing all items, therefore it's something we want the user doing after some consideration.

With that in mind, I think a good place to put it as the bottom of the item list as a button that adopts the tertiary button style. When we added the the view to the cart, we specified the index as 99, which will put it at the bottom, so the rest is on the Sass to make it look the way we want.

In Sass > _removeall-removeall.scss, put:

.removeall-container {
    margin-bottom: $sc-margin-lv6;
}

.removeall-container-link {
    @extend .button-tertiary;
    @extend .button-medium;
    width: 100%;
    margin-top: $sc-margin-lv3;
}

/* If you don't have these in your theme */
.global-views-confirmation-footer {
    margin-top: $sc-margin-lv3;
}

.global-views-confirmation-confirm-button {
    @extend .button-secondary;
    @extend .button-medium;
}

.global-views-confirmation-cancel-button {
    @extend .button-tertiary;
    @extend .button-medium;
}

The first two declarations are explicitly to do with this functionality; the bottom three are because my theme doesn't contain styles for the confirmation modal, so it's just some additional styling for that.

Anyway, you'll see that for the link itself, we're extending two classes we use for buttons: .button-tertiary to set the colors and font styles, and .button-medium to control its size. I've also set it to 100% width, for purely stylistic reasons so that it will fill the container width. You could, of course, just have it centered.

Note that if you're using an older version of SCA, some of the base styles used in the Sass file may have different names in your version (eg the spacing classes).

Test and Deploy

That's it from the code point of view. Now the only thing to do is to test it out.

As this contains modifications to the backend, you will have to deploy it before it will work. As it's an extension, you'll need to activate it as well.

Do gulp extension:deploy to push it up; once it's there, head over to the activation manager to get it running.

To test it out, add multiple item lines to your cart and then go to the shopping cart page. At the bottom of the list, you should see your new button. Click it and you should see a confirmation modal that triggers the removal of all items after you confirm. Remember, the link won't show unless there are items in the cart.

Also try removing just one item when you have multiple lines in your cart. The SuiteScript we coded modifies the behavior of the generic delete function, so we need to make sure that when a shopper goes to delete one line, that line — and only that line — is deleted.

If you experience issues, try rebuilding the manifest and deploying/activating again. Make sure you've added entries to the entrypoint objects.

As we talked about in another post recently, you can use your browser's developer tools to troubleshoot the functionality. Specifically, you can examine the service call in your browser. It should be a DELETE to the LiveOrder.ServiceController (not LiveOrder.Line.ServiceController) and it should return a 200 status after completion (it's fine if the response returns all the lines that used to be in the cart as long as the cart shows as empty). If it returns a GET with 405 status then it means the request was either not sent correctly, or not handled correctly. Specifically, it means that the service doesn't support the method requested which may mean that our backend file modifications haven't taken effect, or it's been sent to the wrong service controller.

Remember to check your manifest file and ensure that there are values in the entry point objects (you might have to manually add/update the paths yourself).

Final Thoughts

Despite being an extension, we have extended some classes that are found in the SuiteCommerce source code. Typically, I would not recommend this methodology but there are mitigating circumstances here. Specifically, we need to modify the behavior of the order model. We probably could have separated out the whole thing, so that it has its own model and service file, for example, but this felt wrong to me. Firstly, the feature we are adding is specific to the model in question — ie, there exists a model and service for the feature 'area' we want to modify. Secondly, this would seem like a lot of unnecessary additional files simply to keep the files separate.

If you're looking at this functionality and you can't run extensions yet, keep in mind that there is nothing in here which is overly reliant upon the extensibility API or the rest of the extension framework. Essentially, the biggest change you'll have to make is where we use the addChildViews() method to create the child view (you can just extend the childViews object of the view's prototype instead) and also where we use the layout component to show a confirmation dialog (for this, see how I recommend doing this in an older blog post).

If you'd like to see my final code, you can download it here: RemoveAll.zip.