Pre-Select Matrix Item Options After Refining a Search

After finishing my previous article on the Elbrus extensibility API prototype I got thinking about what possible practical applications we could come up with for it. I mean, the examples provided are good at showing off parts of it — but was there a real life example that we could use it for? It turns out there was as there was one idea I had been thinking about for a while. Let me explain.

There's a neat little feature I've seen on some online stores that's been around for a while but we don't have in SCA. The user journey goes something like this:

  1. As a customer, I want to buy a red shirt
  2. I go to an online store and perform a search for shirts
  3. I refine by the color red
  4. I refine by the size medium
  5. I look through the results and click on an item I like
  6. On the PDP, I select the item options (size and color)
  7. I add to cart

It's that penultimate point that got me thinking. I mean, I already refined by color and size, so why doesn't the application know what size and color I want? Why do I have select the same options again? Why can't the red and medium-sized shirt be ready for me to add to cart?

Well, why not indeed! Can we store refinements and then automatically select those matrix item options when the user visits a product detail page? Sure we can! Let's list some of things we want:

  1. Find the best place within the application to listen for the event of the user refining a search
  2. Temporarily store the user's refinement choices (eg on their device)
  3. When a user visits a PDP, go through the stored refinements and cross-check them against the ones available on the PDP; if there's a match, select them
  4. Use the new extensibility API, in particular pdp.on('afterShowContent'...) (to execute code after the page has finishing rendering) and pdp.setOption()

One extension idea that we're not doing but could be cool, if you're into it, is to build up these details in my account and save them in the backend. For example, if you're a fashion retailer you could ask your customers if they'd like to store a profile of their measurements in their account; whenever they visit, this information could be pulled down and run against the clothing items' sizes to pre-select sizes that would most likely fit them. This, however, would require a lot of work (more than I could fit into a tutorial) as well as detailed product information. Anyway, let's get started.

Basic Module Prep

Create the basic module structure. In your customizations directory, create RefinementStorer and create two sub-directories: Configuration and JavaScript. Then create the following files:

  • Configuration > RefinementStorer.json
  • JavaScript > RefinementStorer.js
  • ns.package.json

Firstly, the configuration file. We don't need any input from the site administrator, really. All we're adding in is an enable/disable switch making it easy to turn on/off. Thus, in RefinementStorer.json add:

{
  "type": "object"

, "subtab": {
    "id": "refinementStorer"
  , "group": "catalog"
  , "title": "Refinement Storer"
  }

, "properties": {
    "refinementStorer.enabled": {
      "group": "catalog"
    , "type": "boolean"
    , "subtab": "refinementStorer"
    , "title": "Enabled"
    , "default": true
    }
  }
}

This should all be clear: create a new subtab (you could rewrite this and use an existing one if you're feeling fancy) and then put a checkbox in it so you can toggle it.

In ns.package.json put:


  "gulp": {
    "javascript": [
      "JavaScript/*"
    ]
  , "configuration": [
      "Configuration/*"
    ]
  }
}

Again, simple stuff: just adding in some lines for the config and eventual JavaScript.

Next, we need to add in a skeleton to the entry point file. In RefinementStorer.js put the following:

define(
  'RefinementStorer'
, [
  ]
, function
  (
  )
{
  'use strict';

  return console.log('Refinement Storer loaded')
});

Finally, update distro.json. We need to register the module and then add it to the dependencies of shopping.js. Save and deploy. Once you've done that, you should see a little message appear in the console. Now we can move onto the code.

Store Refinements

OK so there are two parts to this: the first is figuring out the best way of getting the existing code to listen for when the user makes a refinement choice; the second is deciding the best way to store this information so it can be retrieved later.

Get the Data

