This approach is not suitable for SuiteCommerce, or SuiteCommerce Advanced sites running 2021.1 or newer. To customize those versions, use the Search component in the extensibility API instead.

The base SuiteCommerce product is set up so that whenever a shopper visits a product detail page, they're returned the item info for that product; if they perform a keyword search or hit a commerce category, they're returned a collection of items.

When you're building your site — that is, customizing our base product — you don't need to go through the work of fetching the data, building the views, or creating templates: we provide all of that for you. The process of customizing these areas is therefore pretty straightforward.

However, you're probably wondering how you can make use of the underlying technology for your next project. what if you want to request product data in an arbitrary part of a new module? Well, you probably know that you have to use SuiteCommerce's item search API, and you may already be familiar with the URL structure for it.

The item search API is a RESTful API that can be queried and will return item data. It powers common features like keyword searches, product list pages, and product detail pages. This article describes the best way to use so that you can add it to your own customizations.

We’ll look at some sample code for using the API, which involves using ready made models and collections that mean we don’t need to construct the URLs ourselves. In the examples, we'll look at two of the four classes available for you to use: one is ideal for getting and handling the data of a particular item, the other for handling multiple product results (ie a collection).

Prior to the Elbrus (2017.1) version of SuiteCommerce Advanced, there was not a proper distinction between item and product models. The principles discussed here can be applied to older versions, but with different syntax and different architecture in mind.

For the purposes of this tutorial, we are will use extension developer tools, which are for Aconcagua (2018.1) and newer versions, but the code can be used in the core developer tools too.

For all the code mentioned in this article, visit 2018-1-aconcagua/ProdData and 2018-1-aconcagua/Product.Model.Examples in the GitHub repo.

Item and Product Models

There are four classes in total that you could use:

  1. Item.Model
  2. Item.Collection
  3. Product.Model
  4. Product.Collection

The quickest way of answering the question of, "how do I make an item search API call in my code?" is: use Product.Model. Before we do that, that's quicky look at them.

The differences between the models and the collections is one you should already be aware of: models are used for individual results returned from an API call (eg for 'details' views), whereas collections are for more than one result (eg for 'list' views).

The differences between the item and product versions are more for how you plan to use the data. The item ones are a lot simpler, but the product ones are more for product details / list pages and manage the 'state' of the items. For example, on the product details page we want to track whether a shopper has selected a particular option so we can update the displayed product information, price, stock, etc, and so for that we want a more dynamic model.

However, as I said, if you just want to tinker around with things then you can make simple calls with the product model. It's written in such a way that it's very easy to supply it with a simple query, such as a keyword or commerce category ID, and get results. After sending it to the search API — which is a feature built into it — you can get a blob of results, which you can use how you wish. Let's look at some simple examples.

Product.Model Basics

The simplest way to start is simply to pass no query parameters and simply return all items. Of course, it'll return the data of only the first 50, which is the default limit, but it's a good place to start so we can get the structure down.

For the purposes of this tutorial, I'm going to use an extension but the code can be used virtually anywhere in your site's code. Assuming you're using an extension too: spin up a new one and in the namespace pattern of <VendorName>.<ExtensionName>.<ModuleName>, I'm going with Example.ProdData.ProdData for my file to test this out in.

Then, within Example.ProdData.ProdData.js, I'm putting:

define('Example.ProdData.ProdData'
, [
    'Product.Model'
  ]
, function
  (
    ProductModel
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      var product = new ProductModel()
    , items = product.get('item').fetch().then(
      function(data, result, jqXhr)
      {
        console.log(data);
        console.log(result);
        console.log(jqXhr);
      });
    }
  }
});

It's currently set up so that the code will run as soon as the code is loaded, which means that on every page, a search API call will be made, which then logs the first 50 items to the developer console, along with the result (ie whether it succeeded or failed), and the final jqXHR object, which may be useful for debugging.

If you spin up your local server (gulp extension:local) and visit any page, you will see the logs appear after the request completes. Depending on your site, this could take some time (on my dev site it takes around 18 seconds) but should print something like this:

A screenshot of the console window in a browser's developer tools. It shows the product data object logged to the console, along with a successful status message, and the full response object.

If you look at the query string parameters in the request headers for this call in your Network tab, you'll see that a number of essential query parameters are already included in the call, such as the company ID, country, field set, etc. This is one of the benefits of using the product model, which sets these things for us.

Making Specific Requests

