Post Featured Image

Create an Extension to Show Color Options on a Product List Page

This functionality is written as an extension for Aconcagua R2 and newer, and SuiteCommerce. It requires the use of the PLP component from the extensibility API. Therefore, it is not appropriate for older sites.

NOTE: this post was updated August 1, 2018. It had previously stated that there was no way to get item context data in item cell views using addChildView — this was incorrect. There is a property called contextDataRequest that can be attached to a new child view to create a copy of the model appropriate to its context. We'll look at this below.

This blog was further updated on August 6, 2018 to reflect that in the R2 release of Aconcagua, we added the environment component to the extensibility API, which allows us to get configuration property values without using SC.Configuration.

Shortly after I wrote my blog post on the PLP component, I received feedback from a customer who was curious to know if it would be possible to use it to replicate some custom functionality they had written for their site. What it does is listen for when a user hovers an item on a product list page, and then shows them all of the color options for it. So, for example, if the'yre hovering a dress (which is by default black) then it'll also show images and links for the dress in red, blue, green, etc. For retail sites, this seems super cool and I became curious about it too — first of all in their functionality, but also whether the new API was up for the job.

Here's how it looks:

I think it's pretty cool. While an interesting bit of functionality in itself, this is also a bit of a showcase of some useful Underscore methods; if you remember, we looked at some of my favorites before — so here are some more!

Core Concepts

In short, it is possible to write this extension purely with the extensibility API. We need two key things:

  1. To add a new child view to each item cell on product list pages (search results)
  2. To access the item data of each of those cells

For the first one, we have two methods that are available throughout most components in the extensibility API: addChildView() and addChildViews. What these do is take a target (an existing view) and a view constructor; with that information, it can create a new view in the place specified. For the second method — addChildViews — you can specify additional settings, such as the index (ie its position), for additional control.

The second, we have the contextDataRequest property, which we can attach to our new view. You may be familiar with contextDataRequest if you've worked with custom content types (CCTs) before. What it does is allow us to specify the context that our new view is being dropped into, so that it can request data. For us, we can specify that the view is going to relate to items, so we can set its value to ['item'] and we will be returned a copy of the item model. So, in other words, the view will know which item it relates to and and will therefore be able to provide data about the available options and images.

So, once we have our new view created, we can pass it a template. The template can be told to generate images and links based on the colors and images made available to it from its context object. When we join all of that together, our final functionality will be a bit of styling away from being exactly what we want. The challenge, therefore, is getting the right information, so let's dive into it and see how we can do this with the extensibility API.

Basic Setup

I am assuming that, at this point, you have either created a new extension or set up a customizations folder for this new functionality. Here's the structure I'm going with:

PLPItemColors

  • Modules
    • Configuration
      • PLPItemColors.json
    • JavaScript
      • PLPItemColors.Hover.View.js
      • PLPItemColors.js
    • Sass
      • _plp_itemcolors-hover.scss
    • Templates
      • plp_itemcolors_hover.tpl
  • manifest.json

From here, we can start with the entrypoint file.

Create the Entrypoint

The entry point file is called PLPItemColors.js. In it, put:

define('PLPItemColors'
, [
    'PLPItemColors.Hover.View'
  ]
, function
  (
    PLPItemColorsHoverView
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      var PLP = container.getComponent('PLP')
    , Environment = container.getComponent('Environment')
    , customColorId = Environment.getConfig('plpItemColors.customColorId')
    , rejectDefault = Environment.getConfig('plpItemColors.rejectDefault');

      if (customColorId)
      {
        PLP.addChildView('ItemDetails.Options', function ()
        {
          return new PLPItemColorsHoverView(
          {
            customColorId: customColorId
          , rejectDefault: rejectDefault
          })
        });
      }
    }
  }
});

We've got just one dependency: a view we're going to create later. Normally, we'd need the configuration object so that we could pluck out some properties; however, new in Aconcagua R2, we've got access to the environment component, which allows us to get configuration values nice and easily. So, that's one less dependency we need to think about.

