Experiment with the Product List Page Component
Caution: this content was last updated over 4 years ago
One of the components of the extensibility API is the Product List Page component. It enables developers to get and set important data and options on search results and category list pages. If you’re new to the extension framework, this component is a good place to start tinkering around.
The PLP component is available when operating within the context of a product list page. 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 article. We will look at a number of different methods available on the component, and then build out a very simple extension idea. The extension will check the filters the user has applied to their search, and then show them a banner if they have selected a particular color (orange).
The PLP has no component-specific events — any it has, it inherits from the base component.
Accessing the Component
As with all component, it is accessible via an instance of the current application. In entry-point files for your extensions, you typically access this in your mountToApp()
method. For example:
mountToApp: function mountToApp (container) {
var PLP = container.getComponent('PLP');
}
You can then access its methods as you wish, or pass it on another object (eg a view constructor).
Before doing so, you should check whether it exists, eg:
if (PLP) {
// do something
}
The PLP component is only available in the shopping application, so this condition will not return a truthy value in the checkout or my account applications.
However, the PLP component will return a truthy value anywhere else within the shopping application (ie outside of product list page instances) so you will need to exercise caution when invoking its methods. You don’t need to worry too much if you are using it on a child view that you have passed into the main PLP view, for example, because that code will only ever execute when the main PLP view is the current context.
Testing and Tutorial Purposes
All of the code snippets in this page assume you have already accessed the PLP component in your code.
If you want to experiment with it in your browser’s developer console, you can temporarily make it available globally.
To do this within your code, you can add the following to your extension’s entry-point file:
window.PLP = container.getComponent('PLP');
This will, obviously, assign it to the global namespace allowing you to access it via the developer console simply as PLP
.
You can also access it directly in your developer console by doing the following:
var PLP = SC.Application('Shopping').getComponent('PLP');
This accesses the SC
global variable. This has the benefit of not requiring you to make changes to an extension, but it does mean you have to do it every time you refresh the page.
It is not good practice to use either of these in your production code, but they can be shortcuts for developers who need to quickly test something.
A number of code samples will use custom field names in examples, you will obviously need to change them to suit your site’s configuration.
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 refinements 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'}})
Adding or Removing Individual Filters
There are currently no methods that support the adding or removing of individual filters. In short, if you wish to do this, then you will need to:
- Get the existing array of filters by using
getFilters()
- Add or remove a filter from this array
- Apply the new array of filters using
setFilters()
For example:
// Assumes you have access to Underscore and the PLP component
// newFilters is an array of filter objects
MyObject.addFilters = function (newFilters) {
// Store current filters
var filters = {};
PLP.getFilters().forEach(function (filter) {
filters[filter.id] = filter.value
});
// Add new filters to that object
_.extend(filters, newFilters);
// Apply the filters
return PLP.setFilters({filters: filters});
}
MyObject.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});
}
Example Customization
So let’s look at a meatier example.
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 have already created. It should, obviously, not show if they haven't or if they remove it. So, how do we do this?
For all the code mentioned in this section, see our GitHub repo 2018-1-aconcagua/PLPStuff.
Create the Entrypoint
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 upper bound (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()
, andgetAllPageSize()
- Get or set the current page with
getPagination()
andsetCurrentPage()
- Get or set the sorting items with
getSorting()
,setSorting()
, andgetAllSorting()
- Get or set the display types with
getDisplay()
,setDisplay()
, andgetAllDisplay()
- 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.
Caution: Chaining Multiple Setters
A number of setter functions force Backbone to re-render the page. For example, setting a new page size will require a new query to the item search API, therefore triggering the page to (soft) reload. If you try to chain together multiple setter functions, you will run into difficulty because the page reload will evacuate the rest of your code
There is no ‘best practice’ to workaround this. It’s a little hacky but you can, however, chain them with if
statements:
PLP.cancelableOn('beforeShowContent', function ()
{
if (PLP.getCategoryInfo() && PLP.getCategoryInfo().urlfragment == 'orange-things')
{
if (PLP.getPageSize().id == '12')
{
PLP.setDisplay({display: 'table'})
}
else
{
PLP.setPageSize({pageSize: '12'});
}
}
});
So, for example, I am checking if we’re on the category page for my orange things. If I am, I then perform another check to see if the page size has been to set to 12 items; if it hasn’t then I set it (which will reload the page and then re-trigger this code), if it is then we change the display option.
So yeah, this isn’t great. Not only is it a lot of nested callbacks, it triggers multiple additional API calls.
In other words, I would avoid doing this.
Item Info
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. But do note that if you’re thinking about customizing the checkout, see our page on how to Add a New Module to the Checkout with the Extensibility API.
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 Underscore documentation too.
Code samples are licensed under the Universal Permissive License (UPL), and may rely on Third-Party Licenses