Post Featured Image

Coding for Commerce Extensions Webinar Follow-Up: Extending the Checkout and Using Events

This blog is a follow-up to a webinar that Jackie Fisher and I gave about the commerce extension framework. It is appropriate only for SuiteCommerce, and Aconcagua or newer sites.

If you attended our Coding For Commerce webinar you should have got to grips with what we're talking about. The commerce extension framework offers new opportunities for developers to create functionality and keep their sites up-to-date and fresh going forward. However, I wanted to follow up with some additional details and examples so that you have what you need to get stuck in.

Firstly, if you haven't already read the previous blog posts I've written on the subject, I strongly recommend taking a look as they will give you a great grounding:

  1. Get to Grips with the Commerce Extensibililty Framework — an overview of the paradigm shift from our old best practices for customizations, to where we are now, including general best practices
  2. Learn How to Developer an Extension for SuiteCommerce — an instructional guide on using the new extension developer tools that take you through building a very basic custom content type, which also happens to be an extension
  3. Get Started with SuiteCommerce Themes — theming is the second big part of the framework, and we have new tools for that too; this post takes you through making your initial setup and creation of a theme

With that in mind, I want this post to focus on one particular part of the framework: the extensibility API itself. The API and the framework are conceptually different — the framework also includes the new developer tools and UI changes — but it is the API that forms the bedrock of this change in paradigm.

We consider the API mature enough to use. It has significant coverage which runs through almost areas of a web store. There are some areas not included, however, that we are working on but not ready: customer accounts and customer profiles. Thus, it's important that we set expectations and say that if you plan to produce customizations for these areas using extensions, you're going to find it difficult. Adding in support for these areas is what we call 'horizontal growth', that is: we intend to add support for more areas and parts of the application as we go.

With that in mind, another thing I want to reiterate is best practice. Typically, the advice is that, where possible, you should use the classes, methods and members of the extensibility API whenever possible. Can you use 'old methods' of customization in your extensions? Well. That is the question, isn't it? The hard answer is yes, but it comes with a big 'but'. And that 'but' is that if you do that, you add uncertainty into your extension. Joaquín Gatica provided a lot of best practice advice in our talk at SuiteWorld, which I've included in the first blog post mentioned above.

Remember, as the core SuiteCommerce product ages, a prime benefit of the API grows in power: that is, customizations built with it are more likely to live longer due to improved compatibility across sites and versions. The more you deviate from the API's defined parameters, the more likely it is that your extension will need work over time to keep it compatible with newer versions. Decisions on how to achieve your extension's design goals are yours, and we counsel that when you're interacting with core SC code you avoid the traditional customization methods whenever possible. For example, this means avoid extending core SC classes, wrapping their functions, manually inserting child views, etc.

But what about if you're creating a standalone part of the application? Does that mean you should avoid using Backbone, Underscore, and other libraries that are core parts of the SC product? No. If you're introducing a significantly large bit of functionality that is self-contained, then you should have more freedom to work as you wish. The key point is that when you are plugging into the core SC product, you do so using the API. If, for example, you were planning on creating an extension that introduces blogging functionality, you would plug into in the application using the API, but once that's done, you can work in that container.

So, with all that in mind, let's go through some example functionality I've written and some of the consituent parts of the API that I've used to make them work. The first one will add a change to the checkout — and before we can code that we need to understand what the checkout is first.

Understanding the Checkout Wizard

Let's talk about the checkout on SuiteCommerce sites.

Firstly, let me say that as a very important part of your site, it's quite complex. When customizing your site, you will typically need to spend a lot of time thinking about it and getting used to it. However, with the advent of the configuration tool a while ago, and extension framework more recently, this has become significantly easier.

If you want to make changes to your checkout, it's good to try and figure out if it can be achieved through configuration (including the web site setup tool) and theming, as these will be the easiest and safest ways to make changes.

In the example image above, you can see some of the functionality items you can tinker with without ever having to touch a line of code. For example, recently I was asked how to change the JavaScript and templates associated with the checkout's header and footer so that it shows the standard links, information, etc. Don't bother! If you want it, just toggle it in the settings.

However, if you are determined to make changes, you need to realize what the checkout is and how it's operates.

The Wizard

