Show Items as Discounted with a Custom Field, Facet and Product Detail Page

Shoppers love special offers and discounts. Maybe it's the thought that you're getting something cheaper than its retail price but there is a sense of urgency and excitement around it. One thing you can do as a retailer to stoke that sense of urgency and excitement is to discount some items and then draw your shoppers' attention to them.

In this article I'm going to take you through the steps for one way of highlighting discounted items. However, the things you'll learn have a wide range of applications throughout your site. Some of the things we'll look at include:

  • Creating a custom field
  • Extending a view's context object
  • Getting properties from the context onto a template
  • Creating, configuring and implementing a custom search facet

You may remember that in a previous article, we talked about facets — this one will take those concepts a little further. If you're unfamiliar with how they work, I advise checking it out first.

Set up the Custom Field

The custom field will be used to store the information about the products that they are on sale. For simplicity's sake, this field will be a checkbox that stores a boolean indicating whether it has been discounted or not. However, you could set this to a text field and store, for example, a percentage of how much the product has been discounted (thus shoppers could search for, say, products that are 30% or more discounted).

Add the Custom Field

To start, go to the backend and then Customization > Lists, Records & Fields > Item Fields > New. Create a new custom field as follows:

  • Label: Discounted
  • ID: _discounted
  • Type: Check Box
  • Applies To Tab

    • Inventory Item: (checked)

You can leave all other fields set to their defaults. Save the field.

Facet and Field Set Data

Next you'll need update your field set and facet data to include your new custom field. As a reminder, field sets determine what data is exposed to site templates; facets are the refinement options on the side of search results.

Go to Setup > SuiteCommerce Advanced > Set Up Web Site and then edit your site.

Firstly, under the Search Index tab, select the Facet Fields sub-tab and then Add button. Select the new custom field from the list. Click OK.

Next we need to configure the field sets used on the search results and product details pages; on my site these are the details and search field sets. Edit each one and add the custom field to each set. Make sure you remember to click OK after editing each one to save your changes. Then click the Save button.

Update a Product

The final bit of data modification relates to products. For the purposes of this tutorial we're going to need at least one product that is marked as discounted.

Navigate to a product by going to Lists > Web Site > Items and edit it. If the item is a matrix item, it is preferable to edit the parent item. Go to the Custom tab and check the Discounted field. Save your changes.

Test the Field

OK, so we've created the field and updated a product so that it has that field marked as true. Now we should perform a basic test to make sure it's working. Go to your search page. You should see your new facet, with basic formatting, in the list of available refinements.

We'll get to sorting this out later, but for now click the true option so that we can return the product that we set to be discounted.

If your facet doesn't show (or the true value isn't showing) then run through the steps above again and make sure you've done them correctly. You may also need to run a search re-index.

Create New Custom Module

Because of the scale of this modification I recommend making a custom module which we'll use for all our changes.

The principle concept for this modification is the prototype method. We're going to do it this way because it gives us the greatest likelihood of interpolating our customizations painlessly when we upgrade the modules later. In other words, we are extending not overriding.

Add Custom Properties to the Context

Start by doing all the normal steps, such as building out the folder structure, creating the ns.package.json file, and updating distro.json. For the purposes of this tutorial, I'm going to name my functionality DiscountDisplay.

Then, under your JavaScript folder, create the entry point file, DiscountDisplay.js. In it, put the following code:

define('DiscountDisplay'
, [
    'underscore'
  , 'ItemDetails.View'
  ]
, function (
    _
  , ItemDetailsView
  )
{
  'use strict';

  return {

    mountToApp: function (application) {

      ItemDetailsView.prototype.getContext = _.wrap(ItemDetailsView.prototype.getContext, function(fn)
      {
        var context = fn.apply(this, _.toArray(arguments).slice(1));
        context.isDiscounted = this.model.get('custitem_discounted');
        return context;
      });
    }
  }
});

So let's run through what's happening here. Firstly, in terms of dependencies we are including Underscore, which will provide us with some functionality to make prototyping easier. Next we're including the main view from the ItemDetails module, which is what connects to the PDP template we want to send the value of our new field to.

After that we have our mountToApp function. It is here that we're putting our important code. As previously mentioned, we use prototyping because it is the cleanest way of extending a specific property, object or method. Here we're taking ItemDetailsView, prototyping it and defining the getContext function of it. We're using the wrap function provided by Underscore, which it defines as:

Wraps the first function inside of the wrapper function, passing it as the first argument. This allows the wrapper to execute code before and after the function runs, adjust the arguments, and execute it conditionally.