If you take a look at the documentation for input parameters, you'll see all the things you can search for. You can search for data on a specific item, perform a keyword search, or request every item in a specific commerce category. How do we do this? By passing a data object to the fetch() method.

For example, if I want to perform a keyword search for "tent", then I could change the code to this:

mountToApp: function mountToApp (container)
{
  var product = new ProductModel()
, query = {q: 'tent'}
, item = product.get('item').fetch({data: query}).then(
  function(data, result, jqXhr)
  {
    console.log(data);
  });
}

This, on my site, returns 25 results for all the different tents I sell on my site.

Here are some other requests you could make:

// Get the item whose internal ID is '8050'
query = {id: '8050'}

// Get all items in the commerce category whose URL is 'orange-things'
query = {commercecategoryurl: 'orange-things'}

// Get the item whose URL component is 'Terralake-Shirt'
query = {url: 'Terralake-Shirt'}

Don't forget that this object can be complex, so feel free to pass it multiple parameters if that's required. It can also be super handy for debugging results as it's super easy to get fresh results. For example, you can change the query to:

query = {
  q: 'tent'
, ssdebug: true
, timestamp: Date.now()
}

The ssdebug parameter is useful for checking the performance of the item API, while timestamping it ensures that the results returned have not already been cached (and are, therefore, freshly generated).

Working with this Data

Once we have the data, we can then think about how we want to use the data. Remember, the functionality that we're using here is stuff that we've used throughout the core SCA code and also within the tutorials that we've already completed. You could extend the product model and use it as the model within your module or extension.

At the moment, our code is currently in a simple entry point file that fires globally and returns results to the console. However, if we want to progress, we need to think about using existing Backbone technology and SuiteCommerce architecture to handle and display our results. Let's move onto that.

Use Item.Collection to Fetch and Display Item Data

So, in this example, we're going to make a very rudimentary keyword search feature.

We're going to replace the existing code we wrote, so you can keep your old file but just make sure it's not in the existing workspace as we're going to overwrite it (I typically append version numbers to the end, changing the file extension) — or you can download my copies at the end of this blog post.

Update the Entrypoint File

We're gonna start by replacing the entrypoint file with some code that calls a router. In Example.ProdData.ProdData.js, put:

define('Example.ProdData.ProdData'
, [
    'Example.ProdData.ProdData.Router'
  ]
, function
  (
    ProdDataRouter
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      return new ProdDataRouter(container)
    }
  }
});

So, all that's going to happen is that when the module is loaded into the application, it creates a new router. Let's do that.

Create the Router

Create Example.ProdData.ProdData.Router.js and in it put:

define('Example.ProdData.ProdData.Router'
, [
    'Backbone'

  , 'Item.Collection'

  , 'Example.ProdData.ProdData.List.View'
  ]
, function
  (
    Backbone

  , ItemCollection

  , ProdDataListView
  )
{
  'use strict';

  return Backbone.Router.extend(
  {
    routes:
    {
      'proddata/:keyword': 'prodData'
    }

  , initialize: function initialize (application)
    {
      this.application = application
    }

  , prodData: function prodData (keyword)
    {
      var collection = new ItemCollection()

    , view = new ProdDataListView(
      {
        application: this.application
      , collection: collection
      })

    , query = {
        q: keyword
      , fieldset: 'details'
      };

      collection.fetch({data: query})
      .done(function ()
      {
        view.showContent();
      });
    }
  })
});

Starting with dependencies, we include Backbone as normal, as well as a list view that we haven't created yet. We also add Item.Collection, which is one of our base classes for calling and handling collections of items. It will make the call for us, do mapping etc, and then output a neat little collection of models for us to use without having to worry about it. 🍦🍦🍦

Next we return a standard Backbone router, specifying our route. Essentially we create the namespace and then a keyword parameter, which we're going to use to capture a search term to use. As with before, you can write this however you want: perhaps you want a commerce category or whatever — as long as it can be interpreted by the search API, it doesn't matter.

Then there's the standard initialization function and then the main function. We've mapped the route to this function and so this is what's called when the URL is visited. From then on, the code looks very similar the code we used for the item model: we prepare a new collection, a new list view, our search query and our field (Item.Collection does not have a default field) — note we're using the details field because it includes a lot of fields, but for production purposes you should create and use a slimmed down to improve performance!

Finally, we make the call to fetch the data and then invoke the view to show its content when it finishes. As we've specified that the view should use our new collection, it'll be passed the data.

