Post Featured Image

Take a Closer Look at the Product List Page Component

This blog post refers to the product list page component found in the extensibility API. As such, it is only applicable to SuiteCommerce sites, or SuiteCommerce Advanced sites running Aconcagua or newer.

Over the past few weeks we've taken in-depth looks at various aspects of the extensibility API, including the PDP component, and the checkout and cart components. In this vein, I'm going to look at another component: the product list page (PLP).

The PLP component is available throughout the shopping application of your site, although many of its methods are not. Typically, you will only be able to retrieve data and perform actions on pages that have facets: ie, search results and commerce categories. Take a look at the JSDoc API documentation and familiarize yourself with its methods, you should see some interesting ones.

There won't be a strict structure to this blog post: I tried to think of a project that one could do for this but I couldn't think of something. In my experience, product list pages tend to be rather utilitarian, and, given their importance, our team has already put a lot of work into making them functional and good-looking. However, there are still small things you can do to modify things to make them fit closer to how you want them to be, so we'll look at all the bits that could help you.

Before we take a look together, I want to highlight something important: at the time of writing, there are no PLP-specific events defined in the API. My guess is that some will be added later and if you're reading this after they have been added (hi from the past👋), some of what I'm about to suggest may be able to be achieved more efficiently or quickly using those events. However, I think there's still good mileage in this stuff, so let's jump in.

NOTE — for the purposes of this tutorial, I am assuming you have set the PLP component to be available as PLP, whether globally (for testing purposes) or within the module you are working on, eg:

// For testing purposes (eg in your browser's console), you can make it a global variable by adding the following code to any module that is passed the application container (eg in a `mountToApp` function in an entry point file of a module)
window.PLP = container.getComponent('PLP');

// But for your final code, you should use a local variable instead
var PLP = container.getComponent('PLP');

Get and Set the Search Keyword

A lot of the methods are pretty straightforward to use, for example if I want to get the keyword that the shopper used, I can just use getSearchText(). But one of the things that's not immediately obvious about the setSearchText() method, is that you must pass it an object rather than a string.

// How to search for the 'tent' keyword
PLP.setSearchText({searchText: 'tent'})

Note that these methods (and a whole host of other PLP methods) will only work on a search results or commerce category page.

Get and Set Refinements

The filters available are the refinments you see in (usually) the left side of a search results page — eg price, size, color, etc. If you want to get these, there are two main methods: getFilters() and getAllFilters().

The first one will return a collection (an array of objects) of all filters that are currently applied to the search results; so, for example, if I have refined by the color orange:

PLP.getFilters()

// > {config: {…}, id: "custitem31", url: "custitem31", value: "orange", isParameter: true}

The id and the value values will probably the most useful to you in your customizations, but you will find a lot of stuff in the config object too.

You may find getAllFilters() particularly useful because it surfaces all possible refinements that could be made to a search, depending, of course, if were to be available.

When a shopper is on a search results page, you can trigger refinements with setFilters(). Like the search text method, it takes an object too but this one requires you to pass it an object within it.

