Here first we have to 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
- Example.RemoveAll.RemoveAll.js
- Sass
- _removeall-removeall.scss
- SuiteScript
- Example.RemoveAll.RemoveAll.js
- Templates
- example_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/Example.RemoveAll.RemoveAll.js",
You should also run a quick a gulp extension:update-manifest in the command line in the parent directory of your extension.
Create the Frontend Entry Point File
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 or newer), 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 > Example.RemoveAll.RemoveAll.js, put:
define('Example.RemoveAll.RemoveAll'
, [
'Example.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
})
}
}
}
});
}
}
};
})
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.
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 > RemoveAll.View.js put:
define('Example.RemoveAll.RemoveAll.View'
, [
'Backbone'
, 'GlobalViews.Confirmation.View'
, 'LiveOrder.Model'
, 'example_removeall_removeall.tpl'
]
, function
(
Backbone
, GlobalViewsConfirmationView
, LiveOrderModel
, example_removeall_removeall_tpl
)
{
'use strict';
return Backbone.View.extend({
template: example_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 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 > Example.RemoveAll.RemoveAll.js, put:
define('Example.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 > example_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).