In software parlance, the checkout operates as a wizard. No, not the wand and robe carrying type: but a series of well-defined steps that take the shopper through the complex process of converting a list of items into an order. In SC namespacing, the majority of the modules associated with this begin with OrderWizard.

Within this wizard, we have three distinct containers to help organize the fields of the checkout:

  1. Step groups — in sum, all of the groups form the whole of the checkout, but individually they might be the shipping address form, payment form, or order review
  2. Steps — each group has at least step, but maybe more
  3. Modules — individual blocks of functionality that do specific things

Groups are really there for visual representations to the shopper. Think about the groups that might appear at the top of the checkout indicating where in the process they are, eg: shipping address, payment, review.

Each one of those could have multiple 'steps' within them, and you may choose to organize them that way; however, most of the time, you'll likely do a 1:1 step to group ratio.

Take, for example, the shipping address. Setting a shipping address is considered a step group — it's a top-level concept — however, within that may be two steps: one where the shopper can choose an existing shipping address, and another where they can enter a new one.

Within those two steps will be a number of modules, many of which will be repeated across both steps because they perform the same jobs (eg we might show a cart summary, or allow shoppers to redeem a promotion code).

I've mocked up the step group in the image above. The outer blue area envelopes everything and refers to the shipping address step group. Within that are the two individual steps highlighted in orange, showing the two possible steps the shopper could interact with. Finally, within each of those, are the individual modules that compose the steps, in white.

The Flow

One configurable setting is how the checkout flows. In terms of what we just talked about, this means changing what groups and steps are presented and in what order. By default, SuiteCommerce includes three flows:

  1. Standard
  2. Billing first
  3. One page checkout (OPC)

Despite its name, 'standard' is becoming increasingly less standard; a better name might be 'traditional'. The idea is that you minimize the amount of information that you show to a shopper at once, breaking up the process across strict contextual lines. For example, you would start with the shipping address only, the shopper clicks to move on, then they select their shipping method, then they click again, then the enter their payment details, etc. There's a certain neatness to this, and for large orders, it helps pages from getting long.

A modification of this schema put the billing address first, as a separate page. In the standard checkout, the addresses are entered on the same page; this method splits it off, emphasizing it. This effectively meant that there were pages the shopper had to complete before their order was complete (not including the login and confirmation pages).

Thus, there was a push towards fitting as much onto one page as possible, creating the one page checkout. More often than not, shoppers prefer conflating groups together as this streamlines the process. For returning customers, it makes the process feel a lot speedier as their details are often saved and don't need to be provided again.

Despite the name, it's not entirely one page. The point is that the form that the shopper can edit is on one page, and then there's a review step before the order is purchased. The flow is quite hasty, so if your customer make low-volume high-value purchases then they may find it a bit jarring (and you should consider adopting the standard flow instead).

The Checkout Component

With all of this in mind, let's take a look at the tools available to us via the extensibility API. As a reminder, we use JSDoc to generate documentation for the API and it's available via the NetSuite help center, or you can follow this link directly to jump to the checkout component.

If you take a look at the methods available, you'll see that there's a lot that reference what we've just been discussing. In particular, we have soem very useful methods for logging steps and groups, including the currently active one.

Of particular interest for getting started are:

  • getStepsInfo()
  • getStepGroupsInfo()
  • getCurrentStep()

The first two each return an array of objects, detailing, in order, what the steps or groups are. The third essentially plucks the step info for the current step and returns that specifically. For example, if I run this on the first page in my site's checkout, I get this:

Let's run through the values quickly:

  • modules — an array of objects, where each object is a module that has loaded into the current step
  • name — a descriptive name of the current step (sometimes it's not set)
  • show_step — if the step will be shown in the page or not (ie due to configuration)
  • state — either present or future, indicates whether it is currently in use or if it will be used later
  • step_group_name — the name of the group to which the step belongs (a group of steps is shown if there is at least one step of the group that is visible)
  • <url — the unique identifier for the step (we'll need this later — also note how it appears in the address bar too)

Let's take a look at an example now.

Add a New Module to Checkout Step

In assessing what to teach, I thought about common customizations. One that we document is how to act a custom transaction body field. We have documentation that shows you how to do this, and it correctly points out that the correct way of implementing this functionality in your template is to modify the theme's templates. However, the question arises: what happens when you've created a new extension that adds this functionality? You can't just override each site's templates — you need to inject a new one.

For the purposes of this tutorial, the custom transaction body field is going to be used to capture the shopper's preferred delivery date. Remember, this tutorial is to show you how to inject new functionality in the checkout, so this isn't going to be a fully fleshed out implementation — there would be a lot more work that needs to be done! It will, however, capture the date effectively and store it correctly.

Implementation Strategy

So, in the grand scheme of the checkout, a module is the smallest of the big three checkout groupings. It is for significant, independent functionality too small to be broken into its own step. Typically this is because you associate it with other functionality similar to it, and want them to live together.

One of the generic methods available across all components is the ability to add child views; however, for the checkout, there are additional methods we can use that more closely integrate it into the checkout wizard that we should take advantage of instead.

The template we'll render will look very similar to the one in the documentation. We will set the type of the input to date so that it is formatted correctly. The user can enter their date manually, or they can use a date picker than will appear when they select the field.

The view will be very simple: it'll just render the template. However, in order to make it work within the context of the order wizard (eg to handle data correctly), we will need to extend a special kind of class built for all views within the checkout process: Wizard.Module.

Finally, we'll also have an entry point file, which will be where we connect to the extensibility API and add our new module. This is perhaps the most interesting part of the tutorial.

Create and Set Up the Custom Transaction Body Field

As previously mentioned, we have documentation on transaction body fields that you can follow for an explanation of how one might implement this functionality without an extension. As it says, we don't need to do any SuiteScript work to get it surface to the model, unlike older versions of SCA, but what we do need to do is set it up and configure our site so that it's included.

In NetSuite go to Customization > Lists, Records & Fields > Transaction Body Fields > New and set it up as follows:

  • Label — Preferred Delivery Date
  • ID — _preferred_date
  • Type — Date
  • Store Value — (checked)
  • Applies To — Sale, Web Store
  • Access > Role — Customer Center, Edit

When that's configured, we need to surface it to the SuiteScript.

Go to Setup > SuiteCommerce Advanced > Configuration and select your site and domain.

In Advanced > Custom Fields, add custbody_preferred_date to the table and save.

We're now ready to begin coding.

Create a New Extension

In your extensions developer workspace, create a new extension using gulp extension:create. Configure it with the following:

  • Fantasy name — Preferred Delivery
  • Extension name — PreferredDelivery
  • Vendor name — Steve Goldberg (or your own)
  • Version number — 1.0.0
  • Description — Allows the capture of a preferred delivery slot
  • Supports — SCA, SCS
  • Application — Checkout
  • File types — Templates, Sass, JavaScript
  • Module name — PreferredDelivery

Next, we need to clear out the directories: we're not going to use the dummy files that were generated for us.

  • Delete the contents of Preferred Delivery > Modules > Preferred Delivery > JavaScript, and Sass and Templates
  • Delete the Assets directory

We should now have clean directories from which to work.

Create the Entry Point File

We're going to need two JS files for this extension: one to act as an entry point file, and the other to be the view that's going to render the input field.

In JavaScript, create SteveGoldberg.PreferredDelivery.PreferredDelivery.js and in it put:

define('SteveGoldberg.PreferredDelivery.PreferredDelivery'
, [
    'SteveGoldberg.PreferredDelivery.PreferredDelivery.View'
  ]
,   function
  (
    PreferredDeliveryContainerView
  )
{
  'use strict';

  return  {
    mountToApp: function mountToApp (container)
    {
      var checkout = container.getComponent('Checkout');

      checkout.addModuleToStep(
      {
        step_url: 'opc'
      , module: {
          id: 'PreferredDeliveryView'
        , index: 6
        , classname: 'SteveGoldberg.PreferredDelivery.PreferredDelivery.View'
        }
      });

      checkout.addModuleToStep(
      {
        step_url: 'review'
      , module: {
          id: 'PreferredDeliveryView'
        , index: 99
        , classname: 'SteveGoldberg.PreferredDelivery.PreferredDelivery.View'
        }
      });
    }
  };
});

So, this file starts out like it normally would: we define what the file is going to be called, and list its dependencies. Note that we've required a view that doesn't exist yet — no worries, we'll sort that out later.

We return a mountToApp function so that whatever's enclosed will run as soon as the module loads. Then we build a variable by calling the checkout component from the application (container). Once we have that, we can invoke the addModuleToStep method. Remember, what we want to do is make a small bit of functionality into an existing step, and this is how we do it.

Look at the object we're passing it:

  • step_url — this is the url of the place we want to add it to. Remember, URLs are treated like IDs in the checkout wizard, so you'll need to find the right one. If you're running a one-page checkout, then this'll likely be opc.
  • module — an object containing the data you want to pass to it:
    • id — the unique ID you want to give it.
    • index — the location in the module stack you want to insert it. This will affect rendering position, and will take precedent over other modules that have the same index.
    • classname — the name of thing you want to render (ie the name you've given the view in its define statement).
    • options — an optional value that will be passed along with it. As the docs say, you can pass an ID of a container in which to render it, but, by default, it'll be rendered in the main container (#wizard-step-content).

You'll see that we're adding the module twice: that is because in the one-page checkout screen, we're going to make editable so that the shopper can add a value; on the review step (step_url: 'review'), we're going to show their submitted value as read-only.

The big differences are that we've set a different step to render it on, and then set the index value to different values (6 will cause it to render in the middle of the page; 99 will force it to render last).

Create the View

OK, so we've told it to render the view but we've not made it yet.

In JavaScript, create SteveGoldberg.PreferredDelivery.PreferreDelivery.View.js and in it put:

define('SteveGoldberg.PreferredDelivery.PreferredDelivery.View'
, [
    'Wizard.Module'

  , 'stevegoldberg_preferreddelivery_preferreddelivery.tpl'
  ]
, function (
    WizardModule

  , stevegoldberg_preferreddelivery_preferreddelivery_tpl
  )
{
  'use strict';

  return WizardModule.extend({

    template: stevegoldberg_preferreddelivery_preferreddelivery_tpl

  , getContext: function getContext()
    {
      return {
        isReview: this.step.step_url == 'review'
      };
    }
  });
});

The crucial thing here is that we are extending a different base view than normal: Wizard.Module. When adding a new value to the checkout wizard, we have to use this view because it has been specially set up to work within the checkout.

Other than that, the file is pretty unremarkable: it renders a template and we're passing one value via the context object. In our case, you'll see we're passing a boolean that analyzes whether the current step is the review step or not. You'll see that we're referring to this.step, which gives us step information, which is a factor of the wizard module view.

Anyway, with that file set up, and the information passed to the template, we can create the template itself.

Create the Template

We're going to re-use the same template for both the editable and read-only parts of its journey. Therefore, we need to do add in a couple of conditionals to make sure we render the correct content at the right time.

In Templates, create stevegoldberg_preferreddelivery_preferreddelivery.tpl and in it put:

<h2 class="preferreddelivery-title">{{translate 'Preferred Delivery Date'}}</h2>
<div id="preferreddelivery-container" class="preferreddelivery-container">
    {{#if isReview}}
        {{#if model.options.custbody_preferred_date}}
            <p>{{model.options.custbody_preferred_date}}</p>
        {{else}}
            <p>{{translate 'No date selected'}}</p>
        {{/if}}
    {{else}}
        <input class="preferreddelivery-input" type="date" name="custbody_preferred_date" data-todayhighlight="true" value="{{model.options.custbody_preferred_date}}">
    {{/if}}
</div>

Again, there should not be anything here too surprising. We check to see if we're in the review step; if we are then we check to see if the customer set a value, if they have then we show that, else we just tell them that no value has been set. You'll note how we access the custom field: {{model.options.custbody_preferred_date}} — this is how the docs teaches us to do it and it is the best way.

Then we set up the input field for all other cases (ie when we expect the user to set a value). Note that we've set the type to date, and that we've set the name to match the ID of the field in NetSuite.

You may also be wondering why, at no point, have we added dependencies for a calendar plugin, and the reason is that it is automatically picked up and displayed on input fields that have the date format. We use the Bootstrap date picker plugin for this, and it 'just works'.

Add the Sass

In Sass, create _preferreddelivery.scss and add in the following:

.preferreddelivery-title {
    @extend .order-wizard-title;
}

.preferreddelivery-container {
    @extend .box-column;
}

.preferreddelivery-input {
    @extend .input-large;
}

These are just basic improvements to the template, they extend existing classes and don't make it too fancy.

Test and Deploy

And that's it. Now we can run gulp extension:local and test out our extension.

After spinning up the local server, you can visit your local version of your site, add something to your cart and proceed to checkout. After logging in, make sure you change the URL to the local SSP version. When you're in the checkout, you should see your new field about halfway down. When you click on it, it should spawn something like this:

Select a date and move on through the checkout. Place your order and then look it up in the backend, and you should see the date has been set in the Custom tab:

Neat! 👍

From here, you can deploy it up to NetSuite and activate it on your site. If your code isn't working, I've added a zip of commented code at the bottom of the page.

(Cancelable) Events

Modifying your site goes beyond simply adding new functionality modularly — you may wish to add new functionality based on a user action, or an event.

We've talked about events before and how they can be useful — nay, vital — in web store development in general. The same is true for extensions too, and we've added a number of events to the extensibility API that you can use so that you don't have to write your own. You can look up the details in the API documentation, or you can view a summary of them in our docs.

Events typically come in two flavors: beforeAction and afterAction: eg beforeAddLine on the cart component will trigger when a shopper tries to add a line item to the cart (but before it actually is), and afterQuantityChange on the PDP, which triggers when the user has adjusted the quantity (and it has gone through).

As well as just calling your particular bit of functionality at these times, you can use these opportunities to simply cancel the event from happening (as long as you use before events). We'll look at canceling events below in some of the examples. On the subject of the examples: these aren't going to be full modules, but rather just some snippets to get you started.

If you want to test these out, you can copy and paste the code into a fresh, simple extension entry point file — just make sure you set up variables for all the components first, like this:

mountToApp: function mountToApp (container)
{
  var pdp = container.getComponent('PDP');
  if (pdp)
  {
    console.log('PDP component available:');
    console.log(pdp);
  }

  var plp = container.getComponent('PLP');
  if (plp)
  {
    console.log('PLP component available:');
    console.log(plp);
  }

  var cart = container.getComponent('Cart');
  if (cart)
  {
    console.log('Cart component available:');
    console.log(cart);
  }

  var checkout = container.getComponent('Checkout');
  if (checkout)
  {
    console.log('Checkout component available:');
    console.log(checkout);
  }

  var environment = container.getComponent('Environment');
  if (environment)
  {
    console.log('Environment component available:');
    console.log(environment);
  }

  // 'layout' is already used as a global variable, so we use a capital L
  var Layout = container.getComponent('Layout');
  if (Layout)
  {
    console.log('Layout component available:');
    console.log(Layout);
  }
}

Note that all the examples below take place within the mountToApp function for convenience; if you're implementing this on a production site, you should obviously use best practices.

Again, all of the examples I'm about to give are in a commented JS file, zipped up and ready to be downloaded at the bottom of the page.

PDP: Prevent the User from Selecting Options by Throwing a Console Error

Throwing an error is the simplest, but probably the ugliest, way of blocking something happening. However, it, at least, serves as a useful way of illustrating how to use an event in the extensibility API.

pdp.on('beforeOptionSelection', function rejectSelectOption ()
{
  throw new Error('No option changing!');
});

So, very simply, we are adding an event listener for when a user selects an option on a product page (eg size or color) and then interrupts them by throwing a console error. This blocks the user selection from taking place. (Although, it's not fullproof as they could still change the options via URL, but you get the idea.)

Both kinds of event canceling are documented here.

PDP: Prevent the User from Changing Quantity by Returning a Rejected Promise

Another, more graceful way, of preventing a user action is to return a rejected promise. I strongly recommend reading up on deferred objects if you're not already familiar with the subject.

pdp.on('beforeQuantityChange', function rejectQuantityChange ()
{
  var quantity = pdp.getItemInfo().quantity;
  return jQuery.Deferred().reject(new Error('Hey! Isn\'t ' + quantity + 'enough?'))
});

Note that this error message isn't actually being used anywhere on this page, but it could be. What will happen if you implement this functionality as-is, is that it will effectively fail silently: there will be no explanation to the shopper as to why they are unable to increase the quantity beyond current levels. But, yeah, you're unlikely to implement this requirement this way: but the use of the event is the important thing here.

Cart: Create a Confirmation Modal When a User Adds a Promotion Code

OK, this one is a little more meatier. When a shopper enters a coupon code and hits submit, it is immediately sent to NetSuite for processing. But what if we wanted to interrupt that process and create a confirmation dialog so that when they submit a code, they're first asked whether they're sure they want to do it? If they click yes, the submission is released and flies off to the server; if they click cancel, the submission is canceled and an error message is shown instead.

Note, for this, we need to add two dependencies: jQuery (as jQuery) and GlobalViews.Confirmation.View (as ConfirmationView).

cart.on('beforeAddPromotion', function confirmPromo ()
{
  var deferred = jQuery.Deferred();

  function resolvePromise ()
  {
    return deferred.resolve()
  };

  function rejectPromise ()
  {
    var key = ['errorMessage'];

    deferred.responseText = '{\"errorMessage\": \"The coupon was not added because you canceled the request\"}'

    return deferred.reject(deferred, key)
  };

  var confirmation = new ConfirmationView(
  {
    title: 'Add Promotion?'
  , body: 'Are you sure you want to add this promo code?'
  , autohide: true
  , callBack: resolvePromise
  , cancelCallBack: rejectPromise
  });

  cart.application.getLayout().showInModal(confirmation);

  return deferred
});

So, this time we're creating and using deferred objects for real now as the confirmation modal supports callbacks but is itself asynchronous (ie, it does not interrupt a function but can be used to trigger new ones depending on what is clicked).

We start by setting up the conditions for what should happen when it is resolved (ie the user agrees) and when it is rejected (the user disagrees). In the first case, there is nothing to do, per se, as we're basically just letting the interrupted function continue. As for the case of rejected promises, we could just send a rejection (like we did with the resolution condition) but while we're here, we may as well take advantage of the fact that the promo code form accepts error messages as to why a code was rejected. This means that we can take advantage of a neat little UI feature; for that we need to create a pseudo-backend error.

Backend error messages are handled by ErrorManagement.ResponseErrorParser. The main function accepts two arguments: a jQuery XHR and an array of keys. A crucial factor of the jqXHR is that it must contain a value called responseText, which is a stringified object that contains at least an error message. The function parses the string and then goes through that object to find the first error message matching the key we provide it. Once it has that, it sends it off to the view to render.

So, if you look back at our code, you can see that what we are doing is first setting our (single-value) array up and then creating the response text. The response text is added to the deferred object, and then returned when we reject its resolution.

With all of that set up and ready to go, we can then move onto creating the confirmation dialog. We've discussed how to add modals and confirmations to sites before, and the advice is still correct. We have global views always available that standardize this functionality, making it super easy to create new ones. Once you've added GlobalViews.Confirmation.View as a dependency, you can invoke it using a view constructor.

After passing it some standard parameters, we add on two important ones: callBack and cancelCallBack — the first is what is called if the user agrees with the dialog's question; the second is what if they disagree.

Once we've constructed that view, we send it to the showInModal method of the layout to render it.

Finally, after that, the deferred object is returned as the final part of the event listener. What this means is that when this event is triggered, a pending deferred object is sent to the application, which blocks the process: it turns the asynchronous process of resolving a promo code into a synchronous one, and one that is contingent on making a decision in the dialog box. (One crucial thing to note is that clicking outside of the dialog, or clicking the close button does not return a rejection — rather annoyingly, this means that the form will become locked in a pending state.)

And that's it. If you run the code, you should see something like this:

🙌

Using Asynchronous Extensibility Methods

So, we've talked a lot about deferred objects and one of the questions you may have: but what about some of the extensibility methods that return promises? How do we interact with them? In other words, instead of triggering an event, how do I deal with an asynchronous method?

Frontend

Well, as we discussed in the deferred objects blog post, you can use jQuery methods such as when() and then(). For example, to log everything in the cart to the console, we can do:

cart.getLines().then(function (lines)
{
  console.log(lines);
});

Which, for me, returns this, for example:

What about calling more than one asynchronous method but wanting to wait for both of them to finish before processing them?

jQuery.when(cart.getLines(), cart.getPromotions())
.done(function (lines, promotions)
{
  console.log(lines);
  console.log(promotions);
});

Backend

Don't forget that we also have a backend component for the cart too. In SuiteScript 1.0 there is no concept of asynchronous behavior. However, we have written a backend component for the cart that uses the same methodology as the frontend one. This can be useful if you want to reuse code that you've written for one in the other, but be aware that this doesn't suddenly make those methods asynchronous: they will still be processed synchronously.

Also note that you are still able to use the synchronous methods if you so choose. So, for example, the following code returns identical results:

var cart = Application.getComponent('Cart');

// Asynchronous
cart.getLines()
.then(function (result)
{
  nlapiLogExecution('DEBUG', 'Asynchronous cart lines', JSON.stringify(result));
});

// Synchronous
var lines = cart.getLinesSync()
nlapiLogExecution('DEBUG', 'Synchronous cart lines', JSON.stringify(lines));

You can dump this into the get method of LiveOrder.ServiceController.js if you want to quickly test it out (just remember to add Application as a dependency to the file).

Add, Replace and Delete Child Views

A common method that runs throughout most components is the ability to inject a new view somewhere on the page. This is available via addChildView. (As mentioned above — if you're going to add a module or view to the checkout, you should use the wizard view.)

I was going to write this up but it wouldn't be that much different from what we already have in our documentation; thus I recommend reading it for information on how to do it. It also includes information on how to replace child views.

Delete Child Views

When it comes to deleting child views, you can just use the removeChildView method. However, note that if you're using this method in an event handler (ie after the page has rendered), then you will need to trigger the parent view to re-render.

For example, if we wanted to hide the quantity input on the PDP after a customer has selected a large size:

pdp.on('afterOptionSelection', function removeQuantity (item)
{
  if (item.cartOptionId == 'custcol_gen_size' && item.value.internalid == '3')
  {
    pdp.removeChildView('Quantity');
    pdp.application.getLayout().getCurrentView().render();
  }
});

Layout Component

Now, just before we end, a quick note about something that's just come out. New in Aconcagua R2 is a versatile generalist: the layout component.

It is available throughout the entirety of the web store, which means that areas previously untouched by a component — such as the login page, or the customer account area — can now have simple additions made to them.

Say, for example, that I want to add a simple view to my login page, I could:

  1. Create the view and template
  2. Add the view as a dependency to entry point file, or some other file within the module
  3. Add three lines of code

So, for example, I could add the following:

Layout.addChildView('Login', function () {
  return new SimpleView();
});

Which could end up looking like this:

Apart from generic, globally available methods, it has one specific method that might be interesting: showContent(). One of the things it offers is access to the showInModal(), if, for instance, you need to access it without using another component.

Here's some sample code you can run in your developer console to try it out (assuming you have Layout set as a global variable):

var ConfirmationView = require('GlobalViews.Confirmation.View');
var confirmation = new ConfirmationView(
{
  title: 'Are you a cool person?'
, body: 'Like, for real?'
, autohide: true
});
Layout.showContent(confirmation, {showInModal: true});

You can see that we're passing a new instance of the confirmation view to the method, along with an object that simply says {showInModal: true}. This triggers the showInModal() method, and does the hard work for us!

💪

Webinar Questions

EDIT, June, 29 2018: We were asked a number of technical questions during the webinar and I want to include the answers to some of them.

Can You Write an Article About Extensions and Overrides?

I'm not entirely sure what aspect of overrides this question refers to but two come to mind:

  1. Using an extension to override a template and/or Sass file in a theme
  2. Using a theme to override a template and/or Sass file in an extension

First, it's not possible to override theme files with an extension. Even if you could, I wouldn't recommend it. Those files are considered unique to every site, so unless you're making a change to a very minor file, it wouldn't be advisable. Remember, for Sass changes remember that you're dealing with cascading stylesheets (CSS!) and override styles using that. For templates: make changes in the site's theme if necessary, but I'm having a hard time imagining a scenario where an extension would need to completely override an existing template when we have a number of tools that let you customize them.

As for using a theme for overriding a template or Sass file in an extension, this is far more legitimate and we have documentation on how to do this. If you're unfamiliar on why you might do this, let me explain:

Let's say you get an extension from a third-party. It looks and works great; you're happy. However, you want to change the template that they provided so that it fits in better with the rest of your theme. You can't edit the files that came with the extension, so what do you do? Override it in your theme!

When you run gulp theme:fetch, the template and Sass files of any active extensions are downloaded into Workspace > Extras — this is important for when you're working locally on your theme as you'll need them in your compiled templates and CSS files. Now, you shouldn't modify these files directly because those changes won't be saved.

Instead, you first need to go looking through the Overrides directory in your theme directory: you should see directory structures that mirrors your extensions'. Any file you copy from an extension's structure and paste into this structure will take precedence over the source, overriding it. In the copy of the file, make your changes. When you want to test it, restart your local theme server and you should see it override the source.

How Do I Know Which Classes I Can Extend?

There's two sides to this:

  1. If I don't have the access to the source code (ie, I'm working entirely on SuiteCommerce and not SuiteCommerce Advanced), how do I know what to extend?
  2. If working with documented components and methods is encouraged, isn't it a bad idea to go through the source code and find things to use (eg Address.Edit.View)?

In the first case, the answer is that working on an extension without the full source will be tough. It is true that we do not document every available class in the source code. In this case, there is not much else to say other than you will be at a disadvantage and if you are keen to make complex customizations, you will need to look at the SCA source code.

In the second, this is referring to strategy or best practices: should I extend a base class like Address.Edit.View? Well, we can't answer that for you — it is a trade-off. If your extension relies entirely on components and their methods, you have a high certainty that it will be transportable across sites and stable across releases; if you use base classes and methods from the source code, you introduce uncertainty.

As we add to the extensibility API, you may find that a base class and method you used has been supplanted with a cleaner alternative, and in that case you can migrate your code to use that instead. For example, we used the confirmation view class as well as a method attached to the application object to show it in a modal — we may well create objects and methods for these in future (indeed, the layout component already offers a way of showing something in a modal). Ultimately, you will need to assess this yourself.

Will There Be a Component for the Customer Account Area?

It's in the roadmap, but as usual we cannot guarantee it or say when it'll be ready.

Where Can We See a Full List of All Available Events?

Each component of the extensibility API has its own list of events specific to it. The JSDoc documentation for each components lists these.

Can a Rejected Promise Pass No Message At All?

Sure, deferred.reject() is just fine.

Is It a Good Idea to Use Extensions for Minor Customizations?

Sure! It makes your customization transportable and, depending on how you code it, makes your site easier to upgrade.

What Types of Customizations Aren't Supported by Extensions?

This is a deep question that's difficult to answer. Remember, the key technology underpinning all of this is the extensibility API — it is what makes extensions powerful. For example, we don't currently offer a component for the customer account area, so modifying the order history page will be quite difficult; but we do have one for the checkout, so you can make some customizations there.

The best thing to do is to read the API documentation and see what methods and events we have.

Can You Use Extensions to Add Completely New Functionality?

Sure. Someone asked about adding blog functionality, which would require an entirely separate part of the site. Go for it! I can't see a reason why something like that couldn't be an extension.

Can I Use the SCA, Theme and Extension Developer Tools At The Same Time?

Yup! That's why you might see 'fallback' errors in your developer console when you run a local server running only one of them: basically, when you start a local server with each of these tools, the final site that you visit is checking to see whether it should get its source code, extension code, and theme code from the NetSuite servers or if you're providing it locally.

NOTE — if you are going to work on both at the same time, we strongly recommend making sure that the latest versions of each are fetched before you start development as it can cause problems. We also advise against using identical variable names in your Sass and your JS.

Do I Need to Edit the Theme to Add a Child View to the PDP?

You could, but if you're working on an extension then no. The PDP component has methods that lets you easily insert a new view into the page, as well as dictate its index value (ie where it appears on the page). The template and Sass file can be included in the extension, and they can be overridden in the theme (see above).

Final Thoughts

The extensibility API has a lot of facets to it that we hope we provide new levels ease when it comes to customizing your web store. This post looked at a few scenarios, first of which was an example of implementing a pretty basic checkout customization. We saw, however, that when it came to adding it with an extension, there was a new level of nuance that we needed to learn first. It required us to understand that the checkout works as a wizard with numerous constituent parts.

We also looked at triggering functionality modifications based on events. In some cases we canceled the events from firing and in others we looked at a way of interrupting them to add a prompt. While, for testing purposes, we used global variables for the components, you probably shouldn't keep this in your code.

Finally, we looked at using the API in the backend. We noted that in this context, the cart component supports both synchronous and asynchronous method types — which is useful for porting frontend code to the backend — even all execution in the backend is processed synchronously.

Don't forget to check out all of the documentation that we have on the subject, eg:

To download my example code, see PreferredDelivery.zip and Vendor.Extension.Module.js.zip

No comments ()