Once we have the values we're going to need, we wrap the whole thing in a check to see if a value has been set (we're going to need this value in our view, so we want to check it at least has a value before proceeding). The customColorId is going to be ID of the field your site uses to store the colors for your product's item options. Having the correct value for this setting is mandatory for this functionality to work, so we're doing a basic check and making sure it at least has a value. If you want to disable this check for local testing, you can just comment it out.

Then, in our mountToApp, which runs when the code is loaded the first time, we're calling on the product list page component. We're going to invoke the aforementioned addChildView() method to create our new view. We're going to pass it two configuration values that we're going to need (these will appear in its options object).

Note the child view we're specifying: it's the view that's used for item details options, but it could be any child view that's in the item cell parent view. You can find the list of child views by inspecting the item cell element in your browser, and looking out for data-view attributes in the source. You could use the one used for price, stock, rating, etc, it doesn't really matter for our purposes. Anyway, once we've specified that, we just pass it a simple view constructor: just return a new one, with no options. It is the view that will be doing the work, so let's move onto that.

Add the View

The view will be created before the page is rendered (but after the item data is made available).

Create PLPItemColors.Hover.View and put it in it:

define('PLPItemColors.Hover.View'
, [
    'plp_itemcolors_hover.tpl'

  , 'underscore'
  ]
, function
  (
    plp_itemcolors_hover_tpl

  , _
  )
{
  'use strict';

  return Backbone.View.extend({
    template: plp_itemcolors_hover_tpl

  , contextDataRequest: ['item']

  , getItemColors: function getItemColors ()
    {
      var itemOptionFields = this.contextData.item().itemoptions_detail.fields
      , itemImagesDetailsMedia = this.contextData.item().itemimages_detail.media
      , thumbnail = this.contextData.item().keyMapping_thumbnail.url
      , self = this

      var itemColors = _.find(itemOptionFields, function (field)
      {
        return field.internalid == self.options.customColorId
      }).values;

      itemColors = _.compact(_.map(itemColors, function (color)
      {
        if (color.internalid)
        {
          return {
            color: color.label
          , url: color.url
          }
        }
      }));

      _.each(itemColors, function (color)
      {
        var image = itemImagesDetailsMedia[color.color] && itemImagesDetailsMedia[color.color].urls ? itemImagesDetailsMedia[color.color].urls[0].url : ''

        color.image = image;
      });

      if (this.options.rejectDefault)
      {
        itemColors = _.reject(itemColors, function (color)
        {
          return color.image == thumbnail;
        });
      }

      return itemColors
    }

  , getContext: function getContext ()
    {
      return {
        itemColors: this.getItemColors()
      }
    }
  })
});

After assigning the template, we request the item data. You'll be familiar with this property if you've worked with CCTs before. We specify that we want item model data. That's the context we're working in. By specifying this, we will be returned a copy of the item model available via this.contextData.item(). From there, we can get the information we want.

After that, we essentially just have one large function that processes the item's model so that we can pluck out all of the bits we want and return a neat little object.

We start with some simple variables; these are obviously not necessary but will make the code easier to read.

After that, we have our first bit of meat. We're going to start with an object that contains all of the different item options that the item has. You can test this by doing something like the following on a PLP:

PLP.getItemsInfo()[0].itemoptions_detail.fields
> [
> {ismandatory: true, internalid: "custcol_gen_color", ismatrixdimension: true, values: Array(4), label: "Color", …},
> {ismandatory: true, internalid: "custcol_gen_size", ismatrixdimension: true, values: Array(6), label: "Size", …}
> ]

What we're with this, is running it through _.find(). This Underscore method takes a list (array of objects) and tries to match it against a criteria. If it's successful, it returns the object that matches first. So what we're doing is saying find us the item options object that matches our custom field ID that we use for storing color options, and then return the values of it. Now, at this point if you haven't entered a value for customColorId, you'll need to temporarily hard code the value (eg custcol_gen_color).

When this runs, it'll return something like this:

> {label: "- Select -", isAvailable: true, url: "/Power-Ananda-Capri_42?custcol_gen_color=undefined"},
> {internalid: "1", label: "black", isAvailable: true, url: "/Power-Ananda-Capri_42?custcol_gen_color=1"},
> {internalid: "13", label: "burgandy", isAvailable: true, url: "/Power-Ananda-Capri_42?custcol_gen_color=13"},
> {internalid: "5", label: "grey", isAvailable: true, url: "/Power-Ananda-Capri_42?custcol_gen_color=5"}

Great! This contains some useful data, but we can tidy it up a bit so that the object we're working with is neater. We need four things:

  1. The object must only contain legitimate item options: the "- Select -" option for example, needs to be removed
  2. It needs to return the color name (label)
  3. It needs to return the URL that directly links to the PDP for the item where the option value is pre-selected
  4. It can't return any falsy (eg undefined, null, etc) values

There are a couple ways of doing this; we could use something like _.filter(), but I want to reformulate the object so that it only has the values I want with the property names that I want. For that, we're going to use _.map(). When you map an existing list, it goes through every object in that list and returns a new object based on the key-value pairs that you specify.

We check the legitimacy of each option by seeing if it has an internal ID; if it does, we tell it to return a simple object containing only the name of the color and the URL to the PDP for that item (with that option attached as a URL parameter); it it doesn't then it returns undefined.

Now, the problem with undefined is that a) its a useless value in our new list and b) it will mess with our code later (eg if we write code that assumes its an object and tries to pull a property from it). This is no good. Helpfully, there is an Underscore method called compact(). What this is does is go through every item in the list and removes 'falsy' values — things like undefined, null, 0, etc. Handy.

Now, when I log itemColors to my console, my object looks like this:

> {color: "black", url: "/Power-Ananda-Capri_42?custcol_gen_color=1"},
> {color: "burgandy", url: "/Power-Ananda-Capri_42?custcol_gen_color=13"},
> {color: "grey", url: "/Power-Ananda-Capri_42?custcol_gen_color=5"}

Now that we have our list of colors and their urls, there is bit of data we need: a single image to represent each color. Just like the rest of the item data, these are available in the item's model.

We start by using _.each (a library function for a loop) to go through each of these item colors and then we discover whether there is a corresponding image for the color in the itemimages_detail object. If there is, we pluck the URL out, otherwise we return an empty string: remember, SuiteCommerce is set up so that empty stringed images will return the 'image not found' placeholder image instead, so we don't have to worry.

If you're unfamiliar with the square bracket notation I use in the loop, it is a useful way of accessing a property dynamically (eg in a loop where we might use a variable). Typically, when you write code to access a property of an object, you know that property name. However, in our case, the property names are all names of the colors we want to access, so we can't provide it with a string (eg media.red, media.orange, etc), so we use bracket notation because it lets us use a variable instead. In our loop, we've set a variable of color to represent the color we're currently iterating over, so we pass that in square brackets and it will be evaluated (without having to use eval) before it attempts to read the object property. Super handy.

So, once that runs we have something like this:

> {color: "black", url: "/Power-Ananda-Capri_42?custcol_gen_color=1", image: "http://mysite.com/images/OL104.media.black.01.jpg"},
> {color: "burgandy", url: "/Power-Ananda-Capri_42?custcol_gen_color=13", image: "http://mysite.com/images/OL104.media.burgandy.01.jpg"},
> {color: "grey", url: "/Power-Ananda-Capri_42?custcol_gen_color=5", image: "http://mysite.com/images/OL104.media.grey.01.jpg"}

And that's basically it. Now, we can also run an additional clean-up process if we want. This is what I can the 'reject default' option: this will remove the color from the object if it matches the one used in the thumbnail image on the PLP (ie the default image). This is useful if you want to only show alternatives to the current image, rather than all possible options.

In the code, we've set this to be a configurable option so, again, if you haven't uploaded and set it yet, you can just change it to if (true) or comment it out. Anyway, for this we're using another neat Underscore method: reject(). Do you remember when we used find()? Well, this is basically the opposite of that. Rather than finding a particular object in a list and returning just that, reject() goes through the list and finds it, and then returns everything except that. It's an easy way of removing a problematic object from a list. Great stuff! 👍