Thus, what we're doing is taking the context for the view and preparing it so we can add our own values to it.

Next, within that function, we create a variable out of the prototype (we're calling it context, but you can call it what you want) by taking fn and slicing out the first argument (which is the original function itself).

Finally, we have our object that we want to modify. Then all we need to do is add in our new property and its value (i.e., isDiscounted) and then return the variable.

Make the Sass and Template Changes

Now there are two ways for us to get our template change to take effect. The first applies if you've not made any customizations to the item_details.tpl template file. However, considering how integral this is to the PDP I'd imagine that you've already made an override for this file as to add your customizations.

We have a whole document on overriding templates and extending Sass that you can read, so I won't run through the steps here. Suffice to say, you connect your desired template changes to the new property in the context like you would any other property. So, for testing purposes, you can put the following change into your template:

{{#if isDiscounted}}<p>ON SALE!</p>{{/if}}

After (re)starting your local server, you'll see the following change on your discounted product's PDP page:

To take it one step further, you can add some styling and even link in back to the search results for all discounted products. For example:

All of this you can do yourself to your site's specifications. Make sure you look at a non-discounted product and to test that it doesn't show.

Customizing the Facet

The final bit of work related to this is the facet. As mentioned previously, it doesn't look like much — so let's change that.

You may have read my previous article on facets and I want to build on that.

Frontend Configuration

As the article states, configuration for facets is handled in the frontend configuration file for the shopping application, namely SC.Shopping.Configuration.js, which is found in Modules > suitecommerce > ShoppingApplication > JavaScript. And, like all other JavaScript files, you should avoid overriding them — instead, you should extend them. We've written up documentation on what this means, if you're unfamiliar. For the purposes of this tutorial, I'm assuming you already have an extension file for SC.Shopping.Configuration.js (the frontend configuration file for the shopping application); if you don't, you'll need to set one up using the instructions in the docs.

In combination with the details in that article and the documentation you should be able to see what properties we should add to configure our facet. For the sake of ease, I'm going to include a complete frontend configuration file (you can modify your own as appropriate):

define('Configurator'
, [
    'SC.Shopping.Configuration'
  , 'discount_display_facet.tpl'
  ]
, function (
    ShoppingConfiguration
  , discount_display_facet_tpl
  )
{
  'use strict';

  return {
    mountToApp: function ()
    {
      ShoppingConfiguration.facets = _.extend(ShoppingConfiguration.facets,[
      {
        id: 'custitem_discounted'
      , name: _('Sale Status').translate()
      , template: discount_display_facet_tpl
      , uncollapsible: true
      }
      ]);
    }
  };
});

So I've got two dependencies: the frontend configuration file for the shopping application and a new facet template, which I haven't created yet.

Next I'm extending the facets object to add in my customizations for my new facet. The properties are pretty standard but note that we've set the template property to match our new non-existent template.

Once you've saved that, let's move onto the template.

Create the New Template

You'll need to create a Templates directory under the DiscountDisplay directory if you haven't already. Also, if your ns.package.json file doesn't include the templates directory then make sure you add that too.

Within your templates directory, create discount_display_facet.tpl and paste in the following code:

{{#if showFacet}}
    <div class="facets-faceted-navigation-item-facet-group" id="{{htmlId}}" data-type="rendered-facet" data-facet-id="{{facetId}}">
        {{#if showHeading}}
            {{#if isUncollapsible}}
                <div class="facets-faceted-navigation-item-facet-group-expander">
                    <h4 class="facets-faceted-navigation-item-facet-group-title">
                        {{facetDisplayName}}
                        {{#if showRemoveLink}}
                        <a class="facets-faceted-navigation-item-filter-delete" href="{{removeLink}}">
                            <i class="facets-faceted-navigation-item-filter-delete-icon"></i>
                        </a>
                        {{/if}}
                    </h4>
                </div>
            {{else}}
                <a href="#" class="facets-faceted-navigation-item-facet-group-expander" data-toggle="collapse" data-target="#{{htmlId}} .facets-faceted-navigation-item-facet-group-wrapper" data-type="collapse" title="{{facetDisplayName}}">
                    <i class="facets-faceted-navigation-item-facet-group-expander-icon"></i>
                    <h4 class="facets-faceted-navigation-item-facet-group-title">{{facetDisplayName}}</h4>
                    {{#if showRemoveLink}}
                        <a class="facets-faceted-navigation-item-filter-delete" href="{{removeLink}}">
                            <i class="facets-faceted-navigation-item-filter-delete-icon"></i>
                        </a>
                    {{/if}}
                </a>
            {{/if}}
        {{/if}}

        <div class="{{#if isCollapsed}} collapse {{else}} collapse in {{/if}} facets-faceted-navigation-item-facet-group-wrapper">
            <div class="facets-faceted-navigation-item-facet-group-content">
                <ul class="facets-faceted-navigation-item-facet-optionlist">
                    {{#each displayValues}}
                        {{#if isBooleanFacet}}
                            <li>
                                <a class="facets-faceted-navigation-item-facet-option {{#if isActive}}option-active{{/if}}" href="{{link}}" title="{{label}}">
                                    {{translate 'On Sale!'}}
                                    {{#if isActive}}
                                        <i class="facets-faceted-navigation-item-facet-option-circle"></i>
                                    {{/if}}
                                </a>
                            </li>
                        {{/if}}
                    {{/each}}
                </ul>
            </div>
        </div>

    </div>
{{/if}}

There's a lot of code here, but you'll note that it is essentially a cut-down version of facets_faceted_navigation_item.tpl, the default facet template. However, we've made some important changes.

Firstly, we've added a conditional statement: {{#if isBooleanFacet}}. We haven't added code to support this yet but it's checking if this value is true before proceeding. If it is true then it renders the following HTML, if it's not then it does nothing. We're doing this because we only want show one option. When we use the default template, it returns two options: true and false. As the two options are mutually exclusive, it gives us three possible states:

  1. Show all items, regardless of whether they are discounted or not.
  2. Show only discounted items.
  3. Show only non-discounted (i.e., full-price) items.

It's extremely unlikely that a shopper will ever choose the third option, so we're going to remove it to improve user experience.

Thus, we know that there can only ever been one option for the shopper to select. And for that we're swapping out the variable {{displayName}} for a string, specifically {{translate 'On Sale!'}}. After all "true" is a bit jarring to shoppers, so let's over friendlier text.

And that's it for the template. Because of the way the template works, a shopper can use the single facet value to toggle discounted items on or off by simply clicking the one facet value, or by clicking the removal link that'll be generated next to it.

The final thing left to do is to get that isBooleanFacet property added to the context object for the view, so let's do that now.

Add Custom Properties to the Context (Again)

By now you should know how to do this: the method is the same as the one we did earlier for the ItemDetails view. So, go back to DiscountDisplay.js and add in Facets.FacetedNavigationItem.View as a dependency. Then, in the mountToApp function add in the following code:

FacetsFacetedNavigationItemView.prototype.getContext = _.wrap(FacetsFacetedNavigationItemView.prototype.getContext, function(fn)
{
  var context = fn.apply(this, _.toArray(arguments).slice(1));
  _.each(context.displayValues,function(value)
  {
    value.isBooleanFacet = value.displayName === 'true';
  });
  return context;
});

As I said, the method of updating the context is the same but the difference here is that we're running through the displayValues object and creates a new property for each. For each one it evaluates whether the displayName for each value is 'true', if it is then it sets the value to true; if not then it is automatically set to false.

You can see, therefore, that this connects neatly into the template. You'll also see that this property won't affect other templates as it isn't used in them.

Test Your Work

Great! That's everything, so save your files and (re)start your local server. Go to your search and take a look at your facets, you should see something like this:

Now select the value to filter by discounted items. It should return your discounted product. Note that you can click the removal link next to the facet or the facet value itself to remove it from the filter. In this way, the facet value works like a toggle.

Summary

In this article we walked through the steps of creating a custom field. The field was used to filter for items that we marked as 'discounted'. We then worked on showing content on the PDP if the product had this field marked as true. Finally, we looked at how we could create a custom facet so that shoppers could refine search results for discounted products.

One of interesting parts of this work was the way we learned how add our own custom properties to a view's context object. This allows us to send values to templates so that we might either print them in the HTML as variables, or use them to do basic logic.

If you're interested in learning more about facets, I'd recommend looking at Facets.FacetedNavigationItem.View.js, which is the view whose context we extended earlier. In particular, if you pay attention to what's happening in the getContext function, you'll see how the values array is constructed. If we were editing this file (i.e., overriding it) then we could have easily added in our property there.

However, as has been demonstrated in this article numerous times, it's easy and preferable to simply extend the parts of JavaScript files that we want to modify or add to. We did it with two views and also the shopping configuration file. Thus, not only do we have a clear separation of our code but it means that when it comes up to upgrade these files, our customizations should work with minimal to no fuss.

Further Reading