// Apply a single filter, which is the size small
PLP.setFilters({filters:{custitem30: 'Small'}}

You can apply more than one filter at once if you wish:

PLP.setFilters({filters:{custitem31: 'orange', custitem30: 'Small'}})

Note that using this method will overwrite any existing filters. At the time of writing, there is no method to add or remove individual (or groups of) filters. If want to 'push' another one (or multiple new filters!) in, then you will need to write some custom code, eg:

PLP.__addFilters = function (newFilters)
{
  // store current filters
  var filters = {}
  PLP.getFilters().forEach(function (filter)
  {
    filters[filter.id] = filter.value
  });

  // add new filters
  _.extend(filters, newFilters);

  // apply the filters
  return PLP.setFilters({filters: filters});
}

So, what we're doing is adding a new method to the PLP component. I've double-underscored the start of the method's name because I am slightly worried about potential conflicts (ie if we — NetSuite — add a method in with the same name in the future).

Anyway, the first thing we do is store the current filters, so we get all the current filters and go through each one. We create key-value pairs in our new object based on the ID and the value of each of them.

Once we have that, we just add in a new key-value pair for our new filter, which we're going to pass to our new method in the form of an object.

Finally, we use the setFilters() method to apply them.

If we want to use this, we can just do: PLP.__addFilters({custitem31: 'orange'}) and it will add the color orange as a refinement. Easy-peasy.

To remove a filter, you could do something similar and just use the delete operator:

PLP.__deleteFilters = function (oldFilters)
{
  // store current filters
  var filters = {}
  PLP.getFilters().forEach(function (filter)
  {
    filters[filter.id] = filter.value
  });

  // remove old filters from current object
  oldFilters.forEach(function (key)
  {
    delete filters[key]
  });

  // apply the filters
  return PLP.setFilters({filters: filters});
}

It looks very similar except instead of looping through our supplied filters to add them, we just go through each one removing them from the filters. This obviously assumes that the provided oldFilters are supplied in an array, eg: PLP.__removeOldFilters([custitem30, custitem31]).

One final note: filters are case-sensitive and will fail silently if the key or value doesn't match exactly, so make sure you get them right!

Events

At the time of writing this, there are no PLP-specific events. You can, however, listen to beforeShowContent and afterShowContent, if you wish, to perform actions when the page loads or refreshes (ie it will be triggered when any of the aforementioned methods are triggered).

For this I thought about what you can do, and an idea I came up with was to show a banner at the top of the search results if the shopper had selected the color orange as a filter. The banner will encourage visitors to visit our special Orange Things commerce category that I added to my site back when we looked at how to get started with them. It should, obviously, not show if they haven't or if they remove it. So, how do we do this?

I've created a new extension and in it a module called PLPStuff. In my entrypoint file, PLPStuff.js, I've put:

define('PLPStuff'
, [
    'PLPStuff.View'
  ]
, function
  (
    PLPStuffView
  )
{
  'use strict';

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

      PLP.cancelableOn('beforeShowContent', function ()
      {
        PLP.addChildViews(PLP.PLP_VIEW,
        {
          'Facets.Items':
          {
            'PLPStuff.View':
            {
              childViewIndex: 1
            , childViewConstructor: function ()
              {
                return new PLPStuffView({PLP: PLP})
              }
            }
          }
        })
      });
    }
  }
});

The event listener waits for when the showContent method is about to be called and then runs its code first. In this case, we're using the generic addChildViews method, which allows us to add a new view to somewhere on the page before it's rendered.

The first parameter we pass it in is the name of the main PLP view, which can be accessed via the PLP_VIEW property of the PLP component (this usually translates to Facets.Browse.View). This is the container view, as it were.

After that we pass an object. The key of the object is the child view we want to add our new view directly into. In my case, I've looked up that the view for the search results is Facets.Items, so I'm setting that as the key. The value of that key is an object.

The key of that object is the name we want to give our new (grand-)child view, which is going to match its class name. The values of that object are simply the position we want to render it in (1, ie, at the top) and the constructor that is going to be used to create it, which is our new view. Note that we're passing it the PLP component as an option: it won't be available within the context of the view without doing this, so it's important.

Create the View

With that sorted, we can then work on the view itself. Create PLPStuff.View and in it put:

define('PLPStuff.View'
, [
    'plpstuff_banner.tpl'

  , 'underscore'
  ]
, function
  (
    plpstuff_banner_tpl

  , _
  )
{
  'use strict';

  return Backbone.View.extend({
    template: plpstuff_banner_tpl

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

  , showBanner: function showBanner ()
    {
      return !_.isEmpty(_.find(this.PLP.getFilters(), function (filter)
      {
        return filter.value == 'orange'
      }))
    }

  , getContext: function getContext ()
    {
      return {
        bannerUrl: 'img/lookingfororangethings.png'
      , showBanner: this.showBanner()
      }
    }
  })
});

We'll get to the template soon.

The first thing we do is make the PLP component available throughout the view by assigning it to this.PLP — we need to do this so we can access the collection of the current filters. If we want to be more precise, we could have just passed the filters, but we could conceivably use other methods in our view for other customizations, right?

Then we create the method that we will use to test whether we are to show our banner. Remember, the requirement is that we show a banner linking to our commerce category, so we need to check whether the color orange is a filter currently applied to the search results and then return true or false.

We know that PLP.getFilters() returns a collection of all filters. To go through them, we're using the find() Underscore method, which loops through the list looking for a result that matches our criteria. In my case, I'm looking for an object which has a key called value with a value of orange. Once we have that, we can then use isEmpty() (another Underscore method) to check whether the result is empty. It'll return true if our filter is not currently applied, so we just negate that with a !. Thus, we now have a simple true/false determiner.

With that, we just need to pass this value to the template, along with the URL for our banner, in our context object.

Create the Template

The template is simple: check to see if we should show the banner, and, if true, do it!

Create Templates > plpstuff_banner.tpl:

{{#if showBanner}}
    <a href="/orange-things"><img src="{{getExtensionAssetsPath bannerUrl}}"></a>
{{/if}}

Save and Test

That's pretty much it. The view will automatically add the view to the existing view; then that view will check whether to show the banner, passing that to the template.

Now when I test this on my local server, I can see the following banner when I refine by the color orange (and then banner disappears when I remove that refinement):

Nice work! 👌

If you wanted to, you could do other things, for example to listen for when a shopper refines by price, and then perhaps offer them a link to your sales category? Let's say you wanted to check whether they have refined by price and that the upperbound (ie the to value) is $30 or less:

!_.isEmpty(_.find(this.PLP.getFilters(), function (filter)
{
  return filter.id == 'pricelevel5' && parseFloat(filter.value.to) <= 30
}))

Get Pagination, Sorting and Item Info

Before we begin, I'm going to start with something you already know. In short, the results of a search are typically split up across multiple pages — this is where we should only a select number of items to a shopper at once. There are a number of things that you or the shopper can do to manipulate this:

  • Get or set the number of items shown at once with getPageSize(), setPageSize(), and getAllPageSize()
  • Get or set the current page with getPagination() and setCurrentPage()
  • Get or set the sorting items with getSorting(), setSorting(), and getAllSorting()
  • Get or set the display types with getDisplay(), setDisplay(), and getAllDisplay()
  • Get information on the items in the current page with getItemsInfo()
  • Get the current item search API URL fragment with getUrl()

I tried to think of particular uses for these methods and I struggled. I suppose you could create a configuration record where site merchandisers specify particular categories or keywords that should have a different number of items shown per page, and then you trigger particular page sizes to better display them. Similarly, if there are particular types of sorting that suit particular categories or search results, you could make changes based on those options.

I've talked before about adding a custom display option, and a combination of that with the above methods could make an interesting combination. You could have a designated page size, sorting and display option for a commerce category! However, there seems to be an issue with chaining multiple PLP methods together with deferred.then(). The thing is, changing pagination, filtering, sorting, etc, all trigger Backbone to use history.navigate(), which causes the page to (internally) reload, call the search API again, etc. Doing this multiple time is bad for performance. This may get addressed in future versions of the API.

You can however, if you really must, chain them with if statements:

PLP.cancelableOn('beforeShowContent', function ()
{
  if (PLP.getCategoryInfo() && PLP.getCategoryInfo().urlfragment == 'orange-things')
  {
    PLP.setPageSize({pageSize: '12'});
    if (PLP.getPageSize().id == '12')
    {
      PLP.setDisplay({display: 'table'})
    }
  }
});

But this feels like a workaround. Plus, in addition to the original API call, it triggers two additional ones, so I'm not keen on it, to say the least.

As for getItemsInfo(), remember this will return data of the items currently being shown on the page. Essentially, it is just a copy of the items object attached to the model that's passed to the collection view when the page renders. There's a lot of information available for each item (depending on the field set you use, of course) so you can rely on this if you need to do something specific to the items that are currently being shown.

// Return all items in the list where the price is above $42
PLP.getItemsInfo().filter(function (item)
{
  return item.onlinecustomerprice > 42
})
// > (9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]

// Return a count of the items in the list that are in stock and those out of stock
_.countBy(PLP.getItemsInfo(), function(item)
{
  return item.isinstock ? 'In stock': 'Out of stock'
});
// > {In stock: 18, Out of stock: 6}

// Return an array of product images for a random item in the list
_.sample(_.pluck(PLP.getItemsInfo(), 'keyMapping_images'),1)
// > [Array(8)]

I don't know, you might a find a use for some of these.

Finally, the getUrl() returns the URL the page is currently using to fetch data from the items API. Again, it's kinda hard to point to a specific use that you may have for this, you may find it useful for debugging. For example, the following command in your developer console will copy the search API URL to your console:

// Just the path
copy(PLP.getUrl())

// The full URL
copy(SC.SESSION.touchpoints.home + PLP.getUrl())

And then you could paste this into a new tab or a text file somewhere, to help you with your debugging.

Final Thoughts

The PLP component is one of the components of the new extensibility API. While it may be available throughout the shopping application, you'll likely only find joy using it while on a search results or commerce category page. In the case of categories, don't forget that you can use getCategoryInfo() to get information on this specific category, which may also be of use to you.

I also gave an example of how to add a child view to the page. Note that this method is generic across all components, so feel free to adapt it to fit your site's customizations. Keep in mind that if you want to add a view to the checkout, I recommend reading up the special way to do that.

Finally, while formal events for the component are not available, you can still make use of the generic beforeShowContent/afterShowContent events, which trigger every time the page changes state (eg changes to filters, keywords, display options, sorting, etc). From there, you can use conditional statements for specific changes (eg specific filters, keywords, etc). You can also use getItemsInfo() to check to see if any particular items are present in the current list — I've given some examples above, but you may also find something useful in the hollows of the Underscore documentation too.

If you want a copy of my code, see PLPStuff.zip.