Finally, in this view, we return the item colors and then pass it to the context object, so that we can work with them in our template.

Create the Template

With the data handled and pushed to the context, we can work with it in our template. Remember, we're going to be rendering this as part of a collection, so the template will be rendered for each item in the list, plus we want to show an image, name and link for each color associated with each item.

In Templates, create plp_itemcolors_hover.tpl with:

<div class="plpitemcolors-hover-container">
    {{#each itemColors}}
        <a class="plpitemcolors-hover-block" href="{{this.url}}">
            <img src="{{resizeImage this.image 'tinythumb'}}">
            <span>{{this.color}}</span>
        </a>
    {{/each}}
</div>

The key thing we're using here is a Handlebars helper called each. As you probably know, this is its way of doing loops. We're saying that for every object in itemColors, create an anchor element linking to the PDP and show its image and name. Note that we can reference the current object by using this within the block.

Another thing we're taking advantage of is the resizeImage custom helper, which takes the image and a specified by image size ID, and calls a version of the image in that size. I'm using my tinyThumb size, which generates resized images in 100 x 100px size. Remember, it's good for performance to resize images before you request them: we're going to be requesting a lot of extra images, and it's important that you don't use full-size images, resized in the browser. If you haven't already, set up your site for image resizing.

And that's basically it for the template. Now we can just add in some styling.

Create the Sass File

The styling you use for your site will be dependent on your theme, how you like things to look, etc, so take this with a pinch of salt. Anyway, create Sass > _plp-itemcolors-hover.scs and in it put:

.plpitemcolors-hover-container {
    position: absolute;
    right: -1500px;
    top: 0;
    transition: 1s;
}

.facets-item-cell-list:hover .plpitemcolors-hover-container {
    right: 0;
}

.plpitemcolors-hover-block {
    display: inline-block;
}

.plpitemcolors-hover-block span {
    display: block;
    text-align: center;
}

So, the key mechanism I'm using for making each item slide in is to store the containers off screen using absolute positioning, and then anchoring them to the side of their parent when that parent node is hovered over. A simple CSS3 transition value to make this happen over half a second, creates an animation that is simple and smooth. The rest is generic stuff just to make sure that elements are positioned correctly.

Create the Configuration File

As mentioned, we have two configurable settings in this functionality: the ID for the custom field used to store item color options (eg custcol_gen_color) and whether to remove the color that matches the color of the thumbnail image.

Create Configuration > PLPItemColors.json with:

{
  "type": "object"
, "subtab":
  {
    "id": "plpItemColors"
  , "title": "PLP Item Colors"
  , "group": "catalog"
  }
, "properties":
  {
    "plpItemColors.customColorId":
    {
      "group": "catalog"
    , "subtab": "plpItemColors"
    , "type": "string"
    , "title": "Custom Field ID for Color"
    }
  , "plpItemColors.rejectDefault":
    {
      "group": "catalog"
    , "subtab": "plpItemColors"
    , "type": "boolean"
    , "title": "Hide Default Color"
    , "default": false
    }
  }
}

This is standard configuration file stuff, so as long as you've done some work with configuration files before, nothing should look surprising. This'll make the configuration options show up in its own sub-tab within the Shopping Catalog tab of the configuration page.

Final Thoughts

And with all those files, you can push up the changes to your site. If you are doing it as an extension, then you'll obviously need to activate it first. When that's done, head over to a product list page, select the list display style, and then hover over some items: you should see your color options fly in from the side!

You can of course, do more to make it fit more with your site's theme. It's also not great when there are more four or five items, at least not on my site. Anyway, that stuff is up to you.

Initially, I did not think it was going to be possible to rely solely on the extensibility API, but it turns we could. The ability to get a copy of the item model was crucial for providing context to each instance of the view. I have a feeling that we're going to be relying on the contextDataRequest property a lot more in the future.

For a copy of my code, which has more lines of commentary than JavaScript, see PLPItemColors.zip.