For the first part, I poked around the code and toyed with some ideas. My second-best idea was to find the view which renders the facet items and then add in a listener for click events. Every time the user clicked one, it would add it to the list. There were, however, a number of problems with this:

  1. Refinements can be added in multiple ways (eg through URLs) and not just by clicking refinements
  2. Additional listeners would have to be added for the removal of refinements, thus more code
  3. Binding to click events required either editing (overriding) a number of templates to add data attributes or to use existing classes (which is unreliable and bad form)
  4. When testing the idea, I actually found it really rather difficult to implement because you need to store both the refinement value and its type (eg "color": "red")

In other words, this solution is a mess. It just makes things very complicated and hacky. Blurgh.

I then realised that there must be a part of the code that already stores this information. After all we, for example, display this information at the top of the page when a user adds refinements:

There must be a way to piggyback this, right?

Of course! After inspecting the facet sidebar I can see that it points to facets_faceted_navigation.tpl, which is used by Facets.FacetedNavigation.View.js. In the initialize method, I can see a bunch of variables being set after they've been pulled from the options parameter; one of which is appliedFacets — how handy.

As an aside, if you want to trace back where this information comes from, you can do a grep for appliedFacets and see it's in Facets.Browse.View.js, which comes from a thing called a 'translator' and then leads back to Facets.Translator.js. The translator file is essentially a utility library for facets: it provides a number of useful functions that make working with facets easier. In particular, the cloneWithoutFacetId is what generates the data we need; but it doesn't really matter: the point is that this information exists. So let's take a look at it.

In RefinementStorer.js add Facets.FacetedNavigation.View as a dependency and then add the following code above the return statement:

FacetsFacetedNavigationView.prototype.initialize = _.wrap(FacetsFacetedNavigationView.prototype.initialize, function wrappedInitialize(fn)
{
  fn.apply(this, _.toArray(arguments).slice(1));

  console.log(this.appliedFacets);
});

So what we're doing is taking the initialize function and then adding in a bit of our own code (which logs the applied facets). If you refresh the search page on your local server and then apply some facets, you should see it return an object in the console.

Neat. If we wanted to, we could strip this down — after all, we just need the ID (eg color) and value (eg orange). But to keep things simple we can pass a copy of the whole object.

But wait, how do we pass the data? Why don't we store it so that it can be retrieved later?

Store the Data

The most obvious thing to do is put it in a cookie right? Well, yes, you could. Remember, we have saved stuff to cookies before. They are a solid way of storing data locally on a user's device. But what they do, remember, is add data that is sent back to the server with each subsequent request. Let's not add more data than we need, right?

There are two alternatives that we can look at: the two parts of the web storage API. The first part is local storage, the other is session storage. This API is built into all of the browsers on our supported browsers list and allows us to store small bits of information. The difference between the local and session parts is that anything stored in the session storage is cleared when the browser is closed, whereas anything in local storage persists until it is cleared.

So out of the three choices available, I'm going to lean towards using session storage. We don't need to send this data to the server and we don't really need to keep this information longer than needed.

The session storage API is easy to use: it has getter and setter methods using key/value (or we can just treat it like an object). Thus, we can now just easily replace the console.log() statement with this:

sessionStorage.refinements = JSON.stringify(this.appliedFacets);

So we're adding an item to the session storage called refinements and then adding in the applied facets as the value. You may note that we're not using the setter method directly, but instead treating it like an object — this is fine. We are also stringifying its value: the reason being that storage can only accept strings.

If you save and refresh the search page, you'll be able to access the web storage by entering sessionStorage.refinements into the console. Doing so, results in a long string of characters, which is your saved refinements.

Awesome stuff, now we need to do something we this!

Pre-Select Matrix Item Options

At this point, we've got an object that contains the refinements the user has most recently selected following a search. On our product detail page, we then have to find a way of matching these refinements to the matrix options. From there, it's a relatively trivial task of selecting the right options.

We know from our previous dalliance with the extensibility API that we can actually pull out quite a lot of information about the item that we're currently looking at on a product detail page. In particular there is a method — .getItemInfo() that can be used to pull all the data we have about that item from the PDP component. So let's do that, shall we? Replace the return statement with the following:

return {
  mountToApp: function mountToApp(application)
  {
    var pdp = application.getComponent('PDP');
    pdp.on('afterShowContent', function(){
      console.log(pdp.getItemInfo().item.itemoptions_detail.fields);
    });
  }
}

In the console, this returns an object that looks like this:

Look familiar? Yes, here we have the matrix item options that specify not only the internal IDs of the options but also where they're sourced from. If you've set up your item options correctly in the backend, then you would've used the same lists of options and values for the facet refinements and the item options. In other words, they're sourced from the same lists.

So what do we need to do now? Well, we need to run through each of our two objects (the stored refinements and the item's matrix options) to find matches. For example, if the shopper refined by a color, does the product have that color?

For this, I had a big think about how to do this and unfortunately I think the only way to get it to work is nested loops. Here's the thing: we have two object arrays and then, within one of them, is another object array. We need three loops. With that said, let's replace the code with the following:

pdp.on('afterShowContent', function(){
  var refinements = JSON.parse(sessionStorage.refinements),
  itemOptions = pdp.getItemInfo().item.itemoptions_detail.fields;

  for (var i in refinements)
  {
    for (var j in itemOptions)
    {
      if (refinements[i].id === itemOptions[j].sourcefrom)
      {
        for (var k in itemOptions[j].values)
        {
          if (refinements[i].value === itemOptions[j].values[k].label)
          {
            pdp.setOption(itemOptions[j].internalid, itemOptions[j].values[k].internalid)
          }
        }
      }
    }
  }
});

I don't like nested loops, so if you find a better way of doing this (eg using Underscore methods) then please let me know.

Anyway, what we're doing is spinning up the stored refinements and then the item's options, then making a comparison between the ID of each stored refinement type and source of each item option. If it finds a match then it goes a level deeper in the item options object and then compares the value of that refinement type against the label of each item option value. If it finds a match in that loop then we know we can pre-select if for the user; and, for that, we use the .setOption() method.

If you save and refresh your local PDP then you should see that the last refinements you set pre-selected. If they're not, you can check a couple of things:

  1. Check the stored information in the console using JSON.parse(sessionStorage.refinements) — if there's no data, set some: either by visiting the search page again and making some refinements or by manually adding it
  2. Add some console loggers to the steps of the loops and see if it's getting stuck somewhere

After that, there isn't much left to do except to tidy up by wrapping the whole contents of the mountToApp function in a conditional:

if (!SC.isPageGenerator() && sessionStorage.refinements && Configuration.refinementStorer.enabled)

Don't forget the closing brackets and also to add SC.Configuration to the module's dependencies.

So all we're doing is making sure that:

  1. It's a human user visiting the page (robots don't have preferences)
  2. We've set refinements already
  3. An administrator has enabled the functionality

If all of that is true, then we run the code. Nice.

Final Thoughts

And that's basically it. We've implemented a module that collects the refinements a user enters and then pre-selects item options based on those preferences.

We made use of some interesting technologies to achieve this:

  1. The web storage API to store the user's preferences. We could have used cookies but this is a bit neater as we don't need to send this data to the server and we get to use a relatively new technology.
  2. The PDP's new extensibility API to add a listener for when the content has finished rendering.
  3. The extensibility API again, but this time to pull in details about the item's matrix options.
  4. And again, so that we can easily set the matrix options the user prefers.

Oh and that delicious triple loop.

I've talked about how you could take thia a step further by storing a user's information in the server. It could be a good idea if you've got a more complex problem to solve, but for preferences it's probably not worth the additional server request.

Another thing I would note is that the nature of the web storage API means that information is not shared between tabs; ie, if a user makes a preference and then opens a product in the list into another tab, they're choices won't be pre-selected. For this, then, you may wish to adopt cookies instead.

If you've gotten stuck or would like a commented version of my code, download RefinementStorer@1.0.0.zip.