Create the List View

List views are there to connect the collection data to the details views that will render each of its models. Basically, they provide the container that holds each of them.

Create Example.ProdData.ProdData.List.View.js and in it put:

define('Example.ProdData.ProdData.List.View'
, [
    'Backbone'
  , 'Backbone.CollectionView'

  , 'Example.ProdData.ProdData.Details.View'

  , 'example_proddata_proddata_list.tpl'
  ]
, function
  (
    Backbone
  , CollectionView

  , ProdDataDetailsView

  , example_proddata_proddata_list_tpl
  )
{
  'use strict';

  return Backbone.View.extend(
  {
    template: example_proddata_proddata_list_tpl

  , initialize: function initialize (options)
    {
      this.application = options.application
    , this.collection = options.collection
    }

  , childViews:
    {
      'ProdData.Collection': function ()
      {
        return new CollectionView(
        {
          'childView': ProdDataDetailsView
        , 'collection': this.collection
        , 'viewsPerRow': 1
        })
      }
    }
  })
});

Our dependencies are the core Backbone collection view, which is our default way of rendering a collection of models. When we construct a new instance of it, we're going to pass it our yet uncreated details view. Finally, we list an (also uncreated) list view template, which will act solely as a container into which we will render each individual details template. This is all reflected in the childViews property.

Create the Details View

The next stop in our whirlwind tour of this module is the details view. This will be used to render each individual model.

Create Example.ProdData.ProdData.Details.View.js and in it put:

define('Example.ProdData.ProdData.Details.View'
, [
    'Backbone'

  , 'example_proddata_proddata_details.tpl'
  ]
, function
  (
    Backbone

  , example_proddata_proddata_details_tpl
  )
{
  'use strict';

  return Backbone.View.extend(
  {
    template: example_proddata_proddata_details_tpl

  , getContext: function ()
    {
      return {
        'displayname': this.model.get('displayname')
      }
    }
  })
});

Simple stuff: we're going to render the specified template and, for the sake of a simple example, we're going to pluck each item's display name from the model and pass it in the context object to the template. You can, of course, choose whatever details you like (as long as the specified field set returns them).

Create the List Template

This is a simple template to act as the container for each of the details template. For the sake of the tutorial, we're keeping it super simple.

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

<div class="container">
    <h2>Product Data</h2>
    <div data-view="ProdData.Collection"></div>
</div>

The data-view attribute's value matches the name we gave in the childViews property of the view — this is how it will know to render the collection in the right place.

Create the Details Template

Next, create example_proddata_proddata_details.tpl, and in it put:

<p>{{displayname}}</p>

Nice and simple: it'll render the display name, which we've passed to it through the view.

Test

Spin up your extensions server (or restart it if it's already running) and then visit #proddata/<search term> on your local site, where <search term> is a keyword that will return results.

On my site, I've searched for beanies, and it returns this:

A screenshot of the example page showing a list of items that match the keyword the user specified in the URL. In this case, it is a number of beanie hats.

👍

Performance Tips

In addition to our standard tips on improving search API performance, there are two additional parameters we can make to the fetch() method on the product model.

AjaxRequestsKiller

AjaxRequestsKiller is a module that you should add as a dependency to the file which makes the fetch and then is passed along with the data object when you a call, like this:

item = product.get('item').fetch(
{
  data: query
, killerId: AjaxRequestsKiller.getKillerId()
})

What this module does is keep of list of all AJAX requests that have been made but not yet completed. The problem is that if a user visits a page, makes an AJAX request, and then moves away before it completes, the AJAX will continue to operate in the background. What this does is listen for when the user goes somewhere else, and then kills any outstanding AJAX requests. We tag them by using the above parameter.

Note that if a user manually changes the URL in their address bar, the list of outstanding requests is lost, but it is maintained if they navigate around the site using Backbone. It's pretty useful for improving performance, especially on API calls where we expect the request to take time.

pageGeneratorPreload

This is a flag you can also set when fetching product details. Passing it along with your data and AJAX killer will specify to the application that we need not wait for the application to finish loading before making API calls. Note, however, that this only applies to pages created by the page generator — ie, it will improve performance for search engines crawling your site.

You can use it like this (it doesn't need to be added as a dependency, it's just a flag):

item = product.get('item').fetch(
{
  data: query
, killerId: AjaxRequestsKiller.getKillerId()
, pageGeneratorPreload: true